From 5f0130d0d156b36fd85e9a2ad2286ba4c6191212 Mon Sep 17 00:00:00 2001 From: StNicolay Date: Wed, 21 Aug 2024 05:22:00 +0300 Subject: [PATCH] God, I hope I'm ready --- desktop_client/create_folder_widget.py | 41 +++++++++------ desktop_client/file.py | 40 +++++++++++---- desktop_client/file_widgets.py | 15 ++++-- desktop_client/folder.py | 27 ++++++---- desktop_client/folder_info.py | 70 +++++++++++++++----------- desktop_client/state.py | 7 +-- desktop_client/sync.py | 43 +++++++++++----- desktop_client/user.py | 4 +- poetry.lock | 43 ++++++++++++---- pyproject.toml | 1 + 10 files changed, 193 insertions(+), 98 deletions(-) diff --git a/desktop_client/create_folder_widget.py b/desktop_client/create_folder_widget.py index 9683cd8..4b9dbaf 100644 --- a/desktop_client/create_folder_widget.py +++ b/desktop_client/create_folder_widget.py @@ -3,12 +3,15 @@ from __future__ import annotations import uuid import file_widgets -from PyQt6.QtWidgets import QLineEdit, QMessageBox, QPushButton, QVBoxLayout, QWidget +from PyQt6.QtWidgets import (QLineEdit, QMessageBox, QPushButton, QVBoxLayout, + QWidget) from request_client import RequestClient class CreateFolderWidget(QWidget): - def __init__(self, folder_id: uuid.UUID, file_list: file_widgets.FileListWidget): + def __init__( + self, folder_id: uuid.UUID, file_list: file_widgets.FileListWidget + ): super().__init__() self.folder_id = folder_id self.file_list = file_list @@ -22,16 +25,24 @@ class CreateFolderWidget(QWidget): self.setLayout(layout) def submit(self): - response = RequestClient().client.post( - "/folders", - json={ - "folder_name": self.edit.text(), - "parent_folder_id": str(self.folder_id), - }, - ) - if not response.is_success: - QMessageBox.warning(None, "Error creating folder", response.text) - else: - QMessageBox.information(None, "Folder created", "Folder created") - self.file_list.update_response() - self.close() + try: + response = RequestClient().client.post( + "/folders", + json={ + "folder_name": self.edit.text(), + "parent_folder_id": str(self.folder_id), + }, + ) + if not response.is_success: + QMessageBox.warning( + None, "Error creating folder", response.text + ) + else: + QMessageBox.information( + None, "Folder created", "Folder created" + ) + self.file_list.update_response() + self.close() + except Exception as e: + print(e) + QMessageBox.critical(self, "HTTP error", str(e)) diff --git a/desktop_client/file.py b/desktop_client/file.py index a727d20..38135d1 100644 --- a/desktop_client/file.py +++ b/desktop_client/file.py @@ -6,6 +6,8 @@ import hashlib import os import uuid +import dateutil +import dateutil.tz import file_widgets import pydantic from PyQt6.QtGui import QIcon @@ -32,12 +34,14 @@ class File(pydantic.BaseModel): def details(self, list: file_widgets.FileListWidget) -> QWidget: del list - file_size = self._format_bytes(self.file_size) + file_size = self._format_size(self.file_size) file_size_text = f"{file_size[0]:.2f} {file_size[1]}" + created_at = self._format_date(self.created_at) + updated_at = self._format_date(self.updated_at) details = ( f"file id: {self.file_id}\nfile_name: {self.file_name}\n" + f"file_size: {file_size_text}\n" - + f"created at: {self.created_at}\nupdated at: {self.updated_at}" + + f"created at: {created_at}\nupdated at: {updated_at}" ) label = QLabel() label.setWindowTitle("File info") @@ -45,7 +49,7 @@ class File(pydantic.BaseModel): return label @staticmethod - def _format_bytes(size: int): + def _format_size(size: int): power = 2**10 n = 0 power_labels = {0: "", 1: "kibi", 2: "mebi", 3: "gibi", 4: "tebi"} @@ -54,6 +58,12 @@ class File(pydantic.BaseModel): n += 1 return size, power_labels[n] + "bytes" + @staticmethod + def _format_date(date: datetime.datetime) -> str: + date = date.replace(tzinfo=dateutil.tz.tzutc()) + date = date.astimezone(dateutil.tz.tzlocal()) + return date.strftime("%Y-%m-%d %H:%M:%S") + def icon(self) -> QIcon: return QIcon(resource_path("assets/file.png")) @@ -76,6 +86,7 @@ class File(pydantic.BaseModel): def create(path: str, parent_id: uuid.UUID): """Upload the file""" + print(path) file_name = os.path.basename(path) try: with open(path, "rb") as f: @@ -94,14 +105,21 @@ class File(pydantic.BaseModel): QMessageBox.critical(None, "HTTP Error", str(e)) def download(self, path: str): - with open(path, "wb") as f, RequestClient().client.stream( - "GET", "/files", params={"file_id": self.file_id} - ) as stream: - if not stream.is_success: - QMessageBox.warning(None, "Error downloading the file") - return - for data in stream.iter_bytes(): - f.write(data) + try: + with open(path, "wb") as f, RequestClient().client.stream( + "GET", "/files", params={"file_id": self.file_id} + ) as stream: + if not stream.is_success: + QMessageBox.warning( + None, + "Error downloading the file", + "Error downloading the file", + ) + return + for data in stream.iter_bytes(): + f.write(data) + except Exception as e: + QMessageBox.warning(None, "Error downloading the file", str(e)) def modify(self, path: str): """Upload the file""" diff --git a/desktop_client/file_widgets.py b/desktop_client/file_widgets.py index f9b7df0..44ed5f2 100644 --- a/desktop_client/file_widgets.py +++ b/desktop_client/file_widgets.py @@ -2,6 +2,7 @@ from __future__ import annotations import dataclasses import uuid +import threading from typing import Protocol, Self import create_folder_widget @@ -151,10 +152,14 @@ class FileListWidget(QListWidget): self.upload_file(file_path) def upload_file(self, file_path): - response = self.current_response() - if not isinstance(response, ListResponse): - return - File.create(file_path, response.folder_id) + def _inner(): + response = self.current_response() + if not isinstance(response, ListResponse): + return + File.create(file_path, response.folder_id) + self.update_response() + + threading.Thread(target=_inner).start() def add_item(self, item: DisplayProtocol): widget = QListWidgetItem(item.name()) @@ -249,7 +254,7 @@ class Sidebar(QWidget): self.folder_widget.show() def sync_all(self): - sync.SyncData.sync_all() + threading.Thread(target=sync.SyncData.sync_all).start() class MainFileWidget(QWidget): diff --git a/desktop_client/folder.py b/desktop_client/folder.py index f11941b..959a4ff 100644 --- a/desktop_client/folder.py +++ b/desktop_client/folder.py @@ -33,7 +33,7 @@ class Folder(pydantic.BaseModel): if not response: QMessageBox.warning(None, "Error deleting folder", response.text) - def details(self, list: file_widgets.FileListWidget) -> QWidget: + def details(self, _: file_widgets.FileListWidget) -> QWidget: import folder_info return folder_info.FolderInfoWidget(self) @@ -49,13 +49,18 @@ class Folder(pydantic.BaseModel): @staticmethod def create(name: str, parent_id: uuid.UUID) -> uuid.UUID: - response = RequestClient().client.post( - "/folders", - json={ - "folder_name": name, - "parent_folder_id": str(parent_id), - }, - ) - if not response.is_success: - QMessageBox.warning(None, "Error creating folder", response.text) - return uuid.UUID(response.text.strip('"')) + try: + response = RequestClient().client.post( + "/folders", + json={ + "folder_name": name, + "parent_folder_id": str(parent_id), + }, + ) + if not response.is_success: + QMessageBox.warning( + None, "Error creating folder", response.text + ) + return uuid.UUID(response.text.strip('"')) + except Exception as e: + QMessageBox.warning(None, "Error creating the folder", str(e)) diff --git a/desktop_client/folder_info.py b/desktop_client/folder_info.py index 61db46b..0da2035 100644 --- a/desktop_client/folder_info.py +++ b/desktop_client/folder_info.py @@ -30,34 +30,40 @@ class Permission(enum.StrEnum): manage = enum.auto() def set(self, folder_id: uuid.UUID, user_id: int): - response = RequestClient().client.post( - "/permissions", - json={ - "folder_id": str(folder_id), - "permission_type": str(self), - "user_id": int(user_id), - }, - ) - if not response.is_success: - QMessageBox.warning( - None, "Error setting permissions", response.text + try: + response = RequestClient().client.post( + "/permissions", + json={ + "folder_id": str(folder_id), + "permission_type": str(self), + "user_id": int(user_id), + }, ) + if not response.is_success: + QMessageBox.warning( + None, "Error setting permissions", response.text + ) + except Exception as e: + QMessageBox.warning(None, "Error setting permissions", str(e)) @staticmethod def delete(folder_id: uuid.UUID, user_id: int, widget: FolderInfoWidget): - response = RequestClient().client.delete( - "/permissions", - params={ - "folder_id": folder_id, - "user_id": user_id, - }, - ) - if not response.is_success: - QMessageBox.warning( - None, "Error deleting permissions", response.text + try: + response = RequestClient().client.delete( + "/permissions", + params={ + "folder_id": folder_id, + "user_id": user_id, + }, ) - else: - widget.redraw() + if not response.is_success: + QMessageBox.warning( + None, "Error deleting permissions", response.text + ) + else: + widget.redraw() + except Exception as e: + QMessageBox.warning(None, "Error deleting permissions", str(e)) @dataclass(slots=True) @@ -66,12 +72,18 @@ class Permissions: @staticmethod def get(folder_id: uuid.UUID) -> Permissions: - mapping = pydantic.TypeAdapter(dict[str, Permission]).validate_json( - RequestClient() - .client.get("/permissions", params={"folder_id": folder_id}) - .text - ) - return Permissions(mapping) + try: + mapping = pydantic.TypeAdapter( + dict[str, Permission] + ).validate_json( + RequestClient() + .client.get("/permissions", params={"folder_id": folder_id}) + .text + ) + return Permissions(mapping) + except Exception as e: + QMessageBox.warning(None, "Error getting permissions", str(e)) + return Permissions({}) class FolderInfoWidget(QWidget): diff --git a/desktop_client/state.py b/desktop_client/state.py index 5621bb3..8691f79 100644 --- a/desktop_client/state.py +++ b/desktop_client/state.py @@ -10,7 +10,7 @@ class State(QMainWindow): def __init__(self): super().__init__() - self.setWindowTitle("Auth App") + self.setWindowTitle("Drive application") self.stack = QStackedWidget() self.register_widget = auth.RegisterWidget(self) @@ -39,10 +39,6 @@ class State(QMainWindow): try: keyring.set_password("auth_app", "access_token", token) request_client.RequestClient().set_token(token) - sync.SyncData.sync_all( - # "/home/stnicolay/backups", - # uuid.UUID("0191397f-ae77-7b2a-bed7-9d28ed56a90a"), - ) self.file_widget = file_widgets.MainFileWidget(self) self.stack.addWidget(self.file_widget) self.stack.setCurrentWidget(self.file_widget) @@ -56,4 +52,5 @@ class State(QMainWindow): def logout(self): keyring.delete_password("auth_app", "access_token") request_client.RequestClient().delete_token() + sync.SyncData.delete_all() self.switch_to_login() diff --git a/desktop_client/sync.py b/desktop_client/sync.py index 39fd2fb..595e8f7 100644 --- a/desktop_client/sync.py +++ b/desktop_client/sync.py @@ -3,13 +3,15 @@ from __future__ import annotations import datetime import os import uuid -from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import ProcessPoolExecutor from time import ctime from file import File from folder import Folder from request_client import RequestClient from sqlmodel import Field, Session, SQLModel, create_engine, delete, select +import multiprocessing +from PyQt6.QtWidgets import QMessageBox class FolderStructure(Folder): @@ -41,20 +43,29 @@ class SyncData(SQLModel, table=True): path: str last_updated: datetime.datetime + def sync_one(self): + try: + merge_folder( + self.path, + FolderStructure.get_structure(self.folder_id), + self.last_updated, + ) + except Exception as e: + SyncData.delete(self.folder_id) + QMessageBox.warning( + None, + "Error syncing folder", + f"Error syncing {self.path!r} folder:\n{e}", + ) + @staticmethod def sync_all(): + with Session(engine) as s: syncs = s.exec(select(SyncData)).fetchall() - with ThreadPoolExecutor(3) as pool: - map_ = pool.map( - lambda sync: merge_folder( - sync.path, - FolderStructure.get_structure(sync.folder_id), - sync.last_updated, - ), - syncs, - ) - tuple(map_) + + with ProcessPoolExecutor(3) as pool: + tuple(pool.map(SyncData.sync_one, syncs)) @staticmethod def new_sync(path: str, folder_id: uuid.UUID): @@ -66,7 +77,9 @@ class SyncData(SQLModel, table=True): ) s.add(sync) s.commit() - upload_folder(path, folder_id) + multiprocessing.Process( + target=lambda: upload_folder(path, folder_id) + ).start() @staticmethod def get_for_folder(folder_id: uuid.UUID) -> SyncData | None: @@ -81,6 +94,12 @@ class SyncData(SQLModel, table=True): s.exec(delete(SyncData).where(SyncData.folder_id == folder_id)) s.commit() + @staticmethod + def delete_all(): + with Session(engine) as s: + s.exec(delete(SyncData)) + s.commit() + sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" diff --git a/desktop_client/user.py b/desktop_client/user.py index 1f2ed0e..317fe7e 100644 --- a/desktop_client/user.py +++ b/desktop_client/user.py @@ -29,7 +29,9 @@ class User(pydantic.BaseModel): url = "/users/current" response = RequestClient().client.get(url, params=params) if not response.is_success: - QMessageBox.warning(None, "Error getting permissions", response.text) + QMessageBox.warning( + None, "Error getting permissions", response.text + ) return return User.model_validate_json(response.text) diff --git a/poetry.lock b/poetry.lock index b9e6926..4259cdc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -427,18 +427,18 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-ena [[package]] name = "jaraco-context" -version = "5.3.0" +version = "6.0.1" description = "Useful decorators and context managers" optional = false python-versions = ">=3.8" files = [ - {file = "jaraco.context-5.3.0-py3-none-any.whl", hash = "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266"}, - {file = "jaraco.context-5.3.0.tar.gz", hash = "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2"}, + {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, + {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [[package]] name = "jaraco-functools" @@ -788,6 +788,20 @@ files = [ {file = "PyQt6_sip-13.8.0.tar.gz", hash = "sha256:2f74cf3d6d9cab5152bd9f49d570b2dfb87553ebb5c4919abfde27f5b9fd69d4"}, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -804,13 +818,13 @@ cli = ["click (>=5.0)"] [[package]] name = "pywin32-ctypes" -version = "0.2.2" +version = "0.2.3" description = "A (partial) reimplementation of pywin32 using ctypes/cffi" optional = false python-versions = ">=3.6" files = [ - {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, - {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, ] [[package]] @@ -828,6 +842,17 @@ files = [ cryptography = ">=2.0" jeepney = ">=0.6" +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -955,4 +980,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "6e84cef360a53faa7bba2d9bb57f01093ff0291c28c02aff4727b6c21fd6d715" +content-hash = "fd70e86969d9632a562a6755e8ff2f7e9d38e795da2f3c5c04eabe77d5723fdb" diff --git a/pyproject.toml b/pyproject.toml index af55285..4b3db16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ pyqt6 = "^6.7.1" keyring = "^25.3.0" python-dotenv = "^1.0.1" sqlmodel = "^0.0.21" +python-dateutil = "^2.9.0.post0" [tool.poetry.group.dev.dependencies]