File sync
This commit is contained in:
parent
4c01ca7510
commit
c101aa8aa6
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||||
|
@ -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))
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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
117
desktop_client/sync.py
Normal 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)
|
@ -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)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user