Error handling, feature cleanup
This commit is contained in:
parent
fafa69202b
commit
94755aa78f
6 changed files with 181 additions and 107 deletions
|
|
@ -12,7 +12,7 @@ Other use cases:
|
||||||
|
|
||||||
- Copying only certain types of documents into a backup folder
|
- Copying only certain types of documents into a backup folder
|
||||||
|
|
||||||
- TODO: Grouping all of your photos into separate folders, by year
|
- Moving only files that contain the name "document"
|
||||||
|
|
||||||
This is my first real Python project, after many unimportant scripts and experiments. I really needed a project like this in my portfolio, even if it's not truly amazing.
|
This is my first real Python project, after many unimportant scripts and experiments. I really needed a project like this in my portfolio, even if it's not truly amazing.
|
||||||
|
|
||||||
|
|
@ -24,9 +24,11 @@ This is my first real Python project, after many unimportant scripts and experim
|
||||||
|
|
||||||
- Set file rules for generic file types (images, documents, etc.)
|
- Set file rules for generic file types (images, documents, etc.)
|
||||||
|
|
||||||
|
- Save your file rules configuration for future use
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can run either the app or the script, but the script won't use the file rules if you haven't set them up.
|
You can run either the app or the script
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
|
|
|
||||||
154
app.py
154
app.py
|
|
@ -4,14 +4,8 @@ Frontend GUI that displays a list of files from a folder.
|
||||||
File actions (move, copy, or delete) can be set for generic file types.
|
File actions (move, copy, or delete) can be set for generic file types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO:
|
|
||||||
# Add comments
|
|
||||||
# Add some error handling
|
|
||||||
# Enable loading custom configuration json
|
|
||||||
# Improve user experience of rules tab
|
|
||||||
|
|
||||||
__name__ = "__main__"
|
__name__ = "__main__"
|
||||||
__version__ = "0.9"
|
__version__ = "1.0"
|
||||||
__author__ = "Gull"
|
__author__ = "Gull"
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
|
@ -23,8 +17,8 @@ from tkinter import messagebox as mb
|
||||||
from tkinter import filedialog as fd
|
from tkinter import filedialog as fd
|
||||||
|
|
||||||
APP_NAME = "Finch Filer"
|
APP_NAME = "Finch Filer"
|
||||||
LOG_PATH = "%/Gull/app.log"
|
LOG_PATH = "%/Gullbase/FinchFiler/app.log"
|
||||||
RULES_PATH = "%/Gull/file_rules_custom.json"
|
RULES_PATH = "%/Gullbase/FinchFiler/file_rules_custom.json"
|
||||||
|
|
||||||
def setup_log():
|
def setup_log():
|
||||||
"""Initializes the logging system."""
|
"""Initializes the logging system."""
|
||||||
|
|
@ -127,44 +121,73 @@ class App(tk.Tk):
|
||||||
def load_config(self, filepath=""):
|
def load_config(self, filepath=""):
|
||||||
"""Loads the file sorting rules from the given file path."""
|
"""Loads the file sorting rules from the given file path."""
|
||||||
if filepath == "": filepath = self.custom_rules_path
|
if filepath == "": filepath = self.custom_rules_path
|
||||||
path = ut.parse_dir(filepath)
|
try:
|
||||||
data = ut.load_json_file(path)
|
path = ut.parse_dir(filepath)
|
||||||
if data != None:
|
except FileNotFoundError as error:
|
||||||
self.fm.setup_file_rules(data, True)
|
print(error)
|
||||||
log.info(f"Loaded custom file rules: {path}")
|
else:
|
||||||
|
try:
|
||||||
|
data = ut.load_json_file(path)
|
||||||
|
self.fm.setup_file_rules(data, True)
|
||||||
|
self.prep()
|
||||||
|
log.info(f"Loaded custom file rules: {path}")
|
||||||
|
except Exception as error:
|
||||||
|
print(error)
|
||||||
|
|
||||||
def save_config(self, filepath=""):
|
def save_config(self, filepath=""):
|
||||||
"""Saves the file sorting rules to the given file path."""
|
"""Saves the file sorting rules to the given file path."""
|
||||||
if filepath == "": filepath = self.custom_rules_path
|
if filepath == "": filepath = self.custom_rules_path
|
||||||
path = ut.parse_dir(filepath)
|
try:
|
||||||
ut.save_json_file(path, {"filemodes": self.fm.filemodes})
|
path = ut.parse_dir(filepath)
|
||||||
log.info(f"Saved custom file rules: {path}")
|
except FileNotFoundError as error:
|
||||||
|
print(error)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
ut.save_json_file(path, {"filemodes": self.fm.filemodes})
|
||||||
|
log.info(f"Saved custom file rules: {path}")
|
||||||
|
except Exception as error:
|
||||||
|
print(error)
|
||||||
|
|
||||||
def reset_config(self):
|
def reset_config(self):
|
||||||
"""Resets the file sorting rules back to default."""
|
"""Resets the file sorting rules back to default."""
|
||||||
data = ut.load_json_file(fm.Manager.RULES_PATH)
|
try:
|
||||||
self.fm.setup_file_rules(data, True)
|
data = ut.load_json_file(fm.Manager.RULES_PATH)
|
||||||
self.prep()
|
self.fm.setup_file_rules(data, True)
|
||||||
log.info(f"Reloaded default file rules")
|
self.prep()
|
||||||
|
log.info(f"Reloaded default file rules")
|
||||||
|
except Exception as error:
|
||||||
|
print(error)
|
||||||
|
log.error("Failed to load default file rules!")
|
||||||
|
|
||||||
def update_mode_data(self, mode, key, value):
|
def update_mode_data(self, mode, key, value):
|
||||||
"""Updates file mode data (key and value)."""
|
"""Updates file mode data (key and value)."""
|
||||||
if mode == "": mode = self.current_mode
|
if mode == "": mode = self.current_mode # Uses current mode
|
||||||
if not mode in self.fm.filemodes: return
|
if not mode in self.fm.filemodes:
|
||||||
self.fm.filemodes[mode].update({key: value})
|
log.warning(f"File mode not found: {mode}")
|
||||||
|
return
|
||||||
|
data = self.fm.filemodes[mode]
|
||||||
|
data.update({key: value})
|
||||||
log.info(f"Set properties for file mode: {key} = {value}")
|
log.info(f"Set properties for file mode: {key} = {value}")
|
||||||
|
|
||||||
def set_rules_variables(self, key):
|
if key == "name": # Refreshes mode list
|
||||||
|
self.v_modelist.set([v["name"] for v in self.fm.filemodes.values()])
|
||||||
|
elif key == "extensions": # Refreshes extensions cache
|
||||||
|
value = value.replace(" ", "") # Removes all spaces
|
||||||
|
data.update({"extensions": value.split(",")}) # Converts to list
|
||||||
|
self.fm.match_file_types()
|
||||||
|
self.fm.update_file_data()
|
||||||
|
self.update_fileview()
|
||||||
|
|
||||||
|
def set_rules_variables(self, mode):
|
||||||
"""Updates the internal variables of the file rules (key is mode)."""
|
"""Updates the internal variables of the file rules (key is mode)."""
|
||||||
if not key in self.fm.filemodes: return
|
if not mode in self.fm.filemodes:
|
||||||
data = self.fm.filemodes[key]
|
log.warning(f"File mode not found: {mode}")
|
||||||
self.v_radio.set(data["action"])
|
return
|
||||||
|
data = self.fm.filemodes[mode]
|
||||||
|
self.v_name.set(data["name"])
|
||||||
|
self.v_action.set(data["action"])
|
||||||
self.v_targetdir.set(data["destination"])
|
self.v_targetdir.set(data["destination"])
|
||||||
# TODO: Change string parsing
|
self.v_extstr.set(", ".join(data["extensions"]))
|
||||||
if type(data["extensions"]) == list: # Needs improvement
|
|
||||||
self.v_extstr.set(", ".join(data["extensions"]))
|
|
||||||
else:
|
|
||||||
self.v_extstr.set(data["extensions"])
|
|
||||||
|
|
||||||
def select_mode_from_list(self, event):
|
def select_mode_from_list(self, event):
|
||||||
"""Selects the current file mode to use; also updates variables."""
|
"""Selects the current file mode to use; also updates variables."""
|
||||||
|
|
@ -188,8 +211,9 @@ class App(tk.Tk):
|
||||||
data = self.fm.filemodes[self.current_mode]
|
data = self.fm.filemodes[self.current_mode]
|
||||||
dir = fd.askdirectory(initialdir = ut.parse_dir(data["destination"]),
|
dir = fd.askdirectory(initialdir = ut.parse_dir(data["destination"]),
|
||||||
title = "Select target directory")
|
title = "Select target directory")
|
||||||
self.v_targetdir.set(dir)
|
if len(dir) > 0:
|
||||||
self.update_mode_data("", "destination", dir)
|
self.v_targetdir.set(dir)
|
||||||
|
self.update_mode_data("", "destination", dir)
|
||||||
|
|
||||||
def set_directory(self, dir):
|
def set_directory(self, dir):
|
||||||
"""Sets the file directory for the app to use."""
|
"""Sets the file directory for the app to use."""
|
||||||
|
|
@ -201,7 +225,8 @@ class App(tk.Tk):
|
||||||
"""Prompts which file directory for the app to use."""
|
"""Prompts which file directory for the app to use."""
|
||||||
dir = fd.askdirectory(initialdir = ut.parse_dir(self.fm.dir),
|
dir = fd.askdirectory(initialdir = ut.parse_dir(self.fm.dir),
|
||||||
title = "Select directory")
|
title = "Select directory")
|
||||||
self.set_directory(dir)
|
if len(dir) > 0:
|
||||||
|
self.set_directory(dir)
|
||||||
|
|
||||||
def ask_load_rules(self):
|
def ask_load_rules(self):
|
||||||
"""Prompts which file rules config for the app to use."""
|
"""Prompts which file rules config for the app to use."""
|
||||||
|
|
@ -210,27 +235,38 @@ class App(tk.Tk):
|
||||||
dir = fd.askopenfilename(initialdir = initdir,
|
dir = fd.askopenfilename(initialdir = initdir,
|
||||||
filetypes = filetypes,
|
filetypes = filetypes,
|
||||||
title = "Load config file")
|
title = "Load config file")
|
||||||
self.load_config(dir)
|
if len(dir) > 0:
|
||||||
|
self.custom_rules_path = dir
|
||||||
|
self.load_config(dir)
|
||||||
|
|
||||||
def ask_save_rules(self):
|
def ask_save_rules(self):
|
||||||
"""Prompts which file name to use to save the file rules config."""
|
"""Prompts which file name to use to save the file rules config."""
|
||||||
initdir = ut.parse_dir(self.custom_rules_path)
|
initdir = ""
|
||||||
filetypes = (("Config files", "*.json"))
|
if self.custom_rules_path == RULES_PATH:
|
||||||
|
initdir = ut.parse_dir("~/") # Switch to home directory
|
||||||
|
else:
|
||||||
|
initdir = ut.parse_dir(self.custom_rules_path)
|
||||||
|
|
||||||
|
filetypes = (("Config files", "*.json"), ("All files", "*.*"))
|
||||||
dir = fd.asksaveasfilename(initialdir = initdir,
|
dir = fd.asksaveasfilename(initialdir = initdir,
|
||||||
filetypes = filetypes,
|
filetypes = filetypes,
|
||||||
title = "Save config file")
|
title = "Save config file")
|
||||||
self.custom_rules_path = dir
|
if len(dir) > 0:
|
||||||
self.save_config(dir)
|
if ".json" not in dir: dir += ".json"
|
||||||
|
self.custom_rules_path = dir
|
||||||
|
self.save_config(dir)
|
||||||
|
|
||||||
def prep(self):
|
def prep(self):
|
||||||
"""Makes the app ready to use after initialization."""
|
"""Makes the app ready to use after initialization."""
|
||||||
self.update_fileview()
|
self.update_fileview()
|
||||||
|
self.v_modelist.set([v["name"] for v in self.fm.filemodes.values()])
|
||||||
if self.current_mode in self.fm.filemodes:
|
if self.current_mode in self.fm.filemodes:
|
||||||
self.set_rules_variables(self.current_mode)
|
self.set_rules_variables(self.current_mode)
|
||||||
|
|
||||||
def run_task(self):
|
def run_task(self):
|
||||||
"""Runs the main file sorting task."""
|
"""Runs the main file sorting task."""
|
||||||
result = mb.askyesno("Task", "Ready to sort all files?")
|
result = mb.askyesno("Task", f"Ready? {len(self.fm.filedata)} files "
|
||||||
|
"will be affected.")
|
||||||
if not result: return
|
if not result: return
|
||||||
|
|
||||||
log.info("File organization started")
|
log.info("File organization started")
|
||||||
|
|
@ -299,15 +335,8 @@ class App(tk.Tk):
|
||||||
"""Files tab of the app."""
|
"""Files tab of the app."""
|
||||||
frame = ttk.Frame(self.book)
|
frame = ttk.Frame(self.book)
|
||||||
|
|
||||||
box = ttk.Frame(frame)
|
button = ttk.Button(frame, text="Start Task", command=self.run_task)
|
||||||
|
button.pack(side=tk.TOP, anchor=tk.W, pady=5)
|
||||||
button = ttk.Button(box, text="Start Task", command=self.run_task)
|
|
||||||
button.grid(column=0, row=0, pady=5)
|
|
||||||
|
|
||||||
button = ttk.Button(box, text="Refresh", command=self.update_fileview)
|
|
||||||
button.grid(column=1, row=0, pady=5)
|
|
||||||
|
|
||||||
box.pack(side=tk.TOP, fill=tk.X)
|
|
||||||
|
|
||||||
self.fileview_columns = ("Last Modified", "Size")
|
self.fileview_columns = ("Last Modified", "Size")
|
||||||
self.fileview = ttk.Treeview(frame, columns=self.fileview_columns)
|
self.fileview = ttk.Treeview(frame, columns=self.fileview_columns)
|
||||||
|
|
@ -353,22 +382,29 @@ class App(tk.Tk):
|
||||||
|
|
||||||
details = ttk.Frame(main)
|
details = ttk.Frame(main)
|
||||||
|
|
||||||
|
i_frame = ttk.Labelframe(details, text="Name")
|
||||||
|
|
||||||
|
self.v_name = tk.StringVar()
|
||||||
|
f_name = lambda e: um("", "name", self.v_name.get())
|
||||||
|
entry = ttk.Entry(i_frame, textvariable=self.v_name)
|
||||||
|
entry.bind('<Return>', f_name)
|
||||||
|
entry.pack(side=tk.LEFT)
|
||||||
|
i_frame.pack(side=tk.TOP, anchor=tk.W, pady=5)
|
||||||
|
|
||||||
i_frame = ttk.Labelframe(details, text="Action")
|
i_frame = ttk.Labelframe(details, text="Action")
|
||||||
|
|
||||||
self.v_radio = tk.StringVar()
|
self.v_action = tk.StringVar()
|
||||||
self.v_actions = ("move", "copy", "delete", "ignore")
|
self.v_actions = ("move", "copy", "delete", "ignore")
|
||||||
f_radio = lambda: um("", "action", self.v_radio.get())
|
f_action = lambda: um("", "action", self.v_action.get())
|
||||||
for s in self.v_actions:
|
for s in self.v_actions:
|
||||||
str = f"{s.capitalize()} "
|
str = f"{s.capitalize()} "
|
||||||
radio = ttk.Radiobutton(i_frame, text=str, value=s,
|
radio = ttk.Radiobutton(i_frame, text=str, value=s,
|
||||||
variable=self.v_radio, command = f_radio)
|
variable=self.v_action, command = f_action)
|
||||||
radio.pack(side=tk.LEFT)
|
radio.pack(side=tk.LEFT)
|
||||||
i_frame.pack(side=tk.TOP, anchor=tk.W, pady=5)
|
i_frame.pack(side=tk.TOP, anchor=tk.W, pady=5)
|
||||||
|
|
||||||
i_frame = ttk.Labelframe(details, text="Target Directory")
|
i_frame = ttk.Labelframe(details, text="Target Directory")
|
||||||
|
|
||||||
# text="Start with \"~/\" for your home (user) directory.")
|
|
||||||
|
|
||||||
self.v_targetdir = tk.StringVar()
|
self.v_targetdir = tk.StringVar()
|
||||||
f_targetdir = lambda e: um("", "destination", self.v_targetdir.get())
|
f_targetdir = lambda e: um("", "destination", self.v_targetdir.get())
|
||||||
entry = ttk.Entry(i_frame, textvariable=self.v_targetdir)
|
entry = ttk.Entry(i_frame, textvariable=self.v_targetdir)
|
||||||
|
|
@ -381,11 +417,7 @@ class App(tk.Tk):
|
||||||
|
|
||||||
i_frame.pack(side=tk.TOP, fill=tk.X, pady=5)
|
i_frame.pack(side=tk.TOP, fill=tk.X, pady=5)
|
||||||
|
|
||||||
i_frame = ttk.Labelframe(details, text="File Extensions")
|
i_frame = ttk.Labelframe(details, text="File Filters")
|
||||||
|
|
||||||
label = ttk.Label(i_frame,
|
|
||||||
text="Separate extensions with a comma and space.")
|
|
||||||
label.pack(side=tk.TOP, anchor=tk.W)
|
|
||||||
|
|
||||||
self.v_extstr = tk.StringVar()
|
self.v_extstr = tk.StringVar()
|
||||||
f_extstr = lambda e: um("", "extensions", self.v_extstr.get())
|
f_extstr = lambda e: um("", "extensions", self.v_extstr.get())
|
||||||
|
|
|
||||||
|
|
@ -3,57 +3,57 @@
|
||||||
"all": {
|
"all": {
|
||||||
"action": "ignore",
|
"action": "ignore",
|
||||||
"active": true,
|
"active": true,
|
||||||
"destination": "~/Downloads/FinchFiler/",
|
"destination": "~/",
|
||||||
"extensions": ["*"],
|
"extensions": ["*.*"],
|
||||||
"name": "All Files"
|
"name": "All Files"
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
"action": "ignore",
|
"action": "ignore",
|
||||||
"active": true,
|
"active": true,
|
||||||
"destination": "~/Pictures/",
|
"destination": "~/Pictures/",
|
||||||
"extensions": ["jpg", "jpeg", "png", "gif", "bmp", "psd", "raw", "webp"],
|
"extensions": ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp", "*.psd", "*.raw", "*.webp"],
|
||||||
"name": "Images"
|
"name": "Images"
|
||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"action": "ignore",
|
"action": "ignore",
|
||||||
"active": true,
|
"active": true,
|
||||||
"destination": "~/Music/",
|
"destination": "~/Music/",
|
||||||
"extensions": ["wav", "mp3", "ogg", "flac", "wma", "aiff", "aac"],
|
"extensions": ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.wma", "*.aiff", "*.aac"],
|
||||||
"name": "Audio"
|
"name": "Audio"
|
||||||
},
|
},
|
||||||
"video": {
|
"video": {
|
||||||
"action": "ignore",
|
"action": "ignore",
|
||||||
"active": true,
|
"active": true,
|
||||||
"destination": "~/Videos/",
|
"destination": "~/Videos/",
|
||||||
"extensions": ["avi", "mpeg", "mp4", "mov", "mkv", "ogv", "webm"],
|
"extensions": ["*.avi", "*.mpeg", "*.mp4", "*.mov", "*.mkv", "*.ogv", "*.webm"],
|
||||||
"name": "Video"
|
"name": "Video"
|
||||||
},
|
},
|
||||||
"document": {
|
"document": {
|
||||||
"action": "ignore",
|
"action": "ignore",
|
||||||
"active": true,
|
"active": true,
|
||||||
"destination": "~/Documents/",
|
"destination": "~/Documents/",
|
||||||
"extensions": ["txt", "doc", "docx", "pdf", "rtf"],
|
"extensions": ["*.txt", "*.doc", "*.docx", "*.pdf", "*.rtf"],
|
||||||
"name": "Documents"
|
"name": "Documents"
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"action": "ignore",
|
"action": "ignore",
|
||||||
"active": true,
|
"active": true,
|
||||||
"destination": "~/Documents/data/",
|
"destination": "~/Documents/data/",
|
||||||
"extensions": ["json", "csv", "db"],
|
"extensions": ["*.json", "*.csv", "*.db"],
|
||||||
"name": "Data"
|
"name": "Data"
|
||||||
},
|
},
|
||||||
"program": {
|
"program": {
|
||||||
"action": "ignore",
|
"action": "ignore",
|
||||||
"active": true,
|
"active": true,
|
||||||
"destination": "~/Downloads/",
|
"destination": "~/Downloads/",
|
||||||
"extensions": ["exe", "msi", "elf"],
|
"extensions": ["*.exe", "*.msi", "*.elf"],
|
||||||
"name": "Programs"
|
"name": "Programs"
|
||||||
},
|
},
|
||||||
"archive": {
|
"archive": {
|
||||||
"action": "ignore",
|
"action": "ignore",
|
||||||
"active": true,
|
"active": true,
|
||||||
"destination": "~/Downloads/",
|
"destination": "~/Downloads/",
|
||||||
"extensions": ["zip", "rar", "tar", "iso", "gz", "lz", "rz", "7z", "dmg"],
|
"extensions": ["*.zip", "*.rar", "*.tar", "*.iso", "*.gz", "*.lz", "*.rz", "*.7z", "*.dmg"],
|
||||||
"name": "Archives"
|
"name": "Archives"
|
||||||
},
|
},
|
||||||
"other": {
|
"other": {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import logging as log
|
||||||
import utils as ut
|
import utils as ut
|
||||||
import send2trash as s2t
|
import send2trash as s2t
|
||||||
|
|
||||||
debug_mode = True
|
debug_mode = False
|
||||||
|
|
||||||
class Manager:
|
class Manager:
|
||||||
"""File manager and organizer system."""
|
"""File manager and organizer system."""
|
||||||
|
|
@ -37,11 +37,15 @@ class Manager:
|
||||||
"""Sets the working directory of the file manager."""
|
"""Sets the working directory of the file manager."""
|
||||||
if os.path.exists(dir):
|
if os.path.exists(dir):
|
||||||
self.dir = dir
|
self.dir = dir
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError(f"Directory path not found: {dir}")
|
||||||
|
|
||||||
def set_config_path(self, dir):
|
def set_config_path(self, dir):
|
||||||
"""Sets the working config path of the file manager."""
|
"""Sets the working config path of the file manager."""
|
||||||
if os.path.exists(dir):
|
if os.path.exists(dir):
|
||||||
self.config_path = dir
|
self.config_path = dir
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError(f"Config path not found: {dir}")
|
||||||
|
|
||||||
def setup_file_rules(self, data, reset=False):
|
def setup_file_rules(self, data, reset=False):
|
||||||
"""Sets or resets file rules from the given data."""
|
"""Sets or resets file rules from the given data."""
|
||||||
|
|
@ -70,10 +74,21 @@ class Manager:
|
||||||
def get_file_properties(self, short_filename):
|
def get_file_properties(self, short_filename):
|
||||||
"""Returns the time, size, mode, and full path of a single file."""
|
"""Returns the time, size, mode, and full path of a single file."""
|
||||||
fullpath = os.path.join(self.dir, short_filename)
|
fullpath = os.path.join(self.dir, short_filename)
|
||||||
# Maybe check if fullpath exists (os.path.exists(fullpath))
|
try:
|
||||||
|
os.stat(fullpath)
|
||||||
|
except Exception as error:
|
||||||
|
raise error
|
||||||
|
|
||||||
_, ext = os.path.splitext(fullpath)
|
_, ext = os.path.splitext(fullpath)
|
||||||
ext = ext[1:].lower()
|
ext = ext[1:].lower()
|
||||||
mode = self.filetypes[ext] if ext in self.filetypes else "other"
|
|
||||||
|
mode = "other"
|
||||||
|
for k, v in self.filetypes.items():
|
||||||
|
if ext in k: mode = v # Matched extension
|
||||||
|
for k, v in self.filetypes.items():
|
||||||
|
if k in short_filename: mode = v # Matched file name
|
||||||
|
# File name has priority over extension
|
||||||
|
|
||||||
stat = os.stat(fullpath)
|
stat = os.stat(fullpath)
|
||||||
return {"time": stat.st_mtime, "size": stat.st_size, "mode": mode,
|
return {"time": stat.st_mtime, "size": stat.st_size, "mode": mode,
|
||||||
"fullpath": fullpath}
|
"fullpath": fullpath}
|
||||||
|
|
@ -123,12 +138,16 @@ class Manager:
|
||||||
if action in w_stats: w_stats[action] += 1
|
if action in w_stats: w_stats[action] += 1
|
||||||
if action != "ignore": w_stats["total"] += 1
|
if action != "ignore": w_stats["total"] += 1
|
||||||
|
|
||||||
log.info(f"Successfully processed {w_stats['total']} files in total")
|
if debug_mode:
|
||||||
|
log.info(f"Simulated {w_stats['total']} files in total")
|
||||||
|
else:
|
||||||
|
log.info(f"Processed {w_stats['total']} files in total")
|
||||||
log.info(f"Moved {w_stats['move']} files")
|
log.info(f"Moved {w_stats['move']} files")
|
||||||
log.info(f"Copied {w_stats['copy']} files")
|
log.info(f"Copied {w_stats['copy']} files")
|
||||||
log.info(f"Deleted {w_stats['delete']} files")
|
log.info(f"Deleted {w_stats['delete']} files")
|
||||||
|
|
||||||
def set_debug_mode(value):
|
def set_debug_mode(value):
|
||||||
|
"""Enables or disables the file manager debug mode."""
|
||||||
global debug_mode
|
global debug_mode
|
||||||
if type(value) == None:
|
if type(value) == None:
|
||||||
debug_mode = not debug_mode # Toggles the mode
|
debug_mode = not debug_mode # Toggles the mode
|
||||||
|
|
@ -136,25 +155,34 @@ def set_debug_mode(value):
|
||||||
debug_mode = value
|
debug_mode = value
|
||||||
|
|
||||||
def move(src, dst):
|
def move(src, dst):
|
||||||
|
"""Moves a file from a source to a destination."""
|
||||||
src, dst = ut.parse_dir(src), ut.parse_dir(dst)
|
src, dst = ut.parse_dir(src), ut.parse_dir(dst)
|
||||||
log.info(f"Moved file: {src} > {dst}")
|
log.info(f"Moved file: {src} > {dst}")
|
||||||
if not debug_mode:
|
if not debug_mode:
|
||||||
shutil.move(src, dst)
|
try:
|
||||||
|
shutil.move(src, dst)
|
||||||
|
except Exception as error:
|
||||||
|
print(error)
|
||||||
return src, dst
|
return src, dst
|
||||||
|
|
||||||
def copy(src, dst):
|
def copy(src, dst):
|
||||||
|
"""Copies a file from a source to a destination."""
|
||||||
src, dst = ut.parse_dir(src), ut.parse_dir(dst)
|
src, dst = ut.parse_dir(src), ut.parse_dir(dst)
|
||||||
log.info(f"Copied file: {src} > {dst}")
|
log.info(f"Copied file: {src} > {dst}")
|
||||||
if not debug_mode:
|
if not debug_mode:
|
||||||
shutil.copy2(src, dst)
|
try:
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
except Exception as error:
|
||||||
|
print(error)
|
||||||
return src, dst
|
return src, dst
|
||||||
|
|
||||||
def delete(src):
|
def delete(src):
|
||||||
|
"""Moves a file to the OS trash equivalent."""
|
||||||
src = ut.parse_dir(src)
|
src = ut.parse_dir(src)
|
||||||
log.info(f"Deleted file: {src}")
|
log.info(f"Deleted file: {src}")
|
||||||
if not debug_mode:
|
if not debug_mode:
|
||||||
try:
|
try:
|
||||||
s2t.send2trash(src)
|
s2t.send2trash(src)
|
||||||
except s2t.TrashPermissionError:
|
except s2t.TrashPermissionError as error:
|
||||||
pass
|
print(error)
|
||||||
return src
|
return src
|
||||||
28
script.py
28
script.py
|
|
@ -1,4 +1,4 @@
|
||||||
"""Script."""
|
"""Script mode for Finch Filer."""
|
||||||
|
|
||||||
__author__ = "Gull"
|
__author__ = "Gull"
|
||||||
|
|
||||||
|
|
@ -8,9 +8,10 @@ import utils as ut
|
||||||
import file_manager as fm
|
import file_manager as fm
|
||||||
from argparse import ArgumentParser as ap
|
from argparse import ArgumentParser as ap
|
||||||
|
|
||||||
LOG_PATH = "%/Gull/script.log"
|
LOG_PATH = "%/Gullbase/FinchFiler/script.log"
|
||||||
SOURCE_PATH = "~/Downloads"
|
SOURCE_PATH = "~/Downloads"
|
||||||
RULES_PATH = "%/Gull/file_rules_custom.json"
|
RULES_PATH = "%/Gullbase/FinchFiler/file_rules_custom.json"
|
||||||
|
RULES_PATH_ALT = "data/file_rules_default.json"
|
||||||
|
|
||||||
def setup_log():
|
def setup_log():
|
||||||
log.basicConfig(**ut.get_log_config(LOG_PATH, log.DEBUG))
|
log.basicConfig(**ut.get_log_config(LOG_PATH, log.DEBUG))
|
||||||
|
|
@ -40,14 +41,25 @@ def run():
|
||||||
|
|
||||||
setup_log()
|
setup_log()
|
||||||
|
|
||||||
fm.set_debug_mode(True) # Temporary
|
|
||||||
|
|
||||||
mgr = fm.Manager()
|
mgr = fm.Manager()
|
||||||
mgr.set_directory(source)
|
try:
|
||||||
mgr.setup_file_rules(ut.load_json_file(rules))
|
mgr.set_directory(source)
|
||||||
|
except FileNotFoundError as error:
|
||||||
|
print(error)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
mgr.setup_file_rules(ut.load_json_file(rules))
|
||||||
|
except FileNotFoundError as error:
|
||||||
|
try:
|
||||||
|
rules = ut.parse_dir(RULES_PATH_ALT, True)
|
||||||
|
mgr.setup_file_rules(ut.load_json_file(rules))
|
||||||
|
except FileNotFoundError as error:
|
||||||
|
print(error)
|
||||||
|
return
|
||||||
|
|
||||||
if mode != "default" and "all" in mgr.filemodes:
|
if mode != "default" and "all" in mgr.filemodes:
|
||||||
mgr.filemodes["all"]["action"] = mode
|
mgr.filemodes["all"]["action"] = mode
|
||||||
# TODO: Error handling
|
|
||||||
|
|
||||||
mgr.update_file_data()
|
mgr.update_file_data()
|
||||||
mgr.run_task()
|
mgr.run_task()
|
||||||
|
|
|
||||||
38
utils.py
38
utils.py
|
|
@ -37,42 +37,42 @@ def format_date(timestamp):
|
||||||
|
|
||||||
def load_json_file(filepath):
|
def load_json_file(filepath):
|
||||||
"""Opens a JSON file, and returns JSON data as a dict."""
|
"""Opens a JSON file, and returns JSON data as a dict."""
|
||||||
try: # Needs improvement!
|
try:
|
||||||
with open(filepath, "r", encoding="utf-8") as f:
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
return data
|
return data
|
||||||
except:
|
except Exception as error:
|
||||||
return None
|
raise error
|
||||||
|
|
||||||
def save_json_file(filepath, data, raw=False):
|
def save_json_file(filepath, data, raw=False):
|
||||||
"""Saves a JSON file, given a dict."""
|
"""Saves a JSON file, given a dict."""
|
||||||
try:
|
try:
|
||||||
with open(filepath, "w", encoding="utf-8") as f:
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, sort_keys=True, indent=None if raw else 2)
|
json.dump(data, f, sort_keys=True, indent=None if raw else 2)
|
||||||
except:
|
except Exception as error:
|
||||||
return False
|
raise error
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_user_dir(dir=""):
|
|
||||||
"""Returns the joined user directory."""
|
|
||||||
return os.path.join(os.path.expanduser("~"), dir)
|
|
||||||
|
|
||||||
def get_appdata_dir(dir=""):
|
|
||||||
"""Returns the joined appdata directory."""
|
|
||||||
return os.path.join(ad.user_data_dir(None, False), dir)
|
|
||||||
|
|
||||||
def parse_dir(path="", ignore_mkdir=False):
|
def parse_dir(path="", ignore_mkdir=False):
|
||||||
"""Converts and creates a directory (can be full file path)."""
|
"""Converts and creates a directory (can be full file path)."""
|
||||||
if path[0] == "~": # Denotes the current user directory
|
if path[0] == "~": # Denotes the current user directory
|
||||||
path = get_user_dir(path.lstrip("~/"))
|
path = pathlib.Path(path).expanduser()
|
||||||
elif path[0] == "%": # Denotes the local appdata directory
|
elif path[0] == "%": # Denotes the local appdata directory
|
||||||
path = get_appdata_dir(path.lstrip("%/"))
|
path = pathlib.Path(ad.user_data_dir(None, False)) / path.lstrip("%/")
|
||||||
|
else:
|
||||||
|
path = pathlib.Path(path)
|
||||||
|
|
||||||
if not ignore_mkdir:
|
if not ignore_mkdir:
|
||||||
pathlib.Path(os.path.split(path)[0]).mkdir(parents=True, exist_ok=True)
|
try:
|
||||||
|
# This seems sloppy
|
||||||
|
dir = str(path)
|
||||||
|
_, ext = os.path.splitext(dir)
|
||||||
|
if len(ext) > 0: dir = os.path.dirname(dir) #Is file
|
||||||
|
pathlib.Path(dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
except Exception as error:
|
||||||
|
raise error
|
||||||
|
|
||||||
return path
|
# Returns as a string
|
||||||
|
return str(path)
|
||||||
|
|
||||||
def get_log_config(path="~/", level=None):
|
def get_log_config(path="~/", level=None):
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue