Improved treeview; added custom directory and message boxes
This commit is contained in:
parent
5a04dd37f1
commit
380212e2be
4 changed files with 143 additions and 50 deletions
109
app.py
109
app.py
|
|
@ -1,4 +1,4 @@
|
|||
"""Download utility app."""
|
||||
"""File sorter utility app."""
|
||||
|
||||
__name__ = "__main__"
|
||||
__author__ = "Gull"
|
||||
|
|
@ -8,9 +8,11 @@ import file_manager as fm
|
|||
import logging as log
|
||||
import utils as ut
|
||||
from tkinter import ttk
|
||||
from tkinter.messagebox import showinfo
|
||||
from tkinter import messagebox as mb
|
||||
from tkinter import filedialog as fd
|
||||
|
||||
debug_mode = False
|
||||
custom_rules_path = "%/Temp/file_rules_custom.json"
|
||||
|
||||
def set_debug_mode(value):
|
||||
global debug_mode
|
||||
|
|
@ -34,12 +36,13 @@ def setup_log():
|
|||
class App(tk.Tk):
|
||||
def __init__(self, width=640, height=480):
|
||||
super().__init__()
|
||||
self.title("Download Sorter")
|
||||
self.title("Gull's File Sorter")
|
||||
self.current_mode = "all"
|
||||
self.option_add('*tearOff', tk.FALSE)
|
||||
self.fm = fm.Manager(True)
|
||||
self.log = list()
|
||||
self.gui()
|
||||
self.load_config()
|
||||
self.prep()
|
||||
|
||||
screen_width = self.winfo_screenwidth()
|
||||
|
|
@ -50,27 +53,53 @@ class App(tk.Tk):
|
|||
|
||||
self.geometry(f"{width}x{height}+{center_x}+{center_y}")
|
||||
|
||||
def update_fileview(self, tree=True):
|
||||
self.fm.set_directory(self.fm.get_directory())
|
||||
def get_filemode_files(self, key):
|
||||
if key == "all": return
|
||||
|
||||
files = dict()
|
||||
for k, v in self.fm.filedata.items():
|
||||
if v["mode"] == key: files[k] = v
|
||||
|
||||
return files if len(files) > 0 else None
|
||||
|
||||
def update_fileview(self):
|
||||
tree = self.v_tree.get()
|
||||
self.fm.update_file_data()
|
||||
log.info(f"Processed file view with {len(self.fm.filedata)} items")
|
||||
log.info(f"Show as tree: {str(tree)}")
|
||||
|
||||
while len(self.fileview.get_children()) > 0:
|
||||
self.fileview.delete(self.fileview.get_children()[-1])
|
||||
|
||||
for k, v in self.fm.filedata.items():
|
||||
values = (k, str(ut.format_date(v["time"])),
|
||||
str(ut.format_bytes(v["size"])))
|
||||
self.fileview.insert("", tk.END, k, values=values)
|
||||
if tree:
|
||||
for k, v in self.fm.filemodes.items():
|
||||
files = self.get_filemode_files(k)
|
||||
if files != None:
|
||||
id = self.fileview.insert("", tk.END, text=v["name"],
|
||||
open=True)
|
||||
|
||||
def load_config(self, filepath="%/Temp/file_rules_custom.json"):
|
||||
data = ut.load_json_file(ut.parse_dir(filepath))
|
||||
self.fm.setup_file_rules(data, True)
|
||||
log.info(f"Loaded custom file rules")
|
||||
for k, v in files.items():
|
||||
values = (str(ut.format_date(v["time"])),
|
||||
str(ut.format_bytes(v["size"])))
|
||||
self.fileview.insert(id, tk.END, text=k, values=values)
|
||||
|
||||
def save_config(self, filepath="%/Temp/file_rules_custom.json"):
|
||||
ut.save_json_file(ut.parse_dir(filepath), self.fm.filemodes)
|
||||
log.info(f"Saved custom file rules")
|
||||
else:
|
||||
for k, v in self.fm.filedata.items():
|
||||
values = (str(ut.format_date(v["time"])),
|
||||
str(ut.format_bytes(v["size"])))
|
||||
self.fileview.insert("", tk.END, text=k, values=values)
|
||||
|
||||
def load_config(self, filepath=custom_rules_path):
|
||||
path = ut.parse_dir(filepath)
|
||||
data = ut.load_json_file(path)
|
||||
if data != None:
|
||||
self.fm.setup_file_rules(data, True)
|
||||
log.info(f"Loaded custom file rules: {path}")
|
||||
|
||||
def save_config(self, filepath=custom_rules_path):
|
||||
path = ut.parse_dir(filepath)
|
||||
ut.save_json_file(path, {"filemodes": self.fm.filemodes})
|
||||
log.info(f"Saved custom file rules: {path}")
|
||||
|
||||
def reset_config(self):
|
||||
data = ut.load_json_file(self.fm.config_path)
|
||||
|
|
@ -106,16 +135,40 @@ class App(tk.Tk):
|
|||
self.set_rules_variables(key)
|
||||
log.debug(f"Selected file mode: {key}")
|
||||
|
||||
def set_directory(self, dir):
|
||||
self.fm.set_directory(dir)
|
||||
self.update_fileview()
|
||||
|
||||
def ask_directory(self):
|
||||
dir = fd.askdirectory(initialdir = self.fm.get_directory(""),
|
||||
title = "Select directory")
|
||||
self.set_directory(dir)
|
||||
|
||||
def prep(self):
|
||||
self.update_fileview()
|
||||
if self.current_mode in self.fm.filemodes:
|
||||
self.set_rules_variables(self.current_mode)
|
||||
|
||||
def run_task(self):
|
||||
result = mb.askyesno("Task", "Ready to sort all files?")
|
||||
if not result: return
|
||||
|
||||
log.info("File organization started")
|
||||
self.fm.run_task()
|
||||
|
||||
result = self.fm.work
|
||||
stats = {"move": 0, "copy": 0, "delete": 0}
|
||||
for v in result: stats[v[0]] += 1
|
||||
mb.showinfo("Task",
|
||||
f"Successfully processed {len(result)} files in total.\n"
|
||||
+ f"Moved {stats["move"]} files.\n"
|
||||
+ f"Copied {stats["copy"]} files.\n"
|
||||
+ f"Deleted {stats["delete"]} files.\n")
|
||||
|
||||
def run_backup(self):
|
||||
result = mb.askyesno("Backup", "Ready to back up all files?")
|
||||
if not result: return
|
||||
|
||||
log.info("File backup started")
|
||||
|
||||
def gui(self):
|
||||
|
|
@ -132,6 +185,8 @@ class App(tk.Tk):
|
|||
self.config(menu=a_menu)
|
||||
|
||||
a_menu_file = tk.Menu(a_menu)
|
||||
a_menu_file.add_command(label="Set Directory",
|
||||
command=self.ask_directory)
|
||||
a_menu_file.add_command(label="Exit", command=self.destroy)
|
||||
|
||||
a_menu.add_cascade(
|
||||
|
|
@ -154,13 +209,21 @@ class App(tk.Tk):
|
|||
button = ttk.Button(box, text="Backup", command=self.run_backup)
|
||||
button.grid(column=2, row=0, pady=5)
|
||||
|
||||
self.v_tree = tk.BooleanVar(value=False)
|
||||
f_check = lambda: self.update_fileview()
|
||||
check = ttk.Checkbutton(box, text="Show As Tree", variable=self.v_tree,
|
||||
command=f_check)
|
||||
check.grid(column=3, row=0, padx=5, pady=5)
|
||||
|
||||
box.pack(side=tk.TOP, fill=tk.X)
|
||||
|
||||
self.fileview_columns = ("Name", "Last Modified", "Size")
|
||||
self.fileview = ttk.Treeview(frame, columns=self.fileview_columns,
|
||||
show="headings")
|
||||
self.fileview_columns = ("Last Modified", "Size")
|
||||
self.fileview = ttk.Treeview(frame, columns=self.fileview_columns)
|
||||
# Disabled "show=headings"
|
||||
|
||||
for v in self.fileview_columns:
|
||||
self.fileview.heading("#0", text="Name") # First (key) column
|
||||
self.fileview.column("#0", width=200, anchor=tk.W)
|
||||
for v in self.fileview_columns: # Value columns
|
||||
self.fileview.heading(v, text=v)
|
||||
self.fileview.column(v, width=100, anchor=tk.W)
|
||||
|
||||
|
|
@ -210,7 +273,7 @@ class App(tk.Tk):
|
|||
radio = ttk.Radiobutton(i_frame, text=str, value=s,
|
||||
variable=self.v_radio, command = f_radio)
|
||||
radio.pack(side=tk.LEFT)
|
||||
i_frame.pack(side=tk.TOP, anchor=tk.W)
|
||||
i_frame.pack(side=tk.TOP, anchor=tk.W, pady=5)
|
||||
|
||||
i_frame = ttk.Labelframe(details, text="Target Directory")
|
||||
|
||||
|
|
@ -225,7 +288,7 @@ class App(tk.Tk):
|
|||
entry.bind('<Return>', f_targetdir)
|
||||
entry.pack(side=tk.TOP, fill=tk.X)
|
||||
|
||||
i_frame.pack(side=tk.TOP, fill=tk.X)
|
||||
i_frame.pack(side=tk.TOP, fill=tk.X, pady=5)
|
||||
|
||||
i_frame = ttk.Labelframe(details, text="File Extensions")
|
||||
|
||||
|
|
@ -239,7 +302,7 @@ class App(tk.Tk):
|
|||
entry.bind('<Return>', f_extstr)
|
||||
entry.pack(side=tk.TOP, fill=tk.X)
|
||||
|
||||
i_frame.pack(side=tk.TOP, fill=tk.X)
|
||||
i_frame.pack(side=tk.TOP, fill=tk.X, pady=5)
|
||||
|
||||
details.grid(row=0, column=1, sticky=tk.NSEW)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"filemodes": {
|
||||
"all": {
|
||||
"action": "delete",
|
||||
"action": "ignore",
|
||||
"active": true,
|
||||
"destination": "~/Downloads/",
|
||||
"extensions": ["*"],
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ import send2trash as s2t
|
|||
debug_mode = True
|
||||
|
||||
class Manager:
|
||||
"""File manager that can store and sort files in a directory."""
|
||||
def __init__(self, auto_config=False):
|
||||
self.dir = os.path.expanduser("~") # Default directory
|
||||
self.filedata = dict()
|
||||
self.work = list() # Work history of last task
|
||||
self.filedata = dict() # Dictionary of files in directory
|
||||
self.filemodes = dict() # Contains file mode data
|
||||
self.filetypes = dict() # Maps file extensions to modes
|
||||
self.config_path = "data/file_rules_default.json"
|
||||
|
|
@ -23,17 +25,20 @@ class Manager:
|
|||
self.setup_file_rules(ut.load_json_file(self.config_path))
|
||||
|
||||
def get_directory(self, target="Downloads"):
|
||||
"""Returns a file path, appending the target string."""
|
||||
dir = os.path.join(os.path.expanduser("~"), target)
|
||||
if os.path.exists(dir): # Needs error handling
|
||||
return dir
|
||||
|
||||
def set_directory(self, dir):
|
||||
"""Sets the working directory of the file manager."""
|
||||
if os.path.exists(dir):
|
||||
self.dir = dir
|
||||
|
||||
def setup_file_rules(self, data, reset=False):
|
||||
if reset: self.filemodes.clear()
|
||||
"""Sets or resets file rules from the given data."""
|
||||
if not "filemodes" in data: return
|
||||
if reset: self.filemodes.clear()
|
||||
|
||||
filemodes = data["filemodes"]
|
||||
for k, v in filemodes.items():
|
||||
|
|
@ -42,59 +47,75 @@ class Manager:
|
|||
self.match_file_types()
|
||||
|
||||
def match_file_types(self):
|
||||
"""Organizes the file types dict into modes."""
|
||||
self.filetypes.clear()
|
||||
for k, v in self.filemodes.items():
|
||||
if "extensions" in v:
|
||||
for ext in v["extensions"]:
|
||||
self.filetypes[ext] = k
|
||||
|
||||
def modify_type_list(self, key, values):
|
||||
def set_type_list(self, key, values):
|
||||
"""Sets the list of extensions for the given file type."""
|
||||
if key in self.filetypes:
|
||||
self.filetypes[key] = list(values)
|
||||
|
||||
def get_file_properties(self, short_filename):
|
||||
"""Returns the time, size, mode, and full path of a single file."""
|
||||
fullpath = os.path.join(self.dir, short_filename)
|
||||
# Maybe check if fullpath exists (os.path.exists(fullpath))
|
||||
_, ext = os.path.splitext(fullpath)
|
||||
ext = ext[1:].lower()
|
||||
mode = self.filetypes[ext] if ext in self.filetypes else "other"
|
||||
stat = os.stat(fullpath)
|
||||
return {"time": stat.st_mtime, "size": stat.st_size, "mode": mode,
|
||||
"fullpath": fullpath}
|
||||
|
||||
def update_file_data(self):
|
||||
"""Updates the data of all files in the active directory."""
|
||||
self.filedata.clear()
|
||||
self.force_update = False
|
||||
with os.scandir(self.dir) as scan:
|
||||
for entry in scan:
|
||||
if entry.is_file():
|
||||
st = entry.stat()
|
||||
data = {"time": st.st_mtime, "size": st.st_size}
|
||||
data = self.get_file_properties(entry.name)
|
||||
self.filedata[entry.name] = data
|
||||
|
||||
def run_task(self):
|
||||
"""Sorts all files in the active directory, following file rules."""
|
||||
self.work.clear()
|
||||
self.match_file_types() # Updates file types dict
|
||||
log.info(f"Now processing {len(self.filedata)} files")
|
||||
if debug_mode:
|
||||
log.warning("Debug mode is enabled; file actions will be ignored")
|
||||
for k, v in self.filedata.items():
|
||||
fullpath = os.path.join(self.dir, k)
|
||||
if os.path.exists(fullpath):
|
||||
_, ext = os.path.splitext(fullpath)
|
||||
ext = ext[1:].lower()
|
||||
mode = self.filetypes[ext] if ext in self.filetypes else "other"
|
||||
v["mode"] = mode
|
||||
v["fullpath"] = fullpath
|
||||
if self.force_update:
|
||||
self.update_file_data()
|
||||
|
||||
for k, v in self.filedata.items():
|
||||
for v in self.filedata.values():
|
||||
rule = self.filemodes[v["mode"]]
|
||||
if rule["action"] == "move":
|
||||
move(v["fullpath"], rule["destination"])
|
||||
elif rule["action"] == "copy":
|
||||
copy(v["fullpath"], rule["destination"])
|
||||
elif rule["action"] == "delete":
|
||||
delete(v["fullpath"])
|
||||
action = rule["action"]
|
||||
if action == "move":
|
||||
src, dst = move(v["fullpath"], rule["destination"])
|
||||
self.work.append((action, src, dst))
|
||||
elif action == "copy":
|
||||
src, dst = copy(v["fullpath"], rule["destination"])
|
||||
self.work.append((action, src, dst))
|
||||
elif action == "delete":
|
||||
src = delete(v["fullpath"])
|
||||
self.work.append((action, src, "(trash)"))
|
||||
|
||||
def move(src, dst):
|
||||
src, dst = ut.parse_dir(src), ut.parse_dir(dst)
|
||||
log.info(f"Moved file: {src} > {dst}")
|
||||
if not debug_mode:
|
||||
shutil.move(src, dst)
|
||||
return src, dst
|
||||
|
||||
def copy(src, dst):
|
||||
src, dst = ut.parse_dir(src), ut.parse_dir(dst)
|
||||
log.info(f"Copied file: {src} > {dst}")
|
||||
if not debug_mode:
|
||||
shutil.copy2(src, dst)
|
||||
return src, dst
|
||||
|
||||
def delete(src):
|
||||
src = ut.parse_dir(src)
|
||||
|
|
@ -103,4 +124,5 @@ def delete(src):
|
|||
try:
|
||||
s2t.send2trash(src)
|
||||
except s2t.TrashPermissionError:
|
||||
pass
|
||||
pass
|
||||
return src
|
||||
18
utils.py
18
utils.py
|
|
@ -36,14 +36,22 @@ def format_date(timestamp):
|
|||
|
||||
def load_json_file(filepath):
|
||||
"""Opens a JSON file, and returns JSON data as a dict."""
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
try: # Needs improvement!
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
except:
|
||||
return None
|
||||
|
||||
def save_json_file(filepath, data, raw=False):
|
||||
"""Saves a JSON file, given a dict."""
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, sort_keys=True, indent=None if raw else 2)
|
||||
try:
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, sort_keys=True, indent=None if raw else 2)
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def get_user_dir(dir=""):
|
||||
"""Returns the joined user directory."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue