mirror of
https://git.srhtnin.garden/sarah/SaraSortd.git
synced 2026-10-13 18:38:11 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6a5b28631 | ||
|
|
4a52120b1a | ||
|
|
234889ba6d | ||
|
|
4389a1c76e | ||
|
|
aef32f6f3b | ||
|
|
78216fc352 | ||
|
|
16e6b7196e | ||
|
|
ec9e97e099 | ||
|
|
70a36d8b96 | ||
|
|
19ceec218f | ||
|
|
2cc72b1730 | ||
|
|
2bf4457981 | ||
|
|
5c39166a66 | ||
|
|
6a6e7dd494 | ||
|
|
a83b2e3c94 | ||
|
|
9be5938a87 | ||
|
|
4e00617e09 | ||
|
|
c5612654f8 | ||
|
|
324d06d6b4 | ||
|
|
b5afad3895 | ||
|
|
a314614055 | ||
|
|
3aa28f76cf | ||
|
|
89f4e59041 | ||
|
|
dc60a9828e | ||
|
|
4901d346f2 | ||
|
|
b04c3426a7 | ||
|
|
15ced89964 | ||
|
|
475eda8dd2 | ||
|
|
e9974f90ee | ||
|
|
68e33861a9 | ||
|
|
f0b2ec62ef | ||
|
|
37af52eaa0 | ||
|
|
2afadaebc8 | ||
|
|
84d61d9381 | ||
|
|
0f2f6673aa | ||
|
|
f2bc2abf9c | ||
|
|
5933dd94d2 | ||
|
|
24c52c17ce | ||
|
|
fd4f7256df | ||
|
|
d43085de71 | ||
|
|
dd00cf0780 | ||
|
|
51c557d228 | ||
|
|
e91ce510fc | ||
|
|
d68550d0a9 | ||
|
|
9fbdec9120 | ||
|
|
0b54a729ff | ||
|
|
a3bc726086 | ||
|
|
739f91a5bf | ||
|
|
fc6f9bd399 | ||
|
|
077cbbb2c1 | ||
|
|
ffd95fdfce | ||
|
|
49b5549b1d | ||
|
|
cbf1a4049c |
No files matched your search
@@ -1,138 +0,0 @@
|
|||||||
# SaraSortd
|
|
||||||
A python daemon that can automatically sort your files to your liking without using AI.
|
|
||||||
|
|
||||||
It's made by Sarah, hence the name. :3
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
toml: Use ```pip install toml``` to install toml.
|
|
||||||
|
|
||||||
## Config explanation
|
|
||||||
|
|
||||||
### SaraSortd Config (Config.toml)
|
|
||||||
|
|
||||||
```
|
|
||||||
Title = "SaraSortd Config" # NOTE: Do not change the title.
|
|
||||||
|
|
||||||
SafeMode = 1 # If SaraSortd should stop if an error with the config appears. 1 is stop, 0 is continue.
|
|
||||||
|
|
||||||
SilentMode = 0 # If SaraSortd should write things it does, to the terminal.
|
|
||||||
|
|
||||||
CheckInput = 3 # How many seconds it takes for SaraSortd to check input directories for new files to sort.
|
|
||||||
|
|
||||||
DotFiles = 0 # If SaraSortd should sort files that begin with ".". This is mainly for linux users, since hidden files always have "." in front of them.
|
|
||||||
|
|
||||||
Logging = 1 # If SaraSortd's logging system should be active. If this is 0, nothing will be logged into files.
|
|
||||||
|
|
||||||
OverwriteDirConf = 0 # If SaraSortd should overwrite directory configs with DirConfig.toml. 0 is no, 1 is yes.
|
|
||||||
|
|
||||||
[Variables] # Only use these in config files, not the actual file names themselves.
|
|
||||||
NextNum = "*" # The next available number. If 1 already exists, 2 will be used. Starts at 1.
|
|
||||||
|
|
||||||
NextChar = "&" # The next available character. If A already exists, B will be used.
|
|
||||||
|
|
||||||
Parent = "@" # The name of the parent directory.
|
|
||||||
|
|
||||||
OrgFileName = "\~" # The original name of the sorted file. If "blahblah.txt" gets sorted, "~" would be equal to "blahblah".
|
|
||||||
|
|
||||||
OrgFileType = "^" # The original file type of the sorted file. If "example.png" gets sorted, "^" would be equal to ".png".
|
|
||||||
|
|
||||||
Year = "[y]" # The current year, E.G. "2026".
|
|
||||||
|
|
||||||
Month = "[m]" # The date of the current month, E.G. "03".
|
|
||||||
|
|
||||||
Day = "[d]" # The date of the current day, E.G. "27".
|
|
||||||
|
|
||||||
Hour = "[h]" # The current hour, E.G. "12".
|
|
||||||
|
|
||||||
Minute = "[n]" # The current minute, E.G. "44".
|
|
||||||
|
|
||||||
Second = "[s]" # The current second, E.G. "11".
|
|
||||||
|
|
||||||
VarCall = "%%" # The characters used at the start and end of variable calls. The first character is to the left; second is to the right. NOTE: If any parameters in the config uses just "%%", it gets a variable from the program itself, not from the config.
|
|
||||||
|
|
||||||
[DirectoryPaths]
|
|
||||||
RootDir = "Unset" # A variable directory. This doesn't NEED to exist for the program to work. It's more of a shorthand for other paths to use! You could add more of these. E.G: Add ImageDir = "%RootDir%/Images", then you could set OutputDir to be "%ImageDir%".
|
|
||||||
|
|
||||||
InputDir = ["%RootDir%"] # If files get put here, they'll be sorted. This is a list, so you can add multiple. E.G: ["%RootDir%", "%RootDir%/input"]
|
|
||||||
|
|
||||||
OutputDir = ["%RootDir%/Output"] # Where sorted files go. This is a list, so you can add multiple. E.G: ["%RootDir%/Output", "%RootDir%/Output2"]. The output directories have separate config files.
|
|
||||||
|
|
||||||
LogDir = "%RootDir%/.Logs" # Where log files from SaraSortd go.
|
|
||||||
|
|
||||||
FailedDir ="%RootDir%/.Failed" # For files that failed to be sorted.
|
|
||||||
|
|
||||||
[Names]
|
|
||||||
LogFileName = "[y].[m].[d]._*" # Name for log files.
|
|
||||||
|
|
||||||
DirConfName = ".@Config" # Name for config files for directories.
|
|
||||||
|
|
||||||
[Log]
|
|
||||||
All = "[h]:[n]:[s] | " # What to put at the start of all lines in Log file.
|
|
||||||
|
|
||||||
Start = "Started: SaraSortd" # When SaraSortd starts.
|
|
||||||
|
|
||||||
NotStart = "Error: Failed to start SaraSortd." # When SaraSortd fails to start for some reason.
|
|
||||||
|
|
||||||
Stop = "\nStopped SaraSortd." # When SaraSortd stops for whatever reason. Usually when you interrupt it in the terminal you ran it in.
|
|
||||||
|
|
||||||
MatchPattern = "Checking %%." # When a file that's trying to be sorted gets checked against a pattern.
|
|
||||||
# %% here is the file against the pattern. E.G: "picture.png against *.jpg"
|
|
||||||
|
|
||||||
Sorted = "Sorted: %%." # When a file gets sorted and where.
|
|
||||||
# %% here is the oldfile to the newfile, E.G: "picture.png to SortedPicture.png".
|
|
||||||
|
|
||||||
NotSorted = "Error: Failed to sort %%. No valid output directory." # When a file from an input directory fails to be sorted to an output directory.
|
|
||||||
# %% here is the path of the file. E.G: "/home/janedoe/picture.png".
|
|
||||||
|
|
||||||
ValueSet = "Set: %%." # When a value in a config is set.
|
|
||||||
# %% here is the parameter and what it got set to. E.G: "Title to TestDir Config File".
|
|
||||||
|
|
||||||
Unset = "Error: %% is not set." # When a value isn't set.
|
|
||||||
# %% here is just the parameter who's value was either "Unset" or None. E.G: "RootDir".
|
|
||||||
```
|
|
||||||
|
|
||||||
### Output Directory Config (DirConfig.toml)
|
|
||||||
|
|
||||||
The output directory config references the SaraSortd config (Config.toml). This means that you can, for example, use the character assigned to "NextNum" in Config.toml in this config (DirConfig.toml). This applies to all Variables in the SaraSortd config (Config.toml).
|
|
||||||
|
|
||||||
```
|
|
||||||
Title = "Unset" # The title of the config. This currently does nothing for SaraSortd, so it can be anything. It gets automatically updated when DirConfig.toml gets copied to an output directory.
|
|
||||||
|
|
||||||
LastFile = "Unset" # The last file that was sorted to this directory. It gets automatically updated whenever a file gets sorted to this directory.
|
|
||||||
|
|
||||||
ParentDir = "Unset" # The name of this directory. It gets automatically updated when DirConfig.toml gets copied to an output directory.
|
|
||||||
|
|
||||||
ValidInputDirs = ["*"] # Directories that are valid for sorting. Basically, if a file is from one of these directories, they can be sorted here. * is a wildcard; ? is a one-character wilcard. E.G: if "ValidInputDirs = ["/TestDir/*"]" any file from any directory in "/TestDir" will be allowed to be sorted here.
|
|
||||||
|
|
||||||
FileLimit = 0
|
|
||||||
|
|
||||||
[[Files]] # The double brackets means that you can add multiple types of files to be sorted here. Just copy [[Files]] and all the parameters under it, and paste it under or over this section, then you may change the values of the new parameters. E.G:
|
|
||||||
|
|
||||||
# [[Files]]
|
|
||||||
# Pattern = "*.png"
|
|
||||||
# NewFileName = "GoodImage^"
|
|
||||||
# NextNum = 1
|
|
||||||
# NextChar = "A"
|
|
||||||
# CaseSensitive = 0
|
|
||||||
# [[Files]]
|
|
||||||
# Pattern = "*.jpg"
|
|
||||||
# NewFileName = "BetterImage^"
|
|
||||||
# NextNum = 1
|
|
||||||
# NextChar = "A"
|
|
||||||
# CaseSensitive = 0
|
|
||||||
# Overwrite = 1
|
|
||||||
|
|
||||||
# Will sort .png files to this directory and rename them to "GoodImage.png", but also sort .jpg files into this directory yet rename those to "BetterImage.jpg".
|
|
||||||
|
|
||||||
Pattern = "*" # A pattern used to sort files to this directory. * is a wildcard; ? is a one-character wildcard. E.G: if Pattern is "*.png", all files ending with .png will end up here.
|
|
||||||
|
|
||||||
NewFileName = "~^" # A pattern for the new names of the files that get sorted to this directory from matching this "Pattern".
|
|
||||||
|
|
||||||
NextNum = 1 # Used to keep track of the next available number for file names. It gets automatically incremented when NextNum is used in NewFileName, and a file gets sorted to this directory and renamed.
|
|
||||||
|
|
||||||
NextChar = "A" # Used to keep track of the next available letter for file names. It gets automatically incremented (E.G: A into B) when NextChar is used in NewFileName, and a file gets sorted to this directory and renamed.
|
|
||||||
|
|
||||||
CaseSensitive = 0 # Whether this "Pattern" is case-sensitive or not. 1 is yes, 0 is no. E.G: if CaseSensitive = 1 and this "Pattern" is "*.PNG", a file named "picture.png" won't get sorted to this directory. If CaseSensitive = 0 and this "Pattern" is "*.PNG", a file named "picture.png" *will* get sorted to this directory.
|
|
||||||
|
|
||||||
Overwrite = 1 # Whether this specific file type can be overwritten by others of the same type. 0 is no, 1 is yes.
|
|
||||||
```
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
Title = "SaraSortd Config"
|
Title = "SaraSortd Config"
|
||||||
|
Version = "7.5"
|
||||||
SafeMode = 1
|
SafeMode = 1
|
||||||
SilentMode = 0
|
SilentMode = 0
|
||||||
CheckInput = 3
|
SortTimer = 1
|
||||||
DotFiles = 0
|
DotFiles = 0
|
||||||
Logging = 1
|
Logging = 1
|
||||||
OverwriteDirConf = 0
|
OverwriteDirConf = 0
|
||||||
@@ -26,18 +27,26 @@ 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]"
|
||||||
DirConfName = ".@Config"
|
InDirConfName = ".@Config"
|
||||||
|
OutDirConfName = ".@Config"
|
||||||
|
FileHistoryName = ".@History"
|
||||||
|
|
||||||
[Log]
|
[Log]
|
||||||
All = "[h]:[n]:[s] | "
|
All = "[h]:[n]:[s] | "
|
||||||
Start = "Started SaraSortd."
|
Start = "Started SaraSortd."
|
||||||
NotStart = "Error: Failed to start SaraSortd."
|
NotStart = "Error: Failed to start SaraSortd."
|
||||||
Stop = "\nStopped SaraSortd."
|
Stop = "Stopped SaraSortd.\n"
|
||||||
MatchPattern = "Checking %%."
|
MatchPattern = "Checking %%."
|
||||||
Sorted = "Sorted %%."
|
Sorted = "Sorted %%."
|
||||||
NotSorted = "Error: Failed to sort %%. No valid output directory."
|
NotSorted = "Error: Failed to sort %%. No valid output directory."
|
||||||
ValueSet = "Set %%."
|
ValueSet = "Set %%."
|
||||||
Unset = "Error: %% is not set."
|
Unset = "Error: %% is not set."
|
||||||
|
Zipping = "Zipping: %%."
|
||||||
|
Unzipping = "Unzipping: %%."
|
||||||
|
NoPermission = "Error: No permission to %%."
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Title = "Unset"
|
||||||
|
Version = "7.5"
|
||||||
|
ParentDir = "Unset"
|
||||||
|
SortDirs = 0
|
||||||
|
|
||||||
|
[[Files]]
|
||||||
|
Pattern = "*"
|
||||||
|
CaseSensitive = 0
|
||||||
File renamed without changes.
@@ -1,13 +1,16 @@
|
|||||||
Title = "Unset"
|
Title = "Unset"
|
||||||
|
Version = "7.5"
|
||||||
LastFile = "Unset"
|
LastFile = "Unset"
|
||||||
ParentDir = "Unset"
|
ParentDir = "Unset"
|
||||||
ValidInputDirs = ["*"]
|
ValidInputDirs = ["*"]
|
||||||
FileLimit = 0
|
FileLimit = 0
|
||||||
|
DeleteOrg = 1
|
||||||
|
Unzip = []
|
||||||
|
|
||||||
[[Files]]
|
[[Files]]
|
||||||
Pattern = "*"
|
Pattern = "*"
|
||||||
NewFileName = "~^"
|
NewFileName = "~^"
|
||||||
NextNum = 1
|
NextNum = "1"
|
||||||
NextChar = "A"
|
NextChar = "A"
|
||||||
CaseSensitive = 0
|
CaseSensitive = 0
|
||||||
Overwrite = 1
|
Overwrite = 1
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
<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.5")
|
||||||
|
|
||||||
|
|
||||||
|
It's made by Sarah, hence the name. :3
|
||||||
|
|
||||||
|
If the first number in the version goes up, it means that old config files will not work with the new SaraSortd.py.
|
||||||
|
If the second number in the version goes up, something new got added, but your old config files will still work just fine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/Usage.png" width="100%" alt="Usage"/>
|
||||||
|
|
||||||
|
|
||||||
|
SaraSortd is for people who don't want to manually sort files, people who want to organise files, and people who want an automatic naming standard for their files.
|
||||||
|
|
||||||
|
Example use-cases:
|
||||||
|
|
||||||
|
- Sorting log files from multiple programs into one directory.
|
||||||
|
|
||||||
|
- Sorting downloaded github repositories to a directory for your IDE.
|
||||||
|
|
||||||
|
- Regularly cloning important files into a backup directory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/Dependencies.png" width="100%" alt="Dependencies"/>
|
||||||
|
|
||||||
|
|
||||||
|
python
|
||||||
|
|
||||||
|
toml: Use ```pip install toml``` to install toml.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/Installation.png" width="100%" alt="Installation"/>
|
||||||
|
|
||||||
|
|
||||||
|
Here's an [installation video](https://youtu.be/QkMZVSJ-PLQ?si=Nd20jDyHT7CN6Ztb) on Linux.
|
||||||
|
|
||||||
|
Here's a step-by-step tutorial (in case you don't want to watch the video).
|
||||||
|
|
||||||
|
1. Install ZIP-file by clicking the green "Code" button on the top right, then clicking "Download Zip".
|
||||||
|
2. Unzip ZIP-file wherever you want the program to live.
|
||||||
|
3. Edit your config files to your liking. Make sure to edit your paths! (E.G. RootDir = "/home/janedoe/Programs/SaraSortd")
|
||||||
|
4. Navigate to the new SaraSortd directory in your terminal (CMD / PowerShell on Windows).
|
||||||
|
5. Run ```python SaraSortd.py``` to run the program. (This will make new directories for you.)
|
||||||
|
6. Edit your new output directories' configs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/ConfigInfo.png" width="100%" alt="Config Info"/>
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/GlobalConf.png" width="100%" alt="GlobalConf"/>
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
Title = "SaraSortd Config" # NOTE: Do not change the title. This is so that the program knows which config file is which.
|
||||||
|
|
||||||
|
Version = "7.5" # 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.
|
||||||
|
|
||||||
|
SilentMode = 0 # If SaraSortd should write things it does, to the terminal.
|
||||||
|
|
||||||
|
SortTimer = 3 # How many seconds it takes for SaraSortd to check input directories for new files to sort.
|
||||||
|
|
||||||
|
DotFiles = 0 # If SaraSortd should sort files that begin with ".". This is mainly for linux users, since hidden files always have "." in front of them.
|
||||||
|
# Files that are still downloading are never sorted or even checked (since 7.4): anything ending in .part (Firefox), .crdownload (Chrome and other Chromium browsers) or .download (Safari), and a file next to its own unfinished download (like the empty "video.mp4" Firefox makes beside "video.mp4.part"). They're sorted as usual once the download finishes.
|
||||||
|
|
||||||
|
Logging = 1 # If SaraSortd's logging system should be active. If this is 0, nothing will be logged into files.
|
||||||
|
|
||||||
|
OverwriteDirConf = 0 # If SaraSortd should overwrite directory configs with the config templates. 0 is no, 1 is yes.
|
||||||
|
|
||||||
|
[Variables] # Only use these in config files, not the actual file names themselves.
|
||||||
|
# Directory paths (InputDir, OutputDir, LogDir, FailedDir, ValidInputDirs, DirConfDir, [DirConfs]) only use %Name% lookups and the date and time variables (since 7.3). The single-character variables below are about the file being sorted, so a path like "%RootDir%/Stuff&Things" stays as it is. A "~" at the start of a path is your home directory.
|
||||||
|
NextNum = "!" # The next available number. If 1 already exists, 2 will be used.
|
||||||
|
|
||||||
|
NextChar = "&" # The next available character. If A already exists, B will be used.
|
||||||
|
|
||||||
|
Parent = "@" # The name of the parent directory.
|
||||||
|
|
||||||
|
OrgFileName = "~" # The original name of the sorted file. If "blahblah.txt" gets sorted, "~" would be equal to "blahblah".
|
||||||
|
|
||||||
|
OrgFileType = "^" # The original file type of the sorted file. If "example.png" gets sorted, "^" would be equal to ".png".
|
||||||
|
|
||||||
|
Year = "[y]" # The current year, E.G. "2026".
|
||||||
|
|
||||||
|
Month = "[m]" # The date of the current month, E.G. "03".
|
||||||
|
|
||||||
|
Day = "[d]" # The date of the current day, E.G. "27".
|
||||||
|
|
||||||
|
Hour = "[h]" # The current hour, E.G. "12".
|
||||||
|
|
||||||
|
Minute = "[n]" # The current minute, E.G. "44".
|
||||||
|
|
||||||
|
Second = "[s]" # The current second, E.G. "11".
|
||||||
|
|
||||||
|
VarCall = "%%" # The characters used at the start and end of variable calls. The first character is to the left; second is to the right. NOTE: If any parameters in the config uses just "%%", it gets a variable from the program itself, not from the config.
|
||||||
|
|
||||||
|
[DirectoryPaths]
|
||||||
|
RootDir = "Unset" # A variable directory. This doesn't NEED to exist for the program to work. It's more of a shorthand for other paths to use! You could add more of these. E.G: Add ImageDir = "%RootDir%/Images", then you could set OutputDir to be "%ImageDir%".
|
||||||
|
|
||||||
|
InputDir = ["%RootDir%"] # If files get put here, they'll be sorted. This is a list, so you can add multiple. E.G: ["%RootDir%", "%RootDir%/input"]
|
||||||
|
|
||||||
|
OutputDir = ["%RootDir%/Output"] # Where sorted files go. This is a list, so you can add multiple. E.G: ["%RootDir%/Output", "%RootDir%/Output2"]. The output directories have separate config files.
|
||||||
|
# Outputs are tried in this order, first with their specific patterns. Catch-all patterns ("*") are only a fallback for files no specific pattern took (since 7.3), so an output that takes everything can be anywhere in the list.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
InDirConfName = ".@Config" # Name for config files for input directories.
|
||||||
|
|
||||||
|
OutDirConfName = ".@Config" # Name for config files for output directories. If a directory is both an input and an output and both names are the same, it uses one config file for both: its [[Files]] are then both the input's and the output's rules, and SaraSortd gives the file every key both roles need (since 7.3).
|
||||||
|
|
||||||
|
FileHistoryName = ".@History" # Name for file history files. This keeps track of sorted files if an input and output are the same directory.
|
||||||
|
|
||||||
|
[Log]
|
||||||
|
All = "[h]:[n]:[s] | " # What to put at the start of all lines in Log file.
|
||||||
|
|
||||||
|
Start = "Started: SaraSortd" # When SaraSortd starts.
|
||||||
|
|
||||||
|
NotStart = "Error: Failed to start SaraSortd." # When SaraSortd fails to start for some reason.
|
||||||
|
|
||||||
|
Stop = "\nStopped SaraSortd." # When SaraSortd stops for whatever reason. Usually when you interrupt it in the terminal you ran it in.
|
||||||
|
|
||||||
|
MatchPattern = "Checking %%." # When a file that's trying to be sorted gets checked against a pattern.
|
||||||
|
# %% here is the file against the pattern. E.G: "picture.png against *.jpg"
|
||||||
|
|
||||||
|
Sorted = "Sorted: %%." # When a file gets sorted and where.
|
||||||
|
# %% here is the oldfile to the newfile, E.G: "picture.png to SortedPicture.png".
|
||||||
|
|
||||||
|
NotSorted = "Error: Failed to sort %%. No valid output directory." # When a file from an input directory fails to be sorted to an output directory.
|
||||||
|
# %% here is the path of the file. E.G: "/home/janedoe/picture.png".
|
||||||
|
|
||||||
|
ValueSet = "Set: %%." # When a value in a config is set.
|
||||||
|
# %% here is the parameter and what it got set to. E.G: "Title to TestDir Config File".
|
||||||
|
|
||||||
|
Unset = "Error: %% is not set." # When a value isn't set.
|
||||||
|
# %% here is just the parameter who's value was either "Unset" or None. E.G: "RootDir".
|
||||||
|
|
||||||
|
Zipping = "Zipping: %%." # When a directory is zipped.
|
||||||
|
# %% here is the path to the directory being zipped.
|
||||||
|
|
||||||
|
Unzipping = "Unzipping: %%." # When a directory gets unzipped.
|
||||||
|
# %% is the path to the directory being unzipped.
|
||||||
|
|
||||||
|
NoPermission = "Error: No permission to %%." # When SaraSortd lacks permission to do something.
|
||||||
|
# %% is the action that the program had no permission to do. E.G. "remove directory /home/bob/exampledir".
|
||||||
|
```
|
||||||
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/OutputDirConf.png" width="100%" alt="OutputDirConf"/>
|
||||||
|
|
||||||
|
|
||||||
|
The output directory config references the SaraSortd config (GlobalConf.toml). This means that you can, for example, use the character assigned to "NextNum" in GlobalConf.toml in this config (OutputDirConf.toml). This applies to all Variables in the SaraSortd config (GlobalConf.toml).
|
||||||
|
|
||||||
|
```
|
||||||
|
Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which.
|
||||||
|
|
||||||
|
Version = "7.5" # 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.
|
||||||
|
|
||||||
|
ParentDir = "Unset" # The name of this directory. It gets automatically updated when this config gets copied to an output directory.
|
||||||
|
|
||||||
|
ValidInputDirs = ["*"] # Directories that are valid for sorting. Basically, if a file is from one of these directories, they can be sorted here. * is a wildcard; ? is a one-character wilcard. E.G: if "ValidInputDirs = ["/TestDir/*"]" any file from any directory in "/TestDir" will be allowed to be sorted here.
|
||||||
|
|
||||||
|
FileLimit = 0 # The max limit of files a directory can have. If this is 0, then there's no limit.
|
||||||
|
|
||||||
|
DeleteOrg = 1 # If SaraSortd should delete the original file or not. If this is 1, then sorted files will be 'moved' to an output directory. If not, they will be 'copied' to an output directory.
|
||||||
|
|
||||||
|
Unzip = [] # What type of files this output directory should try to unzip. E.G:
|
||||||
|
# Unzip = ["zip", "tar.gz"] would unzip .zip files and .tar files. This pairs insanely well with input directories' config's "SortDirs" parameter.
|
||||||
|
|
||||||
|
[[Files]] # The double brackets means that you can add multiple types of files to be sorted here. Just copy [[Files]] and all the parameters under it, and paste it under or over this section, then you may change the values of the new parameters. E.G:
|
||||||
|
|
||||||
|
# [[Files]]
|
||||||
|
# Pattern = "*.png"
|
||||||
|
# NewFileName = "GoodImage^"
|
||||||
|
# NextNum = 1
|
||||||
|
# NextChar = "A"
|
||||||
|
# CaseSensitive = 0
|
||||||
|
# [[Files]]
|
||||||
|
# Pattern = "*.jpg"
|
||||||
|
# NewFileName = "BetterImage^"
|
||||||
|
# NextNum = 1
|
||||||
|
# NextChar = "A"
|
||||||
|
# CaseSensitive = 0
|
||||||
|
# Overwrite = 1
|
||||||
|
|
||||||
|
# Will sort .png files to this directory and rename them to "GoodImage.png", but also sort .jpg files into this directory yet rename those to "BetterImage.jpg".
|
||||||
|
|
||||||
|
Pattern = "*" # A pattern used to sort files to this directory. * is a wildcard; ? is a one-character wildcard. E.G: if Pattern is "*.png", all files ending with .png will end up here.
|
||||||
|
|
||||||
|
NewFileName = "~^" # A pattern for the new names of the files that get sorted to this directory from matching this "Pattern".
|
||||||
|
|
||||||
|
NextNum = 1 # Used to keep track of the next available number for file names. If you change NextNum to be 01 or 001 instead of 1, it'll still work. 09 => 10. It gets automatically incremented when NextNum is used in NewFileName, and a file gets sorted to this directory and renamed.
|
||||||
|
|
||||||
|
NextChar = "A" # Used to keep track of the next available letter for file names. If you change NextChar to be AA or AAA instead of A, it'll still work. AZ => BA. It gets automatically incremented (E.G: A into B or AA into AB then AZ into BA) when NextChar is used in NewFileName, and a file gets sorted to this directory and renamed.
|
||||||
|
|
||||||
|
CaseSensitive = 0 # Whether this "Pattern" is case-sensitive or not. 1 is yes, 0 is no. E.G: if CaseSensitive = 1 and this "Pattern" is "*.PNG", a file named "picture.png" won't get sorted to this directory. If CaseSensitive = 0 and this "Pattern" is "*.PNG", a file named "picture.png" *will* get sorted to this directory.
|
||||||
|
|
||||||
|
Overwrite = 1 # Whether this specific file type can be overwritten by others of the same type. 0 is no, 1 is yes.
|
||||||
|
|
||||||
|
# ValidInputDirs = ["%RootDir%/Downloads"] # Optional (since 7.1). Like the ValidInputDirs above, but only for this [[Files]] entry, so different input directories can send different file types here. Without it, this entry takes files from every input directory the ValidInputDirs above allows.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<img align="center" src="https://git.srhtnin.garden/sarah/RepoAssets/raw/branch/main/SaraSortd/Banners/InputDirConf.png" width="100%" alt="InputDirConf"/>
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
Title = "Unset" # NOTE: Do not change the title. This is so that the program knows which config file is which.
|
||||||
|
|
||||||
|
Version = "7.5"
|
||||||
|
|
||||||
|
ParentDir = "Unset" # The name of this directory. It gets automatically updated when this config gets copied to an output directory.
|
||||||
|
|
||||||
|
SortDirs = 0 # If the input directory should be allowed to sort directories within it. Automatically uses .zip to move directories.
|
||||||
|
|
||||||
|
[[Files]] # The double brackets means that you can add multiple types of files to be sorted here. Just copy [[Files]] and all the parameters under it, and paste it under or over this section, then you may change the values of the new parameters.
|
||||||
|
|
||||||
|
Pattern = "*" # A pattern used to decide if a file should be sorted or not. * is a wildcard; ? is a one-character wildcard. E.G: if Pattern is "*.png", all files ending with .png in this directory will try to be sorted.
|
||||||
|
|
||||||
|
CaseSensitive = 0 # Whether Pattern is case-sensitive or not. If CaseSensitive = 1 and your pattern is "*.PNG", then a file named "example.png" won't be sorted.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
+416
-111
@@ -6,7 +6,9 @@ import shutil
|
|||||||
import time
|
import time
|
||||||
import toml
|
import toml
|
||||||
|
|
||||||
ConfPath = "./Config.toml"
|
# Version = "7.5"
|
||||||
|
|
||||||
|
ConfPath = "./GlobalConf.toml"
|
||||||
|
|
||||||
Conf = None
|
Conf = None
|
||||||
ConfVars = None
|
ConfVars = None
|
||||||
@@ -149,8 +151,10 @@ def Parse(
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
Value = GetConf(Content, Path)
|
Value = GetConf(Content, Path)
|
||||||
if Value is not None:
|
if Value is None:
|
||||||
return Value
|
return Match.group(0) # Unknown names are left as they are (they used to crash).
|
||||||
|
|
||||||
|
return ResolveLookups(str(Value), Path)
|
||||||
|
|
||||||
Now = datetime.datetime.now()
|
Now = datetime.datetime.now()
|
||||||
if OrgFile is not None:
|
if OrgFile is not None:
|
||||||
@@ -166,11 +170,11 @@ def Parse(
|
|||||||
ConfVars["OrgFileName"]: str(OrgFileName),
|
ConfVars["OrgFileName"]: str(OrgFileName),
|
||||||
ConfVars["OrgFileType"]: str(OrgFileType),
|
ConfVars["OrgFileType"]: str(OrgFileType),
|
||||||
ConfVars["Year"]: str(Now.year),
|
ConfVars["Year"]: str(Now.year),
|
||||||
ConfVars["Month"]: str(Now.month),
|
ConfVars["Month"]: str(Now.month).rjust(2, "0"),
|
||||||
ConfVars["Day"]: str(Now.day),
|
ConfVars["Day"]: str(Now.day).rjust(2, "0"),
|
||||||
ConfVars["Hour"]: str(Now.hour),
|
ConfVars["Hour"]: str(Now.hour).rjust(2, "0"),
|
||||||
ConfVars["Minute"]: str(Now.minute),
|
ConfVars["Minute"]: str(Now.minute).rjust(2, "0"),
|
||||||
ConfVars["Second"]: str(Now.second),
|
ConfVars["Second"]: str(Now.second).rjust(2, "0"),
|
||||||
ConfVars["VarCall"]: str(VarCall),
|
ConfVars["VarCall"]: str(VarCall),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,41 +191,181 @@ def Parse(
|
|||||||
return String
|
return String
|
||||||
|
|
||||||
|
|
||||||
def Clone(Source, Destination, NewName, Delete = False):
|
def ResolveLookups(String, Path = ConfPath, Depth = 0):
|
||||||
Dir(Destination, Output = False, CopyConf = False)
|
# %Name% lookups, also inside the values they bring in (since 7.3).
|
||||||
shutil.copy2(Source, Destination)
|
# E.G: ImageDir = "%RootDir%/Images" used as "%ImageDir%" gives the full path. Unknown names are left as they are.
|
||||||
|
def Replacer(Match):
|
||||||
|
Value = GetConf(Match.group(1), Path) if Match.group(1) else None
|
||||||
|
if Value is None or Depth >= 10:
|
||||||
|
return Match.group(0)
|
||||||
|
|
||||||
BaseName = os.path.basename(Source)
|
return ResolveLookups(str(Value), Path, Depth + 1)
|
||||||
shutil.move(os.path.join(Destination, BaseName), os.path.join(Destination, NewName))
|
|
||||||
|
VarCall0 = re.escape(ConfVars["VarCall"][0])
|
||||||
|
VarCall1 = re.escape(ConfVars["VarCall"][1])
|
||||||
|
return re.sub(f"{VarCall0}(.*?){VarCall1}", Replacer, String)
|
||||||
|
|
||||||
|
|
||||||
|
def ParsePath(String):
|
||||||
|
# Directory paths only get %Name% lookups and the date and time variables (since 7.3). The single-character
|
||||||
|
# variables (NextNum, NextChar, Parent, OrgFileName, OrgFileType) are about the file being sorted, so a path
|
||||||
|
# like "~/Stuff&Things" stays as it is. A "~" at the start is your home directory.
|
||||||
|
Now = datetime.datetime.now()
|
||||||
|
DateValues = {
|
||||||
|
ConfVars["Year"]: str(Now.year),
|
||||||
|
ConfVars["Month"]: str(Now.month).rjust(2, "0"),
|
||||||
|
ConfVars["Day"]: str(Now.day).rjust(2, "0"),
|
||||||
|
ConfVars["Hour"]: str(Now.hour).rjust(2, "0"),
|
||||||
|
ConfVars["Minute"]: str(Now.minute).rjust(2, "0"),
|
||||||
|
ConfVars["Second"]: str(Now.second).rjust(2, "0"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for Key, Value in DateValues.items():
|
||||||
|
if Key and Key in String:
|
||||||
|
String = String.replace(Key, Value)
|
||||||
|
|
||||||
|
return os.path.expanduser(ResolveLookups(String))
|
||||||
|
|
||||||
|
|
||||||
|
def ConfFolder(PathText):
|
||||||
|
# Relative paths in GlobalConf.toml (DirConfDir, [DirConfs]) count from GlobalConf.toml's own folder.
|
||||||
|
PathText = ParsePath(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 Slashes(Path):
|
||||||
|
# Windows paths use "\\" (and configs may mix both); compare and name them with "/" only.
|
||||||
|
return Path.replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
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". On Windows a drive's ":" is dropped (not allowed in names).
|
||||||
|
Root = Slashes(ParsePath(ConfDirs.get("RootDir", ""))).rstrip("/")
|
||||||
|
Path = Slashes(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(":", "").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 Slashes(ParsePath(Key)).rstrip("/") == Slashes(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 = HistoryPath(Destination)
|
||||||
|
|
||||||
|
if not os.path.exists(FileHistoryPath):
|
||||||
|
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
|
||||||
|
toml.dump({"Title": f"{os.path.basename(Destination)} History", "History": []}, HistoryFile)
|
||||||
|
|
||||||
|
HistoryFileData = toml.load(FileHistoryPath)
|
||||||
|
if (NewName not in HistoryFileData["History"]):
|
||||||
|
HistoryFileData["History"].append(NewName)
|
||||||
|
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
|
||||||
|
toml.dump(HistoryFileData, HistoryFile)
|
||||||
|
|
||||||
|
os.replace(Source, os.path.join(Destination, NewName)) # overwrites on Windows too, like rename on Linux
|
||||||
|
return
|
||||||
|
|
||||||
|
Dir(Destination, Output = False, CopyConf = False)
|
||||||
|
# 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:
|
if Delete:
|
||||||
os.remove(Source)
|
os.remove(Source)
|
||||||
|
|
||||||
|
|
||||||
def Dir(Path, Output = True, CopyConf = True):
|
def CompleteDirConf(DirConf, Template, Kind):
|
||||||
|
# Gives an existing directory config the keys it's missing from the template (since 7.3). A directory that is
|
||||||
|
# both an input and an output shares one config by default, so the second role's keys used to be missing and
|
||||||
|
# SaraSortd crashed. Output configs' [[Files]] entries also get the output keys (NewFileName, NextNum, ...).
|
||||||
|
Data = toml.load(DirConf)
|
||||||
|
TemplateData = toml.load(Template)
|
||||||
|
Changed = False
|
||||||
|
|
||||||
|
for Key, Value in TemplateData.items():
|
||||||
|
if Key != "Files" and Key not in Data:
|
||||||
|
Data[Key] = Value
|
||||||
|
Changed = True
|
||||||
|
|
||||||
|
if Kind == "Out":
|
||||||
|
Defaults = (TemplateData.get("Files") or [{}])[0]
|
||||||
|
for Entry in Data.get("Files", []):
|
||||||
|
for Key, Value in Defaults.items():
|
||||||
|
if Key not in Entry:
|
||||||
|
Entry[Key] = Value
|
||||||
|
Changed = True
|
||||||
|
|
||||||
|
if Changed:
|
||||||
|
with open(DirConf, "w", encoding = "utf-8") as File:
|
||||||
|
toml.dump(Data, File)
|
||||||
|
|
||||||
|
|
||||||
|
def Dir(Path, Output = True, CopyConf = "OutputDirConf.toml"):
|
||||||
ParentName = os.path.basename(Path)
|
ParentName = os.path.basename(Path)
|
||||||
os.makedirs(Path, exist_ok=True)
|
os.makedirs(Path, exist_ok=True)
|
||||||
|
|
||||||
if CopyConf:
|
if CopyConf:
|
||||||
NewDirConf = f"{Parse(String = ConfNames['DirConfName'], Path = ConfPath, Parent = ParentName)}.toml"
|
for Kind in ("Out", "In"):
|
||||||
if Conf["OverwriteDirConf"] == 1 or not os.path.exists(
|
if Kind not in CopyConf:
|
||||||
os.path.join(f"{Path}/{NewDirConf}")
|
continue
|
||||||
):
|
|
||||||
Clone("DirConfig.toml", Path, NewDirConf)
|
DirConf = DirConfPath(Path, Kind)
|
||||||
UpdateConf(
|
|
||||||
f"{Path}/{Parse(String = ConfNames['DirConfName'], Path = ConfPath, Parent = ParentName)}.toml",
|
if Conf["OverwriteDirConf"] == 1 or not os.path.exists(DirConf):
|
||||||
"ParentDir",
|
Clone(CopyConf, os.path.dirname(DirConf), os.path.basename(DirConf))
|
||||||
ParentName,
|
UpdateConf(DirConf, "ParentDir", ParentName)
|
||||||
)
|
UpdateConf(DirConf, "Title", f"{ParentName} Config")
|
||||||
UpdateConf(
|
|
||||||
f"{Path}/{Parse(String = ConfNames['DirConfName'], Path = ConfPath, Parent = ParentName)}.toml",
|
else:
|
||||||
"Title",
|
CompleteDirConf(DirConf, CopyConf, Kind)
|
||||||
f"{ParentName} Config",
|
|
||||||
)
|
|
||||||
|
|
||||||
if Output:
|
if Output:
|
||||||
ParsedPath = Parse(String = Path)
|
ParsedPath = ParsePath(Path)
|
||||||
ParsedOutputs = [Parse(String = Dir) for Dir in ConfDirs["OutputDir"]]
|
ParsedOutputs = [ParsePath(Dir) for Dir in ConfDirs["OutputDir"]]
|
||||||
if ParsedPath not in ParsedOutputs:
|
if ParsedPath not in ParsedOutputs:
|
||||||
UpdateConf(ConfPath, "OutputDir", Path, True)
|
UpdateConf(ConfPath, "OutputDir", Path, True)
|
||||||
|
|
||||||
@@ -233,27 +377,62 @@ def LogWrite(Text):
|
|||||||
ConfNames["LogFileName"] is not None
|
ConfNames["LogFileName"] is not None
|
||||||
and ConfNames["LogFileName"] != "Unset"
|
and ConfNames["LogFileName"] != "Unset"
|
||||||
):
|
):
|
||||||
Dir(Parse(String = ConfDirs["LogDir"], Path = ConfPath), Output = False, CopyConf = False)
|
Dir(ParsePath(ConfDirs["LogDir"]), Output = False, CopyConf = False)
|
||||||
LogFile = f"{Parse(String = ConfDirs['LogDir'], Path = ConfPath)}/{Parse(String = ConfNames['LogFileName'], Path = ConfPath)}.log"
|
LogFile = f"{ParsePath(ConfDirs['LogDir'])}/{Parse(String = ConfNames['LogFileName'], Path = ConfPath)}.log"
|
||||||
with open(LogFile, "a", encoding = "utf-8", buffering = 1) as File:
|
with open(LogFile, "a", encoding = "utf-8", buffering = 1) as File:
|
||||||
File.write(f"{Parse(String = ConfLog['All'], Path = ConfPath)}{Text}\n")
|
File.write(f"{Parse(String = ConfLog['All'], Path = ConfPath)}{Text}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def FileInputValid(File, FilePath, ParentName):
|
||||||
|
# A [[Files]] entry can have its own ValidInputDirs (since 7.1), so one output directory can take
|
||||||
|
# different patterns from different input directories. Without one, the entry takes files from
|
||||||
|
# every input directory the output directory's own ValidInputDirs allows.
|
||||||
|
ValidInputDirs = File.get("ValidInputDirs")
|
||||||
|
if ValidInputDirs is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for ValidInputDir in ValidInputDirs:
|
||||||
|
if fnmatch.fnmatchcase(os.path.dirname(FilePath), ParsePath(ValidInputDir)):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Files that are still downloading (since 7.4): Firefox's .part, Chromium's .crdownload and Safari's .download.
|
||||||
|
# SaraSortd never touches them, not even to check them against patterns.
|
||||||
|
PartialDownloadSuffixes = (".part", ".crdownload", ".download")
|
||||||
|
|
||||||
|
|
||||||
|
def IsPartialDownload(FilePath):
|
||||||
|
if os.path.basename(FilePath).lower().endswith(PartialDownloadSuffixes):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# A file next to its own unfinished download, E.G. the empty "video.mp4" Firefox makes beside
|
||||||
|
# "video.mp4.part" and fills in at the end. Moving it would break the download.
|
||||||
|
return any(os.path.exists(FilePath + Suffix) for Suffix in PartialDownloadSuffixes)
|
||||||
|
|
||||||
|
|
||||||
|
def IsCatchAll(Pattern):
|
||||||
|
# A pattern made of only "*" takes any file.
|
||||||
|
return Pattern != "" and set(Pattern) == {"*"}
|
||||||
|
|
||||||
|
|
||||||
def DecideNewPath(FilePath):
|
def DecideNewPath(FilePath):
|
||||||
UnsortedFile = os.path.basename(FilePath)
|
UnsortedFile = os.path.basename(FilePath)
|
||||||
|
Candidates = []
|
||||||
|
|
||||||
for Output in ConfDirs["OutputDir"]:
|
for Output in ConfDirs["OutputDir"]:
|
||||||
Output = Parse(String = Output, Path = ConfPath)
|
Output = ParsePath(Output)
|
||||||
|
|
||||||
ParentName = os.path.basename(Output)
|
ParentName = os.path.basename(Output)
|
||||||
|
|
||||||
DirConfName = Parse(String = ConfNames["DirConfName"], Path = ConfPath, Parent = ParentName)
|
OutDirConfPath = DirConfPath(Output, "Out")
|
||||||
DirConfPath = f"{Output}/{DirConfName}.toml"
|
|
||||||
|
|
||||||
ValidInputDirs = GetConf("ValidInputDirs", DirConfPath)
|
ValidInputDirs = GetConf("ValidInputDirs", OutDirConfPath) or []
|
||||||
|
InputDirValid = False
|
||||||
|
|
||||||
for ValidInputDir in ValidInputDirs:
|
for ValidInputDir in ValidInputDirs:
|
||||||
ValidInputDir = Parse(String = ValidInputDir, Parent = GetConf("ParentDir", DirConfPath))
|
ValidInputDir = ParsePath(ValidInputDir)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["MatchPattern"], VarCall = f"{os.path.dirname(FilePath)} against {ValidInputDir}")
|
TextOutput = Parse(String = ConfLog["MatchPattern"], VarCall = f"{os.path.dirname(FilePath)} against {ValidInputDir}")
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
@@ -263,13 +442,12 @@ def DecideNewPath(FilePath):
|
|||||||
if (InputDirValid):
|
if (InputDirValid):
|
||||||
break
|
break
|
||||||
|
|
||||||
if not os.path.exists(DirConfPath) or not InputDirValid:
|
if not os.path.exists(OutDirConfPath) or not InputDirValid:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
OutputFiles = 0
|
OutputFiles = 0
|
||||||
|
|
||||||
FileLimitConf = GetConf("FileLimit", DirConfPath)
|
FileLimitConf = GetConf("FileLimit", OutDirConfPath)
|
||||||
|
|
||||||
if FileLimitConf > 0:
|
if FileLimitConf > 0:
|
||||||
for OutputFile in os.listdir(Output):
|
for OutputFile in os.listdir(Output):
|
||||||
@@ -281,62 +459,99 @@ def DecideNewPath(FilePath):
|
|||||||
if OutputFiles >= FileLimitConf:
|
if OutputFiles >= FileLimitConf:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
FileConf = GetConf("Files", DirConfPath)
|
FileConf = GetConf("Files", OutDirConfPath)
|
||||||
|
|
||||||
if not FileConf:
|
if not FileConf:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for File in FileConf:
|
Candidates.append((Output, ParentName, OutDirConfPath, FileConf))
|
||||||
Pattern = File["Pattern"]
|
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["MatchPattern"], VarCall = f"{UnsortedFile} against {Pattern}")
|
# Specific patterns first, in OutputDir order. Catch-alls ("*") are a fallback for files nothing else took,
|
||||||
LogWrite(TextOutput)
|
# so an output with the template's catch-all doesn't take every file just because it's listed first (since 7.3).
|
||||||
Speak(TextOutput)
|
for CatchAll in (False, True):
|
||||||
|
for Output, ParentName, OutDirConfPath, FileConf in Candidates:
|
||||||
if File["CaseSensitive"] == 1:
|
for FileIndex, File in enumerate(FileConf):
|
||||||
Match = fnmatch.fnmatchcase(UnsortedFile, Pattern)
|
if IsCatchAll(File["Pattern"]) != CatchAll or not FileInputValid(File, FilePath, ParentName):
|
||||||
|
|
||||||
else:
|
|
||||||
Match = fnmatch.fnmatch(UnsortedFile.lower(), Pattern.lower())
|
|
||||||
|
|
||||||
if Match:
|
|
||||||
NewFileName = File["NewFileName"]
|
|
||||||
|
|
||||||
NewFileName = Parse(
|
|
||||||
String = NewFileName,
|
|
||||||
Path = DirConfPath,
|
|
||||||
OrgFile = UnsortedFile,
|
|
||||||
NextNum = File["NextNum"],
|
|
||||||
NextChar = File["NextChar"],
|
|
||||||
Parent = ParentName,
|
|
||||||
)
|
|
||||||
|
|
||||||
if File["Overwrite"] == 0 and os.path.exists(os.path.join(Output, NewFileName)):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ConfVars["NextNum"] in NewFileName:
|
Pattern = File["Pattern"]
|
||||||
UpdateConf(
|
|
||||||
DirConfPath,
|
|
||||||
"NextNum",
|
|
||||||
(int(GetConf("NextNum", DirConfPath)) + 1),
|
|
||||||
)
|
|
||||||
|
|
||||||
if ConfVars["NextChar"] in NewFileName:
|
TextOutput = Parse(String = ConfLog["MatchPattern"], VarCall = f"{UnsortedFile} against {Pattern}")
|
||||||
CurrentChar = GetConf("NextChar", DirConfPath)
|
LogWrite(TextOutput)
|
||||||
if CurrentChar and len(CurrentChar) == 1 and CurrentChar.isalpha():
|
Speak(TextOutput)
|
||||||
NewChar = chr(((ord(CurrentChar.upper()) - 65 + 1) % 26) + 65)
|
|
||||||
UpdateConf(DirConfPath, "NextChar", NewChar)
|
|
||||||
|
|
||||||
return f"{Output}/{NewFileName}"
|
if File["CaseSensitive"] == 1:
|
||||||
|
Match = fnmatch.fnmatchcase(UnsortedFile, Pattern)
|
||||||
|
|
||||||
|
else:
|
||||||
|
Match = fnmatch.fnmatch(UnsortedFile.lower(), Pattern.lower())
|
||||||
|
|
||||||
|
if Match:
|
||||||
|
NewFileName = File["NewFileName"]
|
||||||
|
|
||||||
|
NewFileName = Parse(String = NewFileName, Path = OutDirConfPath, OrgFile = UnsortedFile, NextNum = File["NextNum"], NextChar = File["NextChar"], Parent = ParentName)
|
||||||
|
|
||||||
|
if File["Overwrite"] == 0 and os.path.exists(os.path.join(Output, NewFileName)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ConfVars["NextNum"] in File["NewFileName"]:
|
||||||
|
ConfData = toml.load(OutDirConfPath)
|
||||||
|
Width = len(File["NextNum"])
|
||||||
|
NewNum = str((int(File["NextNum"]) + 1)).rjust(Width, "0")
|
||||||
|
print(File["NextNum"],"=>",NewNum)
|
||||||
|
|
||||||
|
# By position: two entries can share a Pattern (with different ValidInputDirs).
|
||||||
|
ConfData["Files"][FileIndex]["NextNum"] = NewNum
|
||||||
|
|
||||||
|
with open(OutDirConfPath, "w", encoding="utf-8") as ConfFile:
|
||||||
|
toml.dump(ConfData, ConfFile)
|
||||||
|
|
||||||
|
TextOutput = Parse(String = ConfLog["ValueSet"], VarCall = f"NextNum to {NewNum}")
|
||||||
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
|
||||||
|
if ConfVars["NextChar"] in File["NewFileName"]:
|
||||||
|
CurrentChar = File["NextChar"]
|
||||||
|
CurrentChar = CurrentChar.upper()
|
||||||
|
Result = list(CurrentChar)
|
||||||
|
|
||||||
|
Count = len(Result) - 1
|
||||||
|
|
||||||
|
while Count >= 0:
|
||||||
|
if Result[Count] == "Z":
|
||||||
|
Result[Count] = "A"
|
||||||
|
Count -= 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
Result[Count] = chr(ord(Result[Count]) + 1)
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
Result.insert(0, "A")
|
||||||
|
|
||||||
|
NewChar = "".join(Result)
|
||||||
|
|
||||||
|
ConfData = toml.load(OutDirConfPath)
|
||||||
|
|
||||||
|
ConfData["Files"][FileIndex]["NextChar"] = NewChar
|
||||||
|
|
||||||
|
with open(OutDirConfPath, "w", encoding="utf-8") as ConfFile:
|
||||||
|
toml.dump(ConfData, ConfFile)
|
||||||
|
|
||||||
|
TextOutput = Parse(String = ConfLog["ValueSet"], VarCall = f"NextChar to {NewChar}")
|
||||||
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
|
||||||
|
return f"{Output}/{NewFileName}"
|
||||||
|
|
||||||
|
|
||||||
def Sort(FilePath):
|
def Sort(FilePath):
|
||||||
NewPath = DecideNewPath(FilePath)
|
NewPath = DecideNewPath(FilePath)
|
||||||
|
|
||||||
if NewPath is None:
|
if NewPath is None:
|
||||||
Dir(Parse(String = ConfDirs["FailedDir"]), Output = False, CopyConf = False)
|
Dir(ParsePath(ConfDirs["FailedDir"]), Output = False, CopyConf = False)
|
||||||
NewName = os.path.basename(FilePath)
|
NewName = os.path.basename(FilePath)
|
||||||
Clone(FilePath, Parse(String = ConfDirs["FailedDir"]), NewName, True)
|
Clone(FilePath, ParsePath(ConfDirs["FailedDir"]), NewName, True)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["NotSorted"], VarCall = FilePath)
|
TextOutput = Parse(String = ConfLog["NotSorted"], VarCall = FilePath)
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
@@ -346,24 +561,24 @@ def Sort(FilePath):
|
|||||||
NewDirPath = os.path.dirname(NewPath)
|
NewDirPath = os.path.dirname(NewPath)
|
||||||
NewName = os.path.basename(NewPath)
|
NewName = os.path.basename(NewPath)
|
||||||
|
|
||||||
try:
|
if (GetConf("DeleteOrg", DirConfPath(NewDirPath, "Out")) == 1):
|
||||||
Clone(FilePath, NewDirPath, NewName, True)
|
Delete = True
|
||||||
UpdateConf(
|
|
||||||
f"{NewDirPath}/{Parse(String = ConfNames['DirConfName'], Parent = os.path.basename(NewDirPath))}.toml",
|
|
||||||
"LastFile",
|
|
||||||
NewName,
|
|
||||||
)
|
|
||||||
|
|
||||||
TextOutput = Parse(
|
else:
|
||||||
String = ConfLog["Sorted"], VarCall = f"{FilePath} to {NewDirPath}/{NewName}"
|
Delete = False
|
||||||
)
|
|
||||||
|
try:
|
||||||
|
Clone(FilePath, NewDirPath, NewName, Delete)
|
||||||
|
UpdateConf(DirConfPath(NewDirPath, "Out"), "LastFile", NewName)
|
||||||
|
|
||||||
|
TextOutput = Parse(String = ConfLog["Sorted"], VarCall = f"{FilePath} to {NewDirPath}/{NewName}")
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
Speak(TextOutput)
|
Speak(TextOutput)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
Dir(Parse(String = ConfDirs["FailedDir"]), Output = False, CopyConf = False)
|
Dir(ParsePath(ConfDirs["FailedDir"]), Output = False, CopyConf = False)
|
||||||
NewName = os.path.basename(NewPath)
|
NewName = os.path.basename(NewPath)
|
||||||
Clone(FilePath, Parse(String = ConfDirs["FailedDir"]), NewName, True)
|
Clone(FilePath, ParsePath(ConfDirs["FailedDir"]), NewName, Delete)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["NotSorted"], VarCall = FilePath)
|
TextOutput = Parse(String = ConfLog["NotSorted"], VarCall = FilePath)
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
@@ -380,13 +595,30 @@ def Init():
|
|||||||
|
|
||||||
if Start:
|
if Start:
|
||||||
for InputDir in ConfDirs["InputDir"]:
|
for InputDir in ConfDirs["InputDir"]:
|
||||||
InputDir = Parse(String = InputDir, Path = ConfPath, Parent = os.path.basename(os.path.dirname(InputDir)))
|
InputDir = ParsePath(InputDir)
|
||||||
Dir(InputDir, Output = False, CopyConf = False)
|
Dir(InputDir, Output = False, CopyConf = "InputDirConf.toml")
|
||||||
|
|
||||||
|
for Output in ConfDirs["OutputDir"]:
|
||||||
|
Output = ParsePath(Output)
|
||||||
|
Dir(Output)
|
||||||
|
|
||||||
|
OutDirConfPath = DirConfPath(Output, "Out")
|
||||||
|
ConfData = toml.load(OutDirConfPath)
|
||||||
|
|
||||||
|
for Item in ConfData.get("Files", []):
|
||||||
|
Character = re.escape(ConfVars["NextChar"])
|
||||||
|
Matches = re.findall(f"{Character}+", Item["NewFileName"])
|
||||||
|
Width = max((len(Match) for Match in Matches), default=0)
|
||||||
|
|
||||||
|
if Width > 0:
|
||||||
|
Item["NextChar"] = Item["NextChar"].upper().rjust(Width, "A")
|
||||||
|
|
||||||
|
with open(OutDirConfPath, "w", encoding = "utf-8") as ConfFile:
|
||||||
|
toml.dump(ConfData, ConfFile)
|
||||||
|
|
||||||
TextOutput = Parse(String = ConfLog["Start"], Path = ConfPath)
|
TextOutput = Parse(String = ConfLog["Start"], Path = ConfPath)
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
Speak(TextOutput)
|
Speak(TextOutput)
|
||||||
|
|
||||||
Main()
|
Main()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -395,39 +627,112 @@ def Init():
|
|||||||
Speak(TextOutput)
|
Speak(TextOutput)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(
|
print("NO CONFIG FOUND! Make sure the 'GlobalConf.toml' is in the same directory as 'SaraSortd.py'")
|
||||||
"NO CONFIG FOUND! Make sure the 'Config.toml' is in the same directory as 'SaraSortd.py'"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def Main():
|
def Main():
|
||||||
for Output in ConfDirs["OutputDir"]:
|
|
||||||
Output = Parse(String = Output, Parent = os.path.basename(os.path.dirname(Output)))
|
|
||||||
Dir(Output)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
for Input in ConfDirs["InputDir"]:
|
for Input in ConfDirs["InputDir"]:
|
||||||
InputPath = Parse(Input)
|
RecurseDir = False
|
||||||
|
|
||||||
|
InputPath = ParsePath(Input)
|
||||||
|
|
||||||
if not os.path.exists(InputPath):
|
if not os.path.exists(InputPath):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
InputDirConfPath = DirConfPath(InputPath, "In")
|
||||||
|
InputDirConf = toml.load(InputDirConfPath)
|
||||||
|
|
||||||
|
for Output in ConfDirs["OutputDir"]:
|
||||||
|
OutputPath = ParsePath(Output)
|
||||||
|
OutDirConfPath = DirConfPath(OutputPath, "Out")
|
||||||
|
|
||||||
|
if len(GetConf("Unzip", OutDirConfPath) or []) >= 1:
|
||||||
|
ZipTypes = GetConf("Unzip", OutDirConfPath)
|
||||||
|
for OutputFile in os.listdir(OutputPath):
|
||||||
|
if OutputFile.startswith(".") or IsPartialDownload(f"{OutputPath}/{OutputFile}"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ZipType in ZipTypes:
|
||||||
|
print(ZipType)
|
||||||
|
if (OutputFile.lower()).endswith((ZipType).lower()):
|
||||||
|
shutil.unpack_archive(f"{OutputPath}/{OutputFile}", OutputPath)
|
||||||
|
TextOutput = Parse(String = ConfLog["Unzipping"], VarCall = f"{OutputPath}/{OutputFile}")
|
||||||
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(f"{OutputPath}/{OutputFile}")
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
TextOutput = Parse(String = ConfLog["NoPermission"], VarCall = f"remove {OutputPath}/{OutputFile}")
|
||||||
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (OutputPath == InputPath):
|
||||||
|
RecurseDir = True
|
||||||
|
FileHistoryPath = HistoryPath(InputPath)
|
||||||
|
|
||||||
|
if not os.path.exists(FileHistoryPath):
|
||||||
|
with open(FileHistoryPath, "w", encoding = "utf-8") as HistoryFile:
|
||||||
|
toml.dump({"Title": f"{os.path.basename(InputPath)} History", "History": [] }, HistoryFile)
|
||||||
|
|
||||||
for FileName in os.listdir(InputPath):
|
for FileName in os.listdir(InputPath):
|
||||||
FilePath = os.path.join(InputPath, FileName)
|
FilePath = f"{InputPath}/{FileName}"
|
||||||
|
if IsPartialDownload(FilePath):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if os.path.isdir(FilePath) and InputDirConf.get("SortDirs", 0) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
FileName = os.path.basename(FilePath)
|
||||||
|
if FileName.startswith(".") and Conf["DotFiles"] == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for ConfFile in InputDirConf["Files"]:
|
||||||
|
if ConfFile["CaseSensitive"] == 1:
|
||||||
|
Match = fnmatch.fnmatchcase(FileName, Parse(ConfFile["Pattern"], Parent = os.path.basename(InputPath)))
|
||||||
|
|
||||||
|
else:
|
||||||
|
Match = fnmatch.fnmatchcase(FileName.lower(), (Parse(ConfFile["Pattern"], Parent = os.path.basename(InputPath))).lower())
|
||||||
|
|
||||||
|
if Match:
|
||||||
|
break
|
||||||
|
|
||||||
|
if Match:
|
||||||
|
if os.path.isdir(FilePath) and InputDirConf.get("SortDirs", 0) == 1:
|
||||||
|
TextOutput = Parse(String = ConfLog["Zipping"], VarCall = FilePath)
|
||||||
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
|
||||||
|
shutil.make_archive(FilePath, "zip", FilePath)
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree(FilePath, ignore_errors=True)
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
TextOutput = Parse(String = ConfLog["NoPermission"], VarCall = f"remove directory {InputPath}/{FileName}")
|
||||||
|
LogWrite(TextOutput)
|
||||||
|
Speak(TextOutput)
|
||||||
|
continue
|
||||||
|
|
||||||
|
FilePath = f"{FilePath}.zip"
|
||||||
|
|
||||||
|
if RecurseDir:
|
||||||
|
HistoryFile = toml.load(FileHistoryPath)
|
||||||
|
|
||||||
|
if (FileName in HistoryFile["History"]):
|
||||||
|
continue
|
||||||
|
|
||||||
if os.path.isfile(FilePath):
|
|
||||||
FileName = os.path.basename(FilePath)
|
|
||||||
if FileName.startswith(".") and Conf["DotFiles"] == 0:
|
|
||||||
continue
|
|
||||||
Sort(FilePath)
|
Sort(FilePath)
|
||||||
|
|
||||||
time.sleep(Conf.get("CheckInput", 10))
|
time.sleep(Conf.get("SortTimer", 10))
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
TextOutput = GetConf("Stop", ConfPath)
|
TextOutput = GetConf("Stop", ConfPath)
|
||||||
LogWrite(TextOutput)
|
LogWrite(TextOutput)
|
||||||
Speak(TextOutput)
|
Speak(TextOutput)
|
||||||
|
|
||||||
|
|
||||||
Init()
|
Init()
|
||||||
Binary file not shown.
Reference in new issue
Block a user