mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-06 08:07:22 +08:00
feat: paginate public share directories (#132)
* feat: paginate public share directories * feat: add cursor pagination to public share listings Unify share and VFS directory payloads, pass cursor through the share API, and add prev/next controls plus tests for cursor-mode adapters. --------- Co-authored-by: shiyu <im@shiyu.dev>
This commit is contained in:
+16
-9
@@ -1,6 +1,6 @@
|
||||
from typing import Annotated, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
|
||||
from api.response import success
|
||||
from domain.audit import AuditAction, audit
|
||||
@@ -95,20 +95,27 @@ async def verify_password(request: Request, token: str, payload: SharePassword):
|
||||
@public_router.get("/{token}/ls")
|
||||
@audit(action=AuditAction.SHARE, description="浏览分享内容")
|
||||
async def list_share_content(
|
||||
request: Request, token: str, path: str = "/", password: Optional[str] = None
|
||||
request: Request,
|
||||
token: str,
|
||||
path: str = "/",
|
||||
password: Optional[str] = None,
|
||||
page_num: int = Query(1, alias="page", ge=1, description="页码"),
|
||||
page_size: int = Query(50, ge=1, le=500, description="每页条数"),
|
||||
cursor: str | None = Query(None, description="游标分页位置"),
|
||||
):
|
||||
share = await ShareService.ensure_share_access(token, password)
|
||||
content = await ShareService.get_shared_item_details(share, path)
|
||||
content = await ShareService.get_shared_item_details(
|
||||
share=share,
|
||||
sub_path=path,
|
||||
page_num=page_num,
|
||||
page_size=page_size,
|
||||
cursor=cursor,
|
||||
)
|
||||
return success(
|
||||
{
|
||||
"path": path,
|
||||
"entries": content.get("items", []),
|
||||
"pagination": {
|
||||
"total": content.get("total", 0),
|
||||
"page": content.get("page", 1),
|
||||
"page_size": content.get("page_size", 1),
|
||||
"pages": content.get("pages", 1),
|
||||
},
|
||||
"pagination": content.get("pagination"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+58
-8
@@ -7,6 +7,7 @@ import bcrypt
|
||||
from fastapi import HTTPException, status
|
||||
from fastapi.responses import Response
|
||||
|
||||
from api.response import listing_pagination
|
||||
from domain.virtual_fs import VirtualFSService
|
||||
from models.database import ShareLink, UserAccount
|
||||
|
||||
@@ -114,7 +115,53 @@ class ShareService:
|
||||
return deleted_count
|
||||
|
||||
@classmethod
|
||||
async def get_shared_item_details(cls, share: ShareLink, sub_path: str = ""):
|
||||
async def _list_shared_virtual_dir(
|
||||
cls,
|
||||
vfs_path: str,
|
||||
page_num: int,
|
||||
page_size: int,
|
||||
cursor: str | None,
|
||||
):
|
||||
try:
|
||||
result = await VirtualFSService.list_virtual_dir(
|
||||
vfs_path,
|
||||
page_num,
|
||||
page_size,
|
||||
cursor=cursor,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="目录未找到")
|
||||
except HTTPException as e:
|
||||
if e.status_code == 404:
|
||||
raise HTTPException(status_code=404, detail="目录未找到") from e
|
||||
raise
|
||||
items, pagination = listing_pagination(
|
||||
result, page_num=page_num, page_size=page_size, cursor=cursor
|
||||
)
|
||||
return {"items": items, "pagination": pagination}
|
||||
|
||||
@classmethod
|
||||
def _single_file_listing(cls, stat: dict, page_num: int, page_size: int):
|
||||
return {
|
||||
"items": [stat] if page_num == 1 else [],
|
||||
"pagination": {
|
||||
"mode": "paged",
|
||||
"page_size": page_size,
|
||||
"total": 1,
|
||||
"page": page_num,
|
||||
"pages": 1,
|
||||
},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def get_shared_item_details(
|
||||
cls,
|
||||
share: ShareLink,
|
||||
sub_path: str = "",
|
||||
page_num: int = 1,
|
||||
page_size: int = 50,
|
||||
cursor: str | None = None,
|
||||
):
|
||||
if not share.paths:
|
||||
raise HTTPException(status_code=404, detail="分享内容为空")
|
||||
|
||||
@@ -124,21 +171,24 @@ class ShareService:
|
||||
full_path = f"{base_shared_path.rstrip('/')}/{sub_path.lstrip('/')}".rstrip("/")
|
||||
if not full_path.startswith(base_shared_path):
|
||||
raise HTTPException(status_code=403, detail="无权访问此路径")
|
||||
try:
|
||||
return await VirtualFSService.list_virtual_dir(full_path)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail="目录未找到")
|
||||
return await cls._list_shared_virtual_dir(
|
||||
full_path, page_num, page_size, cursor
|
||||
)
|
||||
|
||||
try:
|
||||
stat = await VirtualFSService.stat_file(base_shared_path)
|
||||
if stat.get("is_dir"):
|
||||
return await VirtualFSService.list_virtual_dir(base_shared_path)
|
||||
return await cls._list_shared_virtual_dir(
|
||||
base_shared_path, page_num, page_size, cursor
|
||||
)
|
||||
|
||||
stat["name"] = base_shared_path.split("/")[-1]
|
||||
return {"items": [stat], "total": 1, "page": 1, "page_size": 1, "pages": 1}
|
||||
return cls._single_file_listing(stat, page_num, page_size)
|
||||
except HTTPException as e:
|
||||
if "Path is a directory" in str(e.detail) or "Not a file" in str(e.detail):
|
||||
return await VirtualFSService.list_virtual_dir(base_shared_path)
|
||||
return await cls._list_shared_virtual_dir(
|
||||
base_shared_path, page_num, page_size, cursor
|
||||
)
|
||||
raise e
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -5,6 +5,7 @@ from urllib.parse import quote
|
||||
from fastapi import HTTPException, Request, UploadFile
|
||||
from fastapi.responses import Response
|
||||
|
||||
from api.response import dir_listing
|
||||
from domain.config import ConfigService
|
||||
from domain.tasks import TaskService
|
||||
from .thumbnail import (
|
||||
@@ -293,31 +294,12 @@ class VirtualFSRouteMixin(VirtualFSTempLinkMixin):
|
||||
async def list_directory(cls, full_path: str, page_num: int, page_size: int, sort_by: str, sort_order: str):
|
||||
full_path = cls._normalize_path(full_path)
|
||||
result = await cls.list_virtual_dir(full_path, page_num, page_size, sort_by, sort_order)
|
||||
pagination = {
|
||||
"mode": result.get("pagination_mode", "paged"),
|
||||
"page_size": result.get("page_size", page_size),
|
||||
}
|
||||
if pagination["mode"] == "cursor":
|
||||
pagination.update(
|
||||
{
|
||||
"cursor": result.get("cursor"),
|
||||
"next_cursor": result.get("next_cursor"),
|
||||
"has_next": bool(result.get("has_next")),
|
||||
}
|
||||
)
|
||||
else:
|
||||
pagination.update(
|
||||
{
|
||||
"total": result["total"],
|
||||
"page": result["page"],
|
||||
"pages": result["pages"],
|
||||
}
|
||||
)
|
||||
return {
|
||||
"path": full_path,
|
||||
"entries": result["items"],
|
||||
"pagination": pagination,
|
||||
}
|
||||
return dir_listing(
|
||||
result,
|
||||
path=full_path,
|
||||
page_num=page_num,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def delete(cls, full_path: str):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from api.response import dir_listing
|
||||
|
||||
from .common import VirtualFSCommonMixin
|
||||
from .resolver import VirtualFSResolverMixin
|
||||
from .listing import VirtualFSListingMixin
|
||||
@@ -47,28 +49,10 @@ class VirtualFSService(
|
||||
result = await cls.list_virtual_dir_with_permission(
|
||||
full_path, user_id, page_num, page_size, sort_by, sort_order, cursor
|
||||
)
|
||||
pagination = {
|
||||
"mode": result.get("pagination_mode", "paged") if isinstance(result, dict) else "paged",
|
||||
"page_size": result.get("page_size", page_size) if isinstance(result, dict) else page_size,
|
||||
}
|
||||
if pagination["mode"] == "cursor":
|
||||
pagination.update(
|
||||
{
|
||||
"cursor": result.get("cursor") if isinstance(result, dict) else cursor,
|
||||
"next_cursor": result.get("next_cursor") if isinstance(result, dict) else None,
|
||||
"has_next": bool(result.get("has_next")) if isinstance(result, dict) else False,
|
||||
}
|
||||
)
|
||||
else:
|
||||
pagination.update(
|
||||
{
|
||||
"total": result.get("total", 0) if isinstance(result, dict) else 0,
|
||||
"page": result.get("page", page_num) if isinstance(result, dict) else page_num,
|
||||
"pages": result.get("pages", 0) if isinstance(result, dict) else 0,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"path": full_path,
|
||||
"entries": result.get("items", []) if isinstance(result, dict) else [],
|
||||
"pagination": pagination,
|
||||
}
|
||||
return dir_listing(
|
||||
result,
|
||||
path=full_path,
|
||||
page_num=page_num,
|
||||
page_size=page_size,
|
||||
cursor=cursor,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user