import os import sqlite3 import time import flask_bcrypt import config class CheckSumCalculator: def __init__(self): pass def checksum(self, string: str): return flask_bcrypt.generate_password_hash(string, config.bcrypt_rounds) class DuplicateDirectoryException(Exception): pass class DuplicateUserException(Exception): pass class User: """ Data structure to hold user information """ def __init__(self, username: str, hashed_password: bytes, admin: bool): self.username = username self.hashed_password = hashed_password self.admin = admin self.readable_directories = set() class Option: """ Data structure to hold a directory option """ def __init__(self, key: str, value: str, dir_id: int = None, opt_id: int = None): self.key = key self.value = value self.id = opt_id self.dir_id = dir_id class Directory: """ Data structure to hold directory information """ def __init__(self, path: str, enabled: bool, options: list, name: str): self.id = None self.path = path self.enabled = enabled self.options = options self.name = name def __str__(self): return self.path + " | enabled: " + str(self.enabled) + " | opts: " + str(self.options) def get_option(self, key): for option in self.options: if option.key == key: return option.value return None def set_default_options(self): self.options = [] for option in config.default_options: self.options.append(Option(option, config.default_options[option])) class Task: INDEX = 1 GEN_THUMBNAIL = 2 def __init__(self, task_type: int, dir_id: int, completed: bool = False, completed_time: time.time = None, task_id: int = None): self.id = task_id self.type = task_type self.dir_id = dir_id self.completed = completed self.completed_time = completed_time class LocalStorage: """ Manages storage of application data to disk. Could be refactored into a abstract class to switch from SQLite3 to something else """ def __init__(self, db_path): self.cached_dirs = {} self.cached_users = {} self.cached_tasks = {} self.db_path = db_path self.dir_cache_outdated = True # Indicates that the database was changed since it was cached in memory self.user_cache_outdated = True self.task_cache_outdated = True def init_db(self, script_path): """Creates a blank database. Overwrites the old one""" if os.path.isfile(self.db_path): os.remove(self.db_path) conn = sqlite3.connect(self.db_path) c = conn.cursor() with open(script_path, "r") as f: c.executescript(f.read()) conn.commit() c.close() conn.close() def save_directory(self, directory: Directory): """ Save directory to storage :param directory: Directory to save :return: id """ self.dir_cache_outdated = True conn = sqlite3.connect(self.db_path) c = conn.cursor() try: c.execute("INSERT INTO Directory (path, enabled, name) VALUES (?, ?, ?)", (directory.path, directory.enabled, directory.name)) c.execute("SELECT last_insert_rowid()") dir_id = c.fetchone()[0] c.close() conn.commit() conn.close() for opt in directory.options: opt.dir_id = dir_id self.save_option(opt) return dir_id except sqlite3.IntegrityError: c.close() conn.close() raise DuplicateDirectoryException("Duplicate directory path: " + directory.path) def remove_directory(self, dir_id: int): """Remove a directory from the database""" self.dir_cache_outdated = True conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("DELETE FROM Option WHERE directory_id=?", (dir_id,)) c.execute("DELETE FROM Task WHERE directory_id=?", (dir_id,)) c.execute("DELETE FROM Directory WHERE id=?", (dir_id,)) c.close() conn.commit() conn.close() def dirs(self) -> dict: if self.dir_cache_outdated: self.cached_dirs = {} conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("SELECT id, path, enabled, name FROM Directory") db_directories = c.fetchall() c.execute("SELECT key, value, directory_id, id FROM Option") db_options = c.fetchall() for db_dir in db_directories: options = [] directory = Directory(db_dir[1], db_dir[2], options, db_dir[3]) for db_opt in db_options: if db_opt[2] == db_dir[0]: options.append(Option(db_opt[0], db_opt[1], db_opt[2], db_opt[3])) directory.id = db_dir[0] self.cached_dirs[directory.id] = directory self.dir_cache_outdated = False return self.cached_dirs else: return self.cached_dirs def save_user(self, user: User): """Save user to storage""" self.user_cache_outdated = True try: conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("INSERT INTO User (username, password, is_admin) VALUES (?,?,?)", (user.username, user.hashed_password, user.admin)) c.close() conn.commit() conn.close() except sqlite3.IntegrityError: raise DuplicateUserException("Duplicate username: " + user.username) def users(self) -> dict: """Get user list""" if self.user_cache_outdated: self.cached_users = {} conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("SELECT username, is_admin FROM User") db_users = c.fetchall() for db_user in db_users: user = User(db_user[0], b"", bool(db_user[1])) c.execute("SELECT username, directory_id FROM User_canRead_Directory WHERE username=?", (db_user[0],)) db_accesses = c.fetchall() for db_access in db_accesses: user.readable_directories.add(db_access[1]) self.cached_users[db_user[0]] = user conn.close() return self.cached_users else: return self.cached_users def auth_user(self, username: str, password: str) -> bool: """Authenticates an user""" conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("SELECT username, password FROM User WHERE username=?", (username,)) db_user = c.fetchone() if db_user is not None: return flask_bcrypt.check_password_hash(db_user[1], password) return False def update_user(self, user: User) -> None: """Updates an user. Will have no effect if the user does not exist""" self.user_cache_outdated = True conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("UPDATE User SET is_admin=? WHERE username=?", (user.admin, user.username)) c.execute("DELETE FROM User_canRead_Directory WHERE username=?", (user.username,)) conn.commit() for access in user.readable_directories: c.execute("INSERT INTO User_canRead_Directory VALUES (?,?)", (user.username, access)) c.close() conn.commit() conn.close() def remove_user(self, username: str): """Remove an user from the database""" self.user_cache_outdated = True conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("DELETE FROM User WHERE username=?", (username,)) c.close() conn.commit() conn.close() def update_directory(self, directory): """Updated a directory (Options are left untouched). Will have no effect if the directory does not exist""" self.dir_cache_outdated = True try: conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("UPDATE Directory SET name=?, path=?, enabled=? WHERE id=?", (directory.name, directory.path, directory.enabled, directory.id)) c.close() conn.commit() conn.close() except sqlite3.IntegrityError: raise DuplicateDirectoryException("Duplicate directory: " + directory.path) def save_option(self, option: Option): self.dir_cache_outdated = True conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("INSERT INTO Option (key, value, directory_id) VALUES (?, ?, ?)", (option.key, option.value, option.dir_id)) c.execute("SELECT last_insert_rowid()") opt_id = c.fetchone()[0] c.close() conn.commit() conn.close() return opt_id def del_option(self, opt_id): """Delete an option from the database""" self.dir_cache_outdated = True conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("DELETE FROM Option WHERE id=?", (opt_id,)) conn.commit() conn.close() def update_option(self, option: Option): """Updates an option""" self.dir_cache_outdated = True conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("UPDATE Option SET key=?, value=? WHERE id=?", (option.key, option.value, option.id)) conn.commit() conn.close() def save_task(self, task: Task): """Save a task to the database""" self.task_cache_outdated = True conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("INSERT INTO Task (directory_id, type, completed, completed_time) VALUES (?,?,?,?)", (task.dir_id, task.type, task.completed, task.completed_time)) c.execute("SELECT last_insert_rowid()") task_id = c.fetchone()[0] conn.commit() c.close() conn.close() return task_id def tasks(self): """Get the (cached) list of taks""" if self.task_cache_outdated: self.cached_tasks = {} conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("SELECT id, directory_id, type, completed, completed_time FROM Task") tasks = c.fetchall() c.close() conn.close() for db_task in tasks: task = Task(db_task[2], db_task[1], db_task[3], db_task[4], db_task[0]) self.cached_tasks[task.id] = task self.task_cache_outdated = False return self.cached_tasks else: return self.cached_tasks def del_task(self, task_id): """Delete a task from the database""" self.task_cache_outdated = True conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("DELETE FROM Task WHERE id=?", (task_id,)) conn.commit() c.close() conn.close() def set_access(self, username, dir_id, has_access): conn = sqlite3.connect(self.db_path) c = conn.cursor() if has_access: try: c.execute("INSERT INTO User_canRead_Directory VALUES (?,?)", (username, dir_id)) except sqlite3.IntegrityError: pass else: c.execute("DELETE FROM User_canRead_Directory WHERE username=? AND directory_id=?", (username, dir_id)) conn.commit() c.close() conn.close() def get_access(self, username): conn = sqlite3.connect(self.db_path) c = conn.cursor() c.execute("SELECT * FROM User_canRead_Directory WHERE username=?", (username,)) accesses = c.fetchall() access_list = [] for access in accesses: access_list.append(access[1]) return access_list