File sync

This commit is contained in:
StNicolay 2024-08-11 08:16:30 +03:00
parent 4c01ca7510
commit d6777fc862
Signed by: StNicolay
GPG Key ID: 9693D04DCD962B0D
7 changed files with 209 additions and 34 deletions

BIN
database.db Normal file

Binary file not shown.

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

@ -5,6 +5,8 @@ import datetime
import os import os
import uuid import uuid
from typing import Protocol, Self from typing import Protocol, Self
import base64
import hashlib
import create_folder_widget import create_folder_widget
import folder_info import folder_info
@ -112,6 +114,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 +191,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]:

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

@ -4,6 +4,8 @@ import httpx
import keyring import keyring
import request_client import request_client
from PyQt6.QtWidgets import QMainWindow, QStackedWidget from PyQt6.QtWidgets import QMainWindow, QStackedWidget
import uuid
import sync
class State(QMainWindow): class State(QMainWindow):
@ -39,6 +41,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)

119
desktop_client/sync.py Normal file
View File

@ -0,0 +1,119 @@
from __future__ import annotations
import os
import uuid
from concurrent.futures import ThreadPoolExecutor
import datetime
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)