109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
|
|
from io import BytesIO
|
||
|
|
from datetime import UTC, datetime
|
||
|
|
|
||
|
|
from sqlalchemy import and_, select
|
||
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
|
from starlette.datastructures import UploadFile
|
||
|
|
|
||
|
|
from app.models.folder import Folder
|
||
|
|
from app.models.remote_mount import RemoteMount, RemoteSyncItem
|
||
|
|
from app.services.document_service import DocumentService
|
||
|
|
from app.services.webdav_service import WebDavNode, WebDavService
|
||
|
|
|
||
|
|
|
||
|
|
class RemoteSyncService:
|
||
|
|
def __init__(self, db: AsyncSession, user_id: str):
|
||
|
|
self.db = db
|
||
|
|
self.user_id = user_id
|
||
|
|
|
||
|
|
async def sync_remote_path(
|
||
|
|
self,
|
||
|
|
mount: RemoteMount,
|
||
|
|
remote_path: str,
|
||
|
|
local_folder_id: str,
|
||
|
|
mode: str = "file",
|
||
|
|
) -> dict:
|
||
|
|
folder = await self.db.execute(
|
||
|
|
select(Folder).where(and_(Folder.id == local_folder_id, Folder.user_id == self.user_id))
|
||
|
|
)
|
||
|
|
if folder.scalar_one_or_none() is None:
|
||
|
|
raise ValueError("本地目标文件夹不存在")
|
||
|
|
|
||
|
|
webdav = WebDavService(mount)
|
||
|
|
document_service = DocumentService(self.db, self.user_id)
|
||
|
|
|
||
|
|
synced = 0
|
||
|
|
skipped = 0
|
||
|
|
failed = 0
|
||
|
|
document_ids: list[str] = []
|
||
|
|
errors: list[str] = []
|
||
|
|
|
||
|
|
if mode == "folder":
|
||
|
|
nodes = await webdav.list_tree(remote_path)
|
||
|
|
targets = self._flatten_files(nodes)
|
||
|
|
else:
|
||
|
|
name = remote_path.rstrip("/").split("/")[-1] or "remote-file"
|
||
|
|
targets = [WebDavNode(path=remote_path, name=name, is_dir=False)]
|
||
|
|
|
||
|
|
for node in targets:
|
||
|
|
try:
|
||
|
|
content, filename = await webdav.download_file(node.path)
|
||
|
|
upload = UploadFile(filename=filename, file=BytesIO(content))
|
||
|
|
document = await document_service.upload_document(self.user_id, upload, folder_id=local_folder_id)
|
||
|
|
await self._upsert_sync_item(mount.id, node, local_folder_id, document.id)
|
||
|
|
document_ids.append(document.id)
|
||
|
|
synced += 1
|
||
|
|
except Exception as exc: # noqa: BLE001
|
||
|
|
failed += 1
|
||
|
|
errors.append(f"{node.path}: {exc}")
|
||
|
|
await self._upsert_sync_item(mount.id, node, local_folder_id, None, status="failed", error=str(exc))
|
||
|
|
|
||
|
|
mount.last_sync_at = datetime.now(UTC).isoformat()
|
||
|
|
await self.db.commit()
|
||
|
|
return {
|
||
|
|
"synced": synced,
|
||
|
|
"skipped": skipped,
|
||
|
|
"failed": failed,
|
||
|
|
"document_ids": document_ids,
|
||
|
|
"errors": errors,
|
||
|
|
}
|
||
|
|
|
||
|
|
def _flatten_files(self, nodes: list[WebDavNode]) -> list[WebDavNode]:
|
||
|
|
results: list[WebDavNode] = []
|
||
|
|
for node in nodes:
|
||
|
|
if node.is_dir:
|
||
|
|
results.extend(self._flatten_files(node.children))
|
||
|
|
else:
|
||
|
|
results.append(node)
|
||
|
|
return results
|
||
|
|
|
||
|
|
async def _upsert_sync_item(
|
||
|
|
self,
|
||
|
|
mount_id: str,
|
||
|
|
node: WebDavNode,
|
||
|
|
local_folder_id: str,
|
||
|
|
local_document_id: str | None,
|
||
|
|
status: str = "synced",
|
||
|
|
error: str | None = None,
|
||
|
|
) -> None:
|
||
|
|
result = await self.db.execute(
|
||
|
|
select(RemoteSyncItem).where(
|
||
|
|
and_(RemoteSyncItem.mount_id == mount_id, RemoteSyncItem.remote_path == node.path)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
sync_item = result.scalar_one_or_none()
|
||
|
|
if sync_item is None:
|
||
|
|
sync_item = RemoteSyncItem(
|
||
|
|
mount_id=mount_id,
|
||
|
|
remote_path=node.path,
|
||
|
|
)
|
||
|
|
self.db.add(sync_item)
|
||
|
|
|
||
|
|
sync_item.remote_etag = node.etag
|
||
|
|
sync_item.remote_modified_at = node.modified_at
|
||
|
|
sync_item.local_folder_id = local_folder_id
|
||
|
|
sync_item.local_document_id = local_document_id
|
||
|
|
sync_item.sync_status = status
|
||
|
|
sync_item.last_error = error
|
||
|
|
sync_item.last_synced_at = datetime.now(UTC).isoformat()
|