diff --git a/routers/files.py b/routers/files.py index 834f541..98320e6 100644 --- a/routers/files.py +++ b/routers/files.py @@ -91,9 +91,11 @@ async def sync_files(db: AsyncSession): )) db_records = [dict(r._mapping) for r in result] - # Build lookup sets + # Build lookup maps all_db_paths = {r["storage_path"] for r in db_records} active_db_paths = {r["storage_path"] for r in db_records if not r["is_deleted"]} + deleted_on_disk = {r["storage_path"]: r for r in db_records + if r["is_deleted"] and r["storage_path"] in disk_files} # New on disk, not in DB at all → create record new_files = disk_files - all_db_paths @@ -116,6 +118,12 @@ async def sync_files(db: AsyncSession): }) added += 1 + # Soft-deleted in DB but still on disk → restore + for rel_path, record in deleted_on_disk.items(): + repo = BaseRepository("files", db) + await repo.restore(record["id"]) + added += 1 + # Active in DB but missing from disk → soft-delete missing_files = active_db_paths - disk_files for record in db_records: @@ -127,10 +135,21 @@ async def sync_files(db: AsyncSession): return {"added": added, "removed": removed} +SORT_OPTIONS = { + "path": "storage_path ASC", + "path_desc": "storage_path DESC", + "name": "original_filename ASC", + "name_desc": "original_filename DESC", + "date": "created_at DESC", + "date_asc": "created_at ASC", +} + + @router.get("/") async def list_files( request: Request, folder: Optional[str] = None, + sort: Optional[str] = None, context_type: Optional[str] = None, context_id: Optional[str] = None, db: AsyncSession = Depends(get_db), @@ -142,36 +161,38 @@ async def list_files( folders = get_folders() + order_by = SORT_OPTIONS.get(sort, "storage_path ASC") + if context_type and context_id: # Files attached to a specific entity - result = await db.execute(text(""" + result = await db.execute(text(f""" SELECT f.*, fm.context_type, fm.context_id FROM files f JOIN file_mappings fm ON fm.file_id = f.id WHERE f.is_deleted = false AND fm.context_type = :ct AND fm.context_id = :cid - ORDER BY f.created_at DESC + ORDER BY {order_by} """), {"ct": context_type, "cid": context_id}) elif folder is not None: if folder == "": # Root folder: files with no directory separator in storage_path - result = await db.execute(text(""" + result = await db.execute(text(f""" SELECT * FROM files WHERE is_deleted = false AND storage_path NOT LIKE '%/%' - ORDER BY created_at DESC + ORDER BY {order_by} """)) else: # Specific folder: storage_path starts with folder/ - result = await db.execute(text(""" + result = await db.execute(text(f""" SELECT * FROM files WHERE is_deleted = false AND storage_path LIKE :prefix - ORDER BY created_at DESC + ORDER BY {order_by} """), {"prefix": folder + "/%"}) else: # All files - result = await db.execute(text(""" + result = await db.execute(text(f""" SELECT * FROM files WHERE is_deleted = false - ORDER BY created_at DESC + ORDER BY {order_by} """)) items = [dict(r._mapping) for r in result] @@ -184,6 +205,7 @@ async def list_files( return templates.TemplateResponse("files.html", { "request": request, "sidebar": sidebar, "items": items, "folders": folders, "current_folder": folder, + "current_sort": sort or "path", "sync_result": sync_result, "context_type": context_type or "", "context_id": context_id or "", diff --git a/templates/files.html b/templates/files.html index 29a1668..35e59e4 100644 --- a/templates/files.html +++ b/templates/files.html @@ -16,41 +16,70 @@ {% endif %} -{% if folders %} -
- All - / - {% for f in folders %} - {{ f }} - {% endfor %} +
+ +
-{% endif %} + +{% set sort_base = '/files?' ~ ('folder=' ~ current_folder ~ '&' if current_folder is not none else '') %} {% if items %} -
- {% for item in items %} -
- - {{ item.original_filename }} - - {{ item.folder }} - {% if item.mime_type %} - {{ item.mime_type.split('/')|last }} - {% endif %} - {% if item.size_bytes %} - {{ "%.1f"|format(item.size_bytes / 1024) }} KB - {% endif %} - {% if item.description %} - {{ item.description[:50] }} - {% endif %} -
- Download -
- -
-
-
- {% endfor %} +
+ + + + + + + + + + + + + {% for item in items %} + + + + + + + + + {% endfor %} + +
+ + Path {{ '▲' if current_sort == 'path' else ('▼' if current_sort == 'path_desc' else '') }} + + + + Name {{ '▲' if current_sort == 'name' else ('▼' if current_sort == 'name_desc' else '') }} + + TypeSize + + Date {{ '▼' if current_sort == 'date' else ('▲' if current_sort == 'date_asc' else '') }} + +
{{ item.folder }} + {{ item.original_filename }} + + {% if item.mime_type %}{{ item.mime_type.split('/')|last }}{% endif %} + + {% if item.size_bytes %}{{ "%.1f"|format(item.size_bytes / 1024) }} KB{% endif %} + + {{ item.created_at.strftime('%Y-%m-%d') if item.created_at else '' }} + + Download +
+ +
+
{% else %}