diff --git a/GlobalConf.toml b/GlobalConf.toml index 3eaee6e..3cc7022 100644 --- a/GlobalConf.toml +++ b/GlobalConf.toml @@ -1,5 +1,5 @@ Title = "SaraSortd Config" -Version = "7.1" +Version = "7.2" SafeMode = 1 SilentMode = 0 SortTimer = 1 @@ -27,6 +27,9 @@ InputDir = [ "%RootDir%/Input" ] OutputDir = [ "%RootDir%/Output" ] LogDir = "%RootDir%/.Logs" FailedDir = "%RootDir%/.Failed" +DirConfDir = "" + +[DirConfs] [Names] LogFileName = "[y].[m].[d]" diff --git a/InputDirConf.toml b/InputDirConf.toml index d1db176..32d501c 100644 --- a/InputDirConf.toml +++ b/InputDirConf.toml @@ -1,5 +1,5 @@ Title = "Unset" -Version = "7.1" +Version = "7.2" ParentDir = "Unset" SortDirs = 0 diff --git a/OutputDirConf.toml b/OutputDirConf.toml index 84c9d8f..4a60163 100644 --- a/OutputDirConf.toml +++ b/OutputDirConf.toml @@ -1,5 +1,5 @@ Title = "Unset" -Version = "7.1" +Version = "7.2" LastFile = "Unset" ParentDir = "Unset" ValidInputDirs = ["*"] diff --git a/README.md b/README.md index ff3fb87..1eb7109 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ 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 @@ -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. -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. @@ -108,6 +108,13 @@ LogDir = "%RootDir%/.Logs" # Where log files from SaraSortd go. 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] 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. -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. @@ -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. -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. diff --git a/SaraSortd.py b/SaraSortd.py index bb24df5..bd1875b 100644 --- a/SaraSortd.py +++ b/SaraSortd.py @@ -6,7 +6,7 @@ import shutil import time import toml -# Version = "7.1" +# Version = "7.2" ConfPath = "./GlobalConf.toml" @@ -189,10 +189,69 @@ def Parse( 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): if (os.path.dirname(Source) == Destination): - FileHistoryPath = f"{Destination}/{Parse(ConfNames["FileHistoryName"], Parent = os.path.basename(Destination))}.toml" - print("FileHistoryPath:",FileHistoryPath) + FileHistoryPath = HistoryPath(Destination) if not os.path.exists(FileHistoryPath): with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile: @@ -208,10 +267,9 @@ def Clone(Source, Destination, NewName, Delete = False): return Dir(Destination, Output = False, CopyConf = False) - shutil.copy2(Source, Destination) - - BaseName = os.path.basename(Source) - shutil.move(os.path.join(Destination, BaseName), os.path.join(Destination, NewName)) + # 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). + shutil.copy2(Source, os.path.join(Destination, NewName)) if Delete: os.remove(Source) @@ -222,39 +280,16 @@ def Dir(Path, Output = True, CopyConf = "OutputDirConf.toml"): os.makedirs(Path, exist_ok=True) if CopyConf: - if "Out" in CopyConf: - NewDirConf = f"{Parse(String = ConfNames['OutDirConfName'], Path = ConfPath, Parent = ParentName)}.toml" + for Kind in ("Out", "In"): + if Kind not in CopyConf: + continue - 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['OutDirConfName'], Path = ConfPath, Parent = ParentName)}.toml", - "ParentDir", - ParentName, - ) + DirConf = DirConfPath(Path, Kind) - UpdateConf( - f"{Path}/{Parse(String = ConfNames['OutDirConfName'], Path = ConfPath, Parent = ParentName)}.toml", - "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 Conf["OverwriteDirConf"] == 1 or not os.path.exists(DirConf): + Clone(CopyConf, os.path.dirname(DirConf), os.path.basename(DirConf)) + UpdateConf(DirConf, "ParentDir", ParentName) + UpdateConf(DirConf, "Title", f"{ParentName} Config") if Output: ParsedPath = Parse(String = Path) @@ -299,8 +334,7 @@ def DecideNewPath(FilePath): ParentName = os.path.basename(Output) - OutDirConfName = Parse(String = ConfNames["OutDirConfName"], Path = ConfPath, Parent = ParentName) - OutDirConfPath = f"{Output}/{OutDirConfName}.toml" + OutDirConfPath = DirConfPath(Output, "Out") ValidInputDirs = GetConf("ValidInputDirs", OutDirConfPath) or [] InputDirValid = False @@ -429,7 +463,7 @@ def Sort(FilePath): NewDirPath = os.path.dirname(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 else: @@ -437,7 +471,7 @@ def Sort(FilePath): try: 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}") LogWrite(TextOutput) @@ -470,7 +504,7 @@ def Init(): Output = Parse(String = Output, Parent = os.path.basename(os.path.dirname(Output))) Dir(Output) - OutDirConfPath = f"{Output}/{Parse(String = ConfNames["OutDirConfName"], Parent = os.path.basename(Output))}.toml" + OutDirConfPath = DirConfPath(Output, "Out") ConfData = toml.load(OutDirConfPath) for Item in ConfData.get("Files", []): @@ -509,12 +543,12 @@ def Main(): if not os.path.exists(InputPath): continue - InputDirConfPath = f"{InputPath}/{Parse(ConfNames["InDirConfName"], Parent = os.path.basename(InputPath))}.toml" + InputDirConfPath = DirConfPath(InputPath, "In") InputDirConf = toml.load(InputDirConfPath) for Output in ConfDirs["OutputDir"]: 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: ZipTypes = GetConf("Unzip", OutDirConfPath) @@ -541,7 +575,7 @@ def Main(): if (OutputPath == InputPath): RecurseDir = True - FileHistoryPath = f"{InputPath}/{Parse(ConfNames["FileHistoryName"], Parent = os.path.basename(InputPath))}.toml" + FileHistoryPath = HistoryPath(InputPath) if not os.path.exists(FileHistoryPath): with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile: