7.2: directory configs can live outside their directories

DirConfDir keeps every directory's config (and history file) in one folder,
and [DirConfs] names a file for single directories, like SaraTag's Lexicons
and Records. Both are optional; without them configs stay inside the
directories as before. Clone now copies straight to the new name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
sarahandClaude Opus 5.5 committed 2026-10-08 18:09:49 +02:00
1 parent aef32f6f3b
commit 4389a1c76e
5 files changed
+97 -53

No files matched your search

+4 -1
View File
@@ -1,5 +1,5 @@
Title = "SaraSortd Config" Title = "SaraSortd Config"
Version = "7.1" Version = "7.2"
SafeMode = 1 SafeMode = 1
SilentMode = 0 SilentMode = 0
SortTimer = 1 SortTimer = 1
@@ -27,6 +27,9 @@ InputDir = [ "%RootDir%/Input" ]
OutputDir = [ "%RootDir%/Output" ] OutputDir = [ "%RootDir%/Output" ]
LogDir = "%RootDir%/.Logs" LogDir = "%RootDir%/.Logs"
FailedDir = "%RootDir%/.Failed" FailedDir = "%RootDir%/.Failed"
DirConfDir = ""
[DirConfs]
[Names] [Names]
LogFileName = "[y].[m].[d]" LogFileName = "[y].[m].[d]"
+1 -1
View File
@@ -1,5 +1,5 @@
Title = "Unset" Title = "Unset"
Version = "7.1" Version = "7.2"
ParentDir = "Unset" ParentDir = "Unset"
SortDirs = 0 SortDirs = 0
+1 -1
View File
@@ -1,5 +1,5 @@
Title = "Unset" Title = "Unset"
Version = "7.1" Version = "7.2"
LastFile = "Unset" LastFile = "Unset"
ParentDir = "Unset" ParentDir = "Unset"
ValidInputDirs = ["*"] ValidInputDirs = ["*"]
+11 -4
View File
@@ -1,6 +1,6 @@
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/SaraSortd.png" width="100%" alt="SaraSortd: a file and directory sorting daemon built with python."/> <img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/SaraSortd.png" width="100%" alt="SaraSortd: a file and directory sorting daemon built with python."/>
(Version = "7.1") (Version = "7.2")
It's made by Sarah, hence the name. :3 It's made by Sarah, hence the name. :3
@@ -58,7 +58,7 @@ Here's a step-by-step tutorial (in case you don't want to watch the video).
``` ```
Title = "SaraSortd Config" # NOTE: Do not change the title. This is so that the program knows which config file is which. Title = "SaraSortd Config" # NOTE: Do not change the title. This is so that the program knows which config file is which.
Version = "7.1" # Just the version number for the config file. Version = "7.2" # Just the version number for the config file.
SafeMode = 1 # If SaraSortd should stop if an error with the config appears. 1 is stop, 0 is continue. SafeMode = 1 # If SaraSortd should stop if an error with the config appears. 1 is stop, 0 is continue.
@@ -108,6 +108,13 @@ LogDir = "%RootDir%/.Logs" # Where log files from SaraSortd go.
FailedDir ="%RootDir%/.Failed" # For files that failed to be sorted. FailedDir ="%RootDir%/.Failed" # For files that failed to be sorted.
DirConfDir = "" # Optional (since 7.2). Where directory configs live. If this is "", every directory keeps its config inside itself. If it's set, the configs (and history files) of all directories go here instead, so the directories stay clean. Relative paths count from GlobalConf.toml's own folder. E.G: DirConfDir = "Dirs"
# Each config is named after its directory's path below RootDir, with "/" turned into "_", and InDirConfName/OutDirConfName applied without the leading ".". E.G: "%RootDir%/Pictures/Cats" gets "Pictures_CatsConfig.toml".
[DirConfs] # Optional (since 7.2). A config file for one directory, wherever you like. These win over DirConfDir. Relative paths count from GlobalConf.toml's own folder. E.G:
# "%RootDir%/Downloads" = "Configs/Downloads.toml"
# "%RootDir%/Pictures" = { In = "Configs/PicturesIn.toml", Out = "Configs/PicturesOut.toml" } # Separate input and output configs.
[Names] [Names]
LogFileName = "[y].[m].[d]" # Name for log files. LogFileName = "[y].[m].[d]" # Name for log files.
@@ -160,7 +167,7 @@ The output directory config references the SaraSortd config (GlobalConf.toml). T
``` ```
Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which. Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which.
Version = "7.1" # The version number of the config. Version = "7.2" # The version number of the config.
LastFile = "Unset" # The last file that was sorted to this directory. It gets automatically updated whenever a file gets sorted to this directory. LastFile = "Unset" # The last file that was sorted to this directory. It gets automatically updated whenever a file gets sorted to this directory.
@@ -216,7 +223,7 @@ Overwrite = 1 # Whether this specific file type can be overwritten by others of
``` ```
Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which. Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which.
Version = "7.1" Version = "7.2"
ParentDir = "Unset" # The name of this directory. It gets automatically updated when this config gets copied to an output directory. ParentDir = "Unset" # The name of this directory. It gets automatically updated when this config gets copied to an output directory.
+80 -46
View File
@@ -6,7 +6,7 @@ import shutil
import time import time
import toml import toml
# Version = "7.1" # Version = "7.2"
ConfPath = "./GlobalConf.toml" ConfPath = "./GlobalConf.toml"
@@ -189,10 +189,69 @@ def Parse(
return String return String
def ConfFolder(PathText):
# Relative paths in GlobalConf.toml (DirConfDir, [DirConfs]) count from GlobalConf.toml's own folder.
PathText = Parse(String = PathText)
if os.path.isabs(PathText):
return os.path.normpath(PathText)
return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(ConfPath)), PathText))
def FlatName(Path):
# A directory's name for files kept outside it: its path below RootDir (or its whole path), "/" turned into "_".
# E.G: "%RootDir%/Pictures/Cats" becomes "Pictures_Cats".
Root = Parse(String = ConfDirs.get("RootDir", "")).rstrip("/")
Path = Path.rstrip("/")
if Root and Path.startswith(Root + "/"):
Relative = Path[len(Root) + 1:]
else:
Relative = Path.strip("/") or os.path.basename(Path)
return Relative.replace("/", "_")
def CentralConf(Path, Kind):
# Since 7.2, a directory's config can live outside it, like SaraTag's Lexicons:
# [DirConfs] names a file per directory ("dir" = "file", or "dir" = { In = "file", Out = "file" }),
# and DirConfDir holds the configs of every directory [DirConfs] doesn't name. None means inside the directory.
for Key, Value in (Conf.get("DirConfs") or {}).items():
if Parse(String = Key).rstrip("/") == Path.rstrip("/"):
if isinstance(Value, dict):
Value = Value.get(Kind)
if Value:
return ConfFolder(Value)
if ConfDirs.get("DirConfDir"):
NameKey = "InDirConfName" if Kind == "In" else "OutDirConfName"
Name = Parse(String = ConfNames[NameKey], Parent = FlatName(Path)).lstrip(".")
return f"{ConfFolder(ConfDirs['DirConfDir'])}/{Name}.toml"
return None
def DirConfPath(Path, Kind):
# The config of an input ("In") or output ("Out") directory.
Central = CentralConf(Path, Kind)
if Central:
return Central
NameKey = "InDirConfName" if Kind == "In" else "OutDirConfName"
return f"{Path}/{Parse(String = ConfNames[NameKey], Path = ConfPath, Parent = os.path.basename(Path))}.toml"
def HistoryPath(Path):
# The history file of a directory that sorts into itself. It goes next to the directory's config
# when that's kept outside the directory.
Central = CentralConf(Path, "In") or CentralConf(Path, "Out")
if Central:
Name = Parse(String = ConfNames["FileHistoryName"], Parent = FlatName(Path)).lstrip(".")
return f"{os.path.dirname(Central)}/{Name}.toml"
return f"{Path}/{Parse(String = ConfNames['FileHistoryName'], Parent = os.path.basename(Path))}.toml"
def Clone(Source, Destination, NewName, Delete = False): def Clone(Source, Destination, NewName, Delete = False):
if (os.path.dirname(Source) == Destination): if (os.path.dirname(Source) == Destination):
FileHistoryPath = f"{Destination}/{Parse(ConfNames["FileHistoryName"], Parent = os.path.basename(Destination))}.toml" FileHistoryPath = HistoryPath(Destination)
print("FileHistoryPath:",FileHistoryPath)
if not os.path.exists(FileHistoryPath): if not os.path.exists(FileHistoryPath):
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile: with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
@@ -208,10 +267,9 @@ def Clone(Source, Destination, NewName, Delete = False):
return return
Dir(Destination, Output = False, CopyConf = False) Dir(Destination, Output = False, CopyConf = False)
shutil.copy2(Source, Destination) # Straight to the new name: copying under the old name first could land on a file that's already
# there (E.G. a template, when DirConfDir is GlobalConf.toml's own folder).
BaseName = os.path.basename(Source) shutil.copy2(Source, os.path.join(Destination, NewName))
shutil.move(os.path.join(Destination, BaseName), os.path.join(Destination, NewName))
if Delete: if Delete:
os.remove(Source) os.remove(Source)
@@ -222,39 +280,16 @@ def Dir(Path, Output = True, CopyConf = "OutputDirConf.toml"):
os.makedirs(Path, exist_ok=True) os.makedirs(Path, exist_ok=True)
if CopyConf: if CopyConf:
if "Out" in CopyConf: for Kind in ("Out", "In"):
NewDirConf = f"{Parse(String = ConfNames['OutDirConfName'], Path = ConfPath, Parent = ParentName)}.toml" if Kind not in CopyConf:
continue
if Conf["OverwriteDirConf"] == 1 or not os.path.exists(os.path.join(f"{Path}/{NewDirConf}")): DirConf = DirConfPath(Path, Kind)
Clone(CopyConf, Path, NewDirConf)
UpdateConf(
f"{Path}/{Parse(String = ConfNames['OutDirConfName'], Path = ConfPath, Parent = ParentName)}.toml",
"ParentDir",
ParentName,
)
UpdateConf( if Conf["OverwriteDirConf"] == 1 or not os.path.exists(DirConf):
f"{Path}/{Parse(String = ConfNames['OutDirConfName'], Path = ConfPath, Parent = ParentName)}.toml", Clone(CopyConf, os.path.dirname(DirConf), os.path.basename(DirConf))
"Title", UpdateConf(DirConf, "ParentDir", ParentName)
f"{ParentName} Config", UpdateConf(DirConf, "Title", f"{ParentName} Config")
)
if "In" in CopyConf:
NewDirConf = f"{Parse(String = ConfNames['InDirConfName'], Path = ConfPath, Parent = ParentName)}.toml"
if Conf["OverwriteDirConf"] == 1 or not os.path.exists(os.path.join(f"{Path}/{NewDirConf}")):
Clone(CopyConf, Path, NewDirConf)
UpdateConf(
f"{Path}/{Parse(String = ConfNames['InDirConfName'], Path = ConfPath, Parent = ParentName)}.toml",
"ParentDir",
ParentName,
)
UpdateConf(
f"{Path}/{Parse(String = ConfNames['InDirConfName'], Path = ConfPath, Parent = ParentName)}.toml",
"Title",
f"{ParentName} Config",
)
if Output: if Output:
ParsedPath = Parse(String = Path) ParsedPath = Parse(String = Path)
@@ -299,8 +334,7 @@ def DecideNewPath(FilePath):
ParentName = os.path.basename(Output) ParentName = os.path.basename(Output)
OutDirConfName = Parse(String = ConfNames["OutDirConfName"], Path = ConfPath, Parent = ParentName) OutDirConfPath = DirConfPath(Output, "Out")
OutDirConfPath = f"{Output}/{OutDirConfName}.toml"
ValidInputDirs = GetConf("ValidInputDirs", OutDirConfPath) or [] ValidInputDirs = GetConf("ValidInputDirs", OutDirConfPath) or []
InputDirValid = False InputDirValid = False
@@ -429,7 +463,7 @@ def Sort(FilePath):
NewDirPath = os.path.dirname(NewPath) NewDirPath = os.path.dirname(NewPath)
NewName = os.path.basename(NewPath) NewName = os.path.basename(NewPath)
if (GetConf("DeleteOrg", f"{NewDirPath}/{Parse(String = ConfNames['OutDirConfName'], Parent = (os.path.basename(NewDirPath)))}.toml") == 1): if (GetConf("DeleteOrg", DirConfPath(NewDirPath, "Out")) == 1):
Delete = True Delete = True
else: else:
@@ -437,7 +471,7 @@ def Sort(FilePath):
try: try:
Clone(FilePath, NewDirPath, NewName, Delete) Clone(FilePath, NewDirPath, NewName, Delete)
UpdateConf(f"{NewDirPath}/{Parse(String = ConfNames['OutDirConfName'], Parent = os.path.basename(NewDirPath))}.toml", "LastFile", NewName) UpdateConf(DirConfPath(NewDirPath, "Out"), "LastFile", NewName)
TextOutput = Parse(String = ConfLog["Sorted"], VarCall = f"{FilePath} to {NewDirPath}/{NewName}") TextOutput = Parse(String = ConfLog["Sorted"], VarCall = f"{FilePath} to {NewDirPath}/{NewName}")
LogWrite(TextOutput) LogWrite(TextOutput)
@@ -470,7 +504,7 @@ def Init():
Output = Parse(String = Output, Parent = os.path.basename(os.path.dirname(Output))) Output = Parse(String = Output, Parent = os.path.basename(os.path.dirname(Output)))
Dir(Output) Dir(Output)
OutDirConfPath = f"{Output}/{Parse(String = ConfNames["OutDirConfName"], Parent = os.path.basename(Output))}.toml" OutDirConfPath = DirConfPath(Output, "Out")
ConfData = toml.load(OutDirConfPath) ConfData = toml.load(OutDirConfPath)
for Item in ConfData.get("Files", []): for Item in ConfData.get("Files", []):
@@ -509,12 +543,12 @@ def Main():
if not os.path.exists(InputPath): if not os.path.exists(InputPath):
continue continue
InputDirConfPath = f"{InputPath}/{Parse(ConfNames["InDirConfName"], Parent = os.path.basename(InputPath))}.toml" InputDirConfPath = DirConfPath(InputPath, "In")
InputDirConf = toml.load(InputDirConfPath) InputDirConf = toml.load(InputDirConfPath)
for Output in ConfDirs["OutputDir"]: for Output in ConfDirs["OutputDir"]:
OutputPath = Parse(Output) OutputPath = Parse(Output)
OutDirConfPath = f"{OutputPath}/{Parse(ConfNames["OutDirConfName"], Parent = os.path.basename(OutputPath))}.toml" OutDirConfPath = DirConfPath(OutputPath, "Out")
if len(GetConf("Unzip", OutDirConfPath)) >= 1: if len(GetConf("Unzip", OutDirConfPath)) >= 1:
ZipTypes = GetConf("Unzip", OutDirConfPath) ZipTypes = GetConf("Unzip", OutDirConfPath)
@@ -541,7 +575,7 @@ def Main():
if (OutputPath == InputPath): if (OutputPath == InputPath):
RecurseDir = True RecurseDir = True
FileHistoryPath = f"{InputPath}/{Parse(ConfNames["FileHistoryName"], Parent = os.path.basename(InputPath))}.toml" FileHistoryPath = HistoryPath(InputPath)
if not os.path.exists(FileHistoryPath): if not os.path.exists(FileHistoryPath):
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile: with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile: