File sync

This commit is contained in:
StNicolay 2024-08-11 08:16:30 +03:00
parent 4c01ca7510
commit c101aa8aa6
Signed by: StNicolay
GPG Key ID: 9693D04DCD962B0D
7 changed files with 214 additions and 50 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 # 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. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
database.db

View File

@ -116,9 +116,7 @@ class LoginWidget(QWidget):
password = self.password_input.text() password = self.password_input.text()
if not username or not password: if not username or not password:
QMessageBox.warning( QMessageBox.warning(self, "Input Error", "Email and Password are required")
self, "Input Error", "Email and Password are required"
)
return return
try: try:
@ -131,12 +129,8 @@ class LoginWidget(QWidget):
if access_token: if access_token:
self.switcher.login(access_token) self.switcher.login(access_token)
else: else:
QMessageBox.warning( QMessageBox.warning(self, "Error", "No access token received")
self, "Error", "No access token received"
)
else: else:
QMessageBox.warning( QMessageBox.warning(self, "Error", f"Login failed: {response.text}")
self, "Error", f"Login failed: {response.text}"
)
except httpx.HTTPError as e: except httpx.HTTPError as e:
QMessageBox.critical(self, "HTTP Error", str(e)) QMessageBox.critical(self, "HTTP Error", str(e))

View File

@ -1,7 +1,9 @@
from __future__ import annotations from __future__ import annotations
import base64
import dataclasses import dataclasses
import datetime import datetime
import hashlib
import os import os
import uuid import uuid
from typing import Protocol, Self from typing import Protocol, Self
@ -13,13 +15,7 @@ import pydantic
import state import state
import user import user
from PyQt6.QtCore import QPoint, Qt from PyQt6.QtCore import QPoint, Qt
from PyQt6.QtGui import ( from PyQt6.QtGui import QAction, QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon
QAction,
QDragEnterEvent,
QDragMoveEvent,
QDropEvent,
QIcon,
)
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QFileDialog, QFileDialog,
QHBoxLayout, QHBoxLayout,
@ -64,9 +60,7 @@ class File(pydantic.BaseModel):
return self.file_name return self.file_name
def delete(self) -> None: def delete(self) -> None:
RequestClient().client.delete( RequestClient().client.delete("/files", params={"file_id": self.file_id})
"/files", params={"file_id": self.file_id}
)
def details(self, list: FileListWidget) -> QWidget: def details(self, list: FileListWidget) -> QWidget:
del list del list
@ -112,6 +106,56 @@ class File(pydantic.BaseModel):
for data in stream.iter_bytes(): for data in stream.iter_bytes():
f.write(data) f.write(data)
def create(path: str, parent_id: uuid.UUID):
"""Upload the file"""
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 httpx.HTTPError as e:
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)
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 httpx.HTTPError as e:
QMessageBox.critical(None, "HTTP Error", str(e))
class Folder(pydantic.BaseModel): class Folder(pydantic.BaseModel):
folder_id: uuid.UUID folder_id: uuid.UUID
@ -139,6 +183,19 @@ class Folder(pydantic.BaseModel):
list.responses.append(ListResponse.get(self.folder_id)) list.responses.append(ListResponse.get(self.folder_id))
list.update() list.update()
@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('"'))
class ResponseProtocol(Protocol): class ResponseProtocol(Protocol):
def items(self) -> list[DisplayProtocol]: def items(self) -> list[DisplayProtocol]:
@ -172,9 +229,7 @@ class ListResponse(pydantic.BaseModel):
return self.get(self.folder_id) return self.get(self.folder_id)
def create_folder(self, file_list: FileListWidget): def create_folder(self, file_list: FileListWidget):
return create_folder_widget.CreateFolderWidget( return create_folder_widget.CreateFolderWidget(self.folder_id, file_list)
self.folder_id, file_list
)
@dataclasses.dataclass(slots=True) @dataclasses.dataclass(slots=True)
@ -250,9 +305,7 @@ class FileListWidget(QListWidget):
response = RequestClient().client.post( response = RequestClient().client.post(
"http://localhost:3000/files", "http://localhost:3000/files",
files=files, files=files,
params={ params={"parent_folder": self.current_response().folder_id},
"parent_folder": self.current_response().folder_id
},
) )
if response.is_success: if response.is_success:
QMessageBox.information( QMessageBox.information(

View File

@ -1,23 +1,24 @@
from __future__ import annotations from __future__ import annotations
import enum
import uuid import uuid
from dataclasses import dataclass from dataclasses import dataclass
from request_client import RequestClient from functools import cache, partial
import enum
import file_widgets import file_widgets
import pydantic
import user
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget,
QMessageBox,
QLabel,
QPushButton,
QComboBox, QComboBox,
QHBoxLayout, QHBoxLayout,
QVBoxLayout, QLabel,
QLineEdit, QLineEdit,
QMessageBox,
QPushButton,
QVBoxLayout,
QWidget,
) )
import pydantic from request_client import RequestClient
from functools import cache, partial
import user
class Permission(enum.StrEnum): class Permission(enum.StrEnum):
@ -35,9 +36,7 @@ class Permission(enum.StrEnum):
}, },
) )
if not response.is_success: if not response.is_success:
QMessageBox.warning( QMessageBox.warning(None, "Error setting permissions", response.text)
None, "Error setting permissions", response.text
)
@staticmethod @staticmethod
def delete(folder_id: uuid.UUID, user_id: int, widget: FolderInfoWidget): def delete(folder_id: uuid.UUID, user_id: int, widget: FolderInfoWidget):
@ -49,9 +48,7 @@ class Permission(enum.StrEnum):
}, },
) )
if not response.is_success: if not response.is_success:
QMessageBox.warning( QMessageBox.warning(None, "Error deleting permissions", response.text)
None, "Error deleting permissions", response.text
)
else: else:
widget.redraw() widget.redraw()
@ -97,14 +94,10 @@ class FolderInfoWidget(QWidget):
combo = QComboBox() combo = QComboBox()
combo.addItems(map(str, Permission)) combo.addItems(map(str, Permission))
combo.setCurrentText(str(permissions)) combo.setCurrentText(str(permissions))
combo.currentTextChanged.connect( combo.currentTextChanged.connect(partial(self.change, user_id=user_id))
partial(self.change, user_id=user_id)
)
delete = QPushButton("Delete") delete = QPushButton("Delete")
delete.clicked.connect( delete.clicked.connect(
partial( partial(Permission.delete, self.folder.folder_id, user_id, self)
Permission.delete, self.folder.folder_id, user_id, self
)
) )
layout.addWidget(name) layout.addWidget(name)
layout.addWidget(combo) layout.addWidget(combo)

View File

@ -1,8 +1,11 @@
import uuid
import auth import auth
import file_widgets import file_widgets
import httpx import httpx
import keyring import keyring
import request_client import request_client
import sync
from PyQt6.QtWidgets import QMainWindow, QStackedWidget from PyQt6.QtWidgets import QMainWindow, QStackedWidget
@ -39,6 +42,10 @@ class State(QMainWindow):
def login(self, token: str): def login(self, token: str):
keyring.set_password("auth_app", "access_token", token) keyring.set_password("auth_app", "access_token", token)
request_client.RequestClient().set_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.file_widget = file_widgets.MainFileWidget(self)
self.stack.addWidget(self.file_widget) self.stack.addWidget(self.file_widget)
self.stack.setCurrentWidget(self.file_widget) self.stack.setCurrentWidget(self.file_widget)

117
desktop_client/sync.py Normal file
View File

@ -0,0 +1,117 @@
from __future__ import annotations
import datetime
import os
import uuid
from concurrent.futures import ThreadPoolExecutor
from time import ctime
import file_widgets
from request_client import RequestClient
from sqlmodel import Field, Session, SQLModel, create_engine, select
class FolderStructure(file_widgets.Folder):
files: list[file_widgets.File]
folders: list[FolderStructure]
def find_folder(self, name: str) -> file_widgets.File | None:
for file in self.folders:
if file.folder_name == name:
return file
def find_file(self, name: str) -> file_widgets.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)
path: str
last_updated: datetime.datetime
folder_id: uuid.UUID
@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_)
@staticmethod
def new_sync(path: str, parent_id):
name = os.path.basename(path)
folder_id = file_widgets.Folder.create(name, parent_id)
with Session(engine) as s:
sync = SyncData(
path=path,
last_updated=datetime.datetime.now(),
folder_id=folder_id,
)
s.add(sync)
s.commit()
upload_folder(path, folder_id)
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_widgets.File.create(item_path, folder_id)
elif os.path.isdir(item_path):
id = file_widgets.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_widgets.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 = file_widgets.Folder.create(item, structure.folder_id)
upload_folder(item_path, folder_id)
else:
merge_folder(item_path, folder, time)

View File

@ -29,9 +29,7 @@ class User(pydantic.BaseModel):
url = "/users/current" url = "/users/current"
response = RequestClient().client.get(url, params=params) response = RequestClient().client.get(url, params=params)
if not response.is_success: if not response.is_success:
QMessageBox.warning( QMessageBox.warning(None, "Error getting permissions", response.text)
None, "Error getting permissions", response.text
)
return return
return User.model_validate_json(response.text) return User.model_validate_json(response.text)