Compare commits

...

7 Commits

Author SHA1 Message Date
5f0130d0d1 God, I hope I'm ready 2024-08-21 05:24:07 +03:00
04a27b592e Pyinstaller support for assets 2024-08-21 04:05:36 +03:00
d6ecae08bd Switched to other domain 2024-08-16 18:40:42 +03:00
aa786de5b4 Cleanup 2024-08-11 14:39:00 +03:00
409f13b584 Sync widget 2024-08-11 10:24:55 +03:00
c101aa8aa6 File sync 2024-08-11 08:19:16 +03:00
4c01ca7510 Folder info and permissions 2024-08-10 08:59:11 +03:00
15 changed files with 704 additions and 174 deletions

2
.gitignore vendored
View File

@ -159,3 +159,5 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
database.db

View File

@ -8,11 +8,7 @@ from state import State
if __name__ == "__main__":
dotenv.load_dotenv()
url = os.environ.get("DRIVE_HOST_URL")
if not url:
url = "localhost:3000"
else:
url = url.strip()
app = QApplication(sys.argv)
window = State(url)
window = State()
window.show()
sys.exit(app.exec())

View File

@ -58,7 +58,7 @@ class RegisterWidget(QWidget):
try:
response = RequestClient().client.post(
"http://localhost:3000/users/register",
"/users/register",
data={
"username": username,
"email": email,
@ -79,11 +79,12 @@ class RegisterWidget(QWidget):
self, "Error", f"Registration failed: {response.text}"
)
except httpx.HTTPError as e:
print(e)
QMessageBox.critical(self, "HTTP Error", str(e))
class LoginWidget(QWidget):
def __init__(self, switcher: State):
def __init__(self, switcher: state.State):
super().__init__()
self.switcher = switcher
@ -116,12 +117,14 @@ class LoginWidget(QWidget):
password = self.password_input.text()
if not username or not password:
QMessageBox.warning(self, "Input Error", "Email and Password are required")
QMessageBox.warning(
self, "Input Error", "Email and Password are required"
)
return
try:
response = RequestClient().client.post(
"http://localhost:3000/users/authorize",
"/users/authorize",
data={"username": username, "password": password},
)
if response.is_success:
@ -129,8 +132,12 @@ class LoginWidget(QWidget):
if access_token:
self.switcher.login(access_token)
else:
QMessageBox.warning(self, "Error", "No access token received")
QMessageBox.warning(
self, "Error", "No access token received"
)
else:
QMessageBox.warning(self, "Error", f"Login failed: {response.text}")
QMessageBox.warning(
self, "Error", f"Login failed: {response.text}"
)
except httpx.HTTPError as e:
QMessageBox.critical(self, "HTTP Error", str(e))

View File

@ -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))

143
desktop_client/file.py Normal file
View File

@ -0,0 +1,143 @@
from __future__ import annotations
import base64
import datetime
import hashlib
import os
import uuid
import dateutil
import dateutil.tz
import file_widgets
import pydantic
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QFileDialog, QLabel, QMessageBox, QWidget
from request_client import RequestClient
from utils import resource_path
class File(pydantic.BaseModel):
file_id: uuid.UUID
file_name: str
file_size: int
sha512: str
created_at: datetime.datetime
updated_at: datetime.datetime
def name(self) -> str:
return self.file_name
def delete(self) -> None:
RequestClient().client.delete(
"/files", params={"file_id": self.file_id}
)
def details(self, list: file_widgets.FileListWidget) -> QWidget:
del list
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: {created_at}\nupdated at: {updated_at}"
)
label = QLabel()
label.setWindowTitle("File info")
label.setText(details)
return label
@staticmethod
def _format_size(size: int):
power = 2**10
n = 0
power_labels = {0: "", 1: "kibi", 2: "mebi", 3: "gibi", 4: "tebi"}
while size > power and n < 4:
size /= power
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"))
def double_click(self, list: file_widgets.FileListWidget) -> None:
location = QFileDialog.getExistingDirectory(
list, caption="Select save location"
)
if not location:
return
with open(
os.path.join(location, self.file_name), "wb"
) as f, RequestClient().client.stream(
"GET", "/files", params={"file_id": self.file_id}
) as stream:
if not stream.is_success:
QMessageBox.warning(list, "Error downloading the file")
return
for data in stream.iter_bytes():
f.write(data)
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:
files = {"file": (file_name, f)}
response = RequestClient().client.post(
"/files",
files=files,
params={"parent_folder": parent_id},
)
if not response.is_success:
QMessageBox.warning(
None, "Error", f"Upload failed: {response.text}"
)
print(response.text)
except Exception as e:
QMessageBox.critical(None, "HTTP Error", str(e))
def download(self, path: str):
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"""
file_name = os.path.basename(path)
hash = hashlib.file_digest(open(path, "rb"), "sha512").digest()
if base64.b64decode(self.sha512) == hash:
return
try:
with open(path, "rb") as f:
files = {"file": (file_name, f)}
response = RequestClient().client.patch(
"/files",
files=files,
params={"file_id": self.file_id},
)
if not response.is_success:
QMessageBox.warning(
None, "Error", f"Upload failed: {response.text}"
)
except Exception as e:
QMessageBox.critical(None, "HTTP Error", str(e))

View File

@ -1,26 +1,30 @@
from __future__ import annotations
import dataclasses
import datetime
import os
import uuid
import threading
from typing import Protocol, Self
import create_folder_widget
import httpx
import pydantic
import state
import sync
import user
from file import File
from folder import Folder
from PyQt6.QtCore import QPoint, Qt
from PyQt6.QtGui import QAction, QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon
from PyQt6.QtGui import (
QAction,
QDragEnterEvent,
QDragMoveEvent,
QDropEvent,
QIcon,
)
from PyQt6.QtWidgets import (
QFileDialog,
QHBoxLayout,
QLabel,
QListWidget,
QListWidgetItem,
QMenu,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
@ -45,93 +49,6 @@ class DisplayProtocol(Protocol):
raise NotImplementedError
class File(pydantic.BaseModel):
file_id: uuid.UUID
file_name: str
file_size: int
sha512: str
created_at: datetime.datetime
updated_at: datetime.datetime
def name(self) -> str:
return self.file_name
def delete(self) -> None:
RequestClient().client.delete("/files", params={"file_id": self.file_id})
def details(self, list: FileListWidget) -> QWidget:
del list
file_size = self._format_bytes(self.file_size)
file_size_text = f"{file_size[0]:.2f} {file_size[1]}"
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}"
)
label = QLabel()
label.setWindowTitle("File info")
label.setText(details)
return label
@staticmethod
def _format_bytes(size: int):
power = 2**10
n = 0
power_labels = {0: "", 1: "kibi", 2: "mebi", 3: "gibi", 4: "tebi"}
while size > power and n < 4:
size /= power
n += 1
return size, power_labels[n] + "bytes"
def icon(self) -> QIcon:
return QIcon("assets/file.png")
def double_click(self, list: FileListWidget) -> None:
location = QFileDialog.getExistingDirectory(
list, caption="Select save location"
)
if not location:
return
with open(
os.path.join(location, self.file_name), "wb"
) as f, RequestClient().client.stream(
"GET", "/files", params={"file_id": self.file_id}
) as stream:
if not stream.is_success:
QMessageBox.warning(list, "Error downloading the file")
return
for data in stream.iter_bytes():
f.write(data)
class Folder(pydantic.BaseModel):
folder_id: uuid.UUID
owner_id: int
folder_name: str
created_at: datetime.datetime
def name(self) -> str:
return self.folder_name
def delete(self) -> None:
print(
RequestClient()
.client.delete("/folders", params={"folder_id": self.folder_id})
.text
)
def details(self, list: FileListWidget) -> QWidget:
# TODO
raise NotImplementedError
def icon(self) -> QIcon:
return QIcon("assets/folder.png")
def double_click(self, list: FileListWidget) -> None:
list.responses.append(ListResponse.get(self.folder_id))
list.update()
class ResponseProtocol(Protocol):
def items(self) -> list[DisplayProtocol]:
raise NotImplementedError
@ -164,7 +81,9 @@ class ListResponse(pydantic.BaseModel):
return self.get(self.folder_id)
def create_folder(self, file_list: FileListWidget):
return create_folder_widget.CreateFolderWidget(self.folder_id, file_list)
return create_folder_widget.CreateFolderWidget(
self.folder_id, file_list
)
@dataclasses.dataclass(slots=True)
@ -186,7 +105,7 @@ class TlpResponse:
def update(self) -> ResponseProtocol:
return self.get()
def create_folder(self):
def create_folder(self, _: FileListWidget):
return # Not much to do
@ -208,7 +127,7 @@ class FileListWidget(QListWidget):
item = self.current_response().items()[row]
item.double_click(self)
def current_response(self) -> ListResponse:
def current_response(self) -> ResponseProtocol:
if not self.responses:
self.update_response()
return self.responses[-1]
@ -228,32 +147,19 @@ class FileListWidget(QListWidget):
def dropEvent(self, event: QDropEvent):
event.accept()
print("hi")
for url in event.mimeData().urls():
file_path = url.toLocalFile()
self.upload_file(file_path)
def upload_file(self, file_path):
file_name = os.path.basename(file_path)
try:
with open(file_path, "rb") as f:
files = {"file": (file_name, f)}
response = RequestClient().client.post(
"http://localhost:3000/files",
files=files,
params={"parent_folder": self.current_response().folder_id},
)
if response.is_success:
QMessageBox.information(
self, "Success", "File uploaded successfully"
)
self.update_response()
else:
QMessageBox.warning(
self, "Error", f"Upload failed: {response.text}"
)
except httpx.HTTPError as e:
QMessageBox.critical(self, "HTTP Error", str(e))
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())
@ -283,7 +189,6 @@ class FileListWidget(QListWidget):
item = self.current_response().items()[row]
item.delete()
self.update_response()
QMessageBox.information(self, "Delete", f"{item.name()} deleted")
def update(self) -> None:
self.clear()
@ -307,8 +212,9 @@ class Sidebar(QWidget):
("Go back", self.go_back),
("Go root", self.go_root),
("Get permitted", self.get_tlp),
("Sync", self.sync),
("Refresh", self.refresh),
("Create folder", self.create_folder),
("Sync all", self.sync_all),
]
for text, func in buttons:
button = QPushButton(text)
@ -337,9 +243,8 @@ class Sidebar(QWidget):
self.file_list.responses.append(TlpResponse.get())
self.file_list.update()
def sync(self):
# TODO
...
def refresh(self):
self.file_list.update_response()
def create_folder(self):
self.folder_widget = self.file_list.current_response().create_folder(
@ -348,6 +253,9 @@ class Sidebar(QWidget):
if self.folder_widget is not None:
self.folder_widget.show()
def sync_all(self):
threading.Thread(target=sync.SyncData.sync_all).start()
class MainFileWidget(QWidget):
def __init__(self, state: state.State):

66
desktop_client/folder.py Normal file
View File

@ -0,0 +1,66 @@
from __future__ import annotations
import datetime
import typing
import uuid
if typing.TYPE_CHECKING:
import file_widgets
import pydantic
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QMessageBox, QWidget
from request_client import RequestClient
from utils import resource_path
class Folder(pydantic.BaseModel):
folder_id: uuid.UUID
owner_id: int
folder_name: str
created_at: datetime.datetime
def name(self) -> str:
return self.folder_name
def delete(self) -> None:
import sync
sync.SyncData.delete(self.folder_id)
response = RequestClient().client.delete(
"/folders", params={"folder_id": self.folder_id}
)
if not response:
QMessageBox.warning(None, "Error deleting folder", response.text)
def details(self, _: file_widgets.FileListWidget) -> QWidget:
import folder_info
return folder_info.FolderInfoWidget(self)
def icon(self) -> QIcon:
return QIcon(resource_path("assets/folder.png"))
def double_click(self, list: file_widgets.FileListWidget) -> None:
import file_widgets
list.responses.append(file_widgets.ListResponse.get(self.folder_id))
list.update()
@staticmethod
def create(name: str, parent_id: uuid.UUID) -> uuid.UUID:
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))

View File

@ -0,0 +1,200 @@
from __future__ import annotations
import enum
import uuid
from dataclasses import dataclass
from functools import cache, partial
import file_widgets
import pydantic
import sync
import user
from PyQt6.QtWidgets import (
QComboBox,
QFileDialog,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QSpacerItem,
QVBoxLayout,
QWidget,
)
from request_client import RequestClient
class Permission(enum.StrEnum):
read = enum.auto()
write = enum.auto()
manage = enum.auto()
def set(self, folder_id: uuid.UUID, user_id: int):
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):
try:
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
)
else:
widget.redraw()
except Exception as e:
QMessageBox.warning(None, "Error deleting permissions", str(e))
@dataclass(slots=True)
class Permissions:
mapping: dict[int, Permission]
@staticmethod
def get(folder_id: uuid.UUID) -> Permissions:
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):
def __init__(self, folder: file_widgets.Folder):
super().__init__()
self.setWindowTitle("Folder info")
self.folder = folder
self.user_cache = cache(user.User.get)
self.user_mapping: dict[str, int] = {}
self.redraw()
def redraw(self):
self.permissions = Permissions.get(self.folder.folder_id)
main_layout = QVBoxLayout()
owner_name = self.user_cache(self.folder.owner_id)
main_layout.addWidget(
QLabel(
f"Owner: {owner_name}\n"
+ f"Folder name: {self.folder.folder_name}\n"
+ f"Created at: {self.folder.created_at}"
)
)
for user_id, permissions in self.permissions.mapping.items():
layout = QHBoxLayout()
name = QLabel(self.user_cache(user_id).username)
combo = QComboBox()
combo.addItems(map(str, Permission))
combo.setCurrentText(str(permissions))
combo.currentTextChanged.connect(
partial(self.change, user_id=user_id)
)
delete = QPushButton("Delete")
delete.clicked.connect(
partial(
Permission.delete, self.folder.folder_id, user_id, self
)
)
layout.addWidget(name)
layout.addWidget(combo)
layout.addWidget(delete)
main_layout.addLayout(layout)
layout = QHBoxLayout()
self.search_str = QLineEdit()
layout.addWidget(self.search_str)
search = QPushButton("Search")
search.clicked.connect(self.search)
layout.addWidget(search)
main_layout.addLayout(layout)
layout = QHBoxLayout()
self.search_combo = QComboBox()
self.perm_combo = QComboBox()
self.perm_combo.addItems(map(str, Permission))
button = QPushButton("+")
button.clicked.connect(self.search_save)
layout.addWidget(self.search_combo)
layout.addWidget(self.perm_combo)
layout.addWidget(button)
main_layout.addLayout(layout)
main_layout.addSpacerItem(QSpacerItem(0, 50))
synced_to = sync.SyncData.get_for_folder(self.folder.folder_id)
if synced_to is not None:
synced_to_text = f"Synched to {synced_to.path}"
else:
synced_to_text = "Not synced"
layout = QHBoxLayout()
label = QLabel(synced_to_text)
layout.addWidget(label)
if synced_to is not None:
button = QPushButton("Delete")
button.clicked.connect(self.remove_sync)
else:
button = QPushButton("Add sync")
button.clicked.connect(self.add_sync)
layout.addWidget(button)
main_layout.addLayout(layout)
if self.layout():
QWidget().setLayout(self.layout())
self.setLayout(main_layout)
def change(self, text: str, *, user_id: int):
Permission(text).set(self.folder.folder_id, user_id)
def search(self):
result = user.UserSearch.search(self.search_str.text())
self.search_combo.clear()
self.user_mapping.clear()
for item in result:
self.user_mapping[item.username] = item.user_id
self.search_combo.addItem(item.username)
def search_save(self):
permission = Permission(self.perm_combo.currentText())
search = self.search_combo.currentText()
if not search:
return
user_id = self.user_mapping[self.search_combo.currentText()]
permission.set(self.folder.folder_id, user_id)
self.redraw()
def add_sync(self):
path = QFileDialog.getExistingDirectory()
if path:
sync.SyncData.new_sync(path, self.folder.folder_id)
self.redraw()
def remove_sync(self):
sync.SyncData.delete(self.folder.folder_id)
self.redraw()

View File

@ -9,9 +9,9 @@ class RequestClient:
def __new__(cls) -> Self:
if cls._client is None:
url = os.environ.get("DRIVE_HOST_URL").strip()
if not url:
url = "localhost:3000"
url = os.environ.get("DRIVE_HOST_URL")
if url is None:
url = "https://drive.stnicolay.ru"
cls._client = httpx.Client(base_url=url)
return super().__new__(cls)

View File

@ -1,19 +1,18 @@
import auth
import file_widgets
import httpx
import keyring
import request_client
from PyQt6.QtWidgets import QMainWindow, QStackedWidget
import sync
from PyQt6.QtWidgets import QMainWindow, QMessageBox, QStackedWidget
class State(QMainWindow):
def __init__(self, url: str):
def __init__(self):
super().__init__()
self.setWindowTitle("Auth App")
self.setWindowTitle("Drive application")
self.stack = QStackedWidget()
self.client = httpx.Client(base_url=url)
self.register_widget = auth.RegisterWidget(self)
self.login_widget = auth.LoginWidget(self)
@ -37,13 +36,21 @@ class State(QMainWindow):
self.stack.setCurrentWidget(self.register_widget)
def login(self, token: str):
keyring.set_password("auth_app", "access_token", token)
request_client.RequestClient().set_token(token)
self.file_widget = file_widgets.MainFileWidget(self)
self.stack.addWidget(self.file_widget)
self.stack.setCurrentWidget(self.file_widget)
try:
keyring.set_password("auth_app", "access_token", token)
request_client.RequestClient().set_token(token)
self.file_widget = file_widgets.MainFileWidget(self)
self.stack.addWidget(self.file_widget)
self.stack.setCurrentWidget(self.file_widget)
except Exception as e:
print(e)
keyring.delete_password("auth_app", "access_token")
QMessageBox.information(
None, "Error logging in", "Error logging in"
)
def logout(self):
keyring.delete_password("auth_app", "access_token")
request_client.RequestClient().delete_token()
sync.SyncData.delete_all()
self.switch_to_login()

148
desktop_client/sync.py Normal file
View File

@ -0,0 +1,148 @@
from __future__ import annotations
import datetime
import os
import uuid
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):
files: list[File]
folders: list[FolderStructure]
def find_folder(self, name: str) -> File | None:
for file in self.folders:
if file.folder_name == name:
return file
def find_file(self, name: str) -> File | None:
for file in self.files:
if file.file_name == name:
return file
@staticmethod
def get_structure(folder_id: uuid.UUID) -> FolderStructure:
return FolderStructure.model_validate_json(
RequestClient()
.client.get("/folders/structure", params={"folder_id": folder_id})
.text
)
class SyncData(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
folder_id: uuid.UUID = Field(unique=True, index=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 ProcessPoolExecutor(3) as pool:
tuple(pool.map(SyncData.sync_one, syncs))
@staticmethod
def new_sync(path: str, folder_id: uuid.UUID):
with Session(engine) as s:
sync = SyncData(
path=path,
last_updated=datetime.datetime.now(),
folder_id=folder_id,
)
s.add(sync)
s.commit()
multiprocessing.Process(
target=lambda: upload_folder(path, folder_id)
).start()
@staticmethod
def get_for_folder(folder_id: uuid.UUID) -> SyncData | None:
with Session(engine) as s:
return s.exec(
select(SyncData).where(SyncData.folder_id == folder_id)
).one_or_none()
@staticmethod
def delete(folder_id: uuid.UUID):
with Session(engine) as s:
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}"
engine = create_engine(sqlite_url, echo=True)
SQLModel.metadata.create_all(engine)
def upload_folder(path: str, folder_id: uuid.UUID):
items = os.listdir(path)
for item in items:
item_path = os.path.join(path, item)
if os.path.isfile(item_path):
File.create(item_path, folder_id)
elif os.path.isdir(item_path):
id = Folder.create(item, folder_id)
upload_folder(item_path, id)
def merge_folder(
path: str,
structure: FolderStructure,
time: datetime.datetime,
):
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isfile(item_path):
file = structure.find_file(item)
if file is None:
File.create(item_path, structure.folder_id)
continue
mtime = datetime.datetime.strptime(
ctime(os.path.getmtime(item_path)), "%a %b %d %H:%M:%S %Y"
)
print(item, mtime, time)
if mtime > time:
file.modify(item_path)
else:
file.download(item_path)
elif os.path.isdir(item_path):
folder = structure.find_folder(item)
if folder is None:
folder_id = Folder.create(item, structure.folder_id)
upload_folder(item_path, folder_id)
else:
merge_folder(item_path, folder, time)

View File

@ -27,9 +27,13 @@ class User(pydantic.BaseModel):
params["user_id"] = user_id
else:
url = "/users/current"
return User.model_validate_json(
RequestClient().client.get(url, params=params).text
)
response = RequestClient().client.get(url, params=params)
if not response.is_success:
QMessageBox.warning(
None, "Error getting permissions", response.text
)
return
return User.model_validate_json(response.text)
@staticmethod
def delete():

12
desktop_client/utils.py Normal file
View File

@ -0,0 +1,12 @@
import os
import sys
from functools import cache
_DEFAULT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@cache
def resource_path(relative_path: str) -> str:
"""Get absolute path to resource, works for dev and for PyInstaller"""
base_path = getattr(sys, "_MEIPASS", _DEFAULT)
return os.path.join(base_path, relative_path)

43
poetry.lock generated
View File

@ -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"

View File

@ -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]