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:
XiaoQing235
2026-08-27 22:21:08 +08:00
committed by GitHub
co-authored by shiyu
parent 4d907c7bfd
commit 0763d457d5
11 changed files with 486 additions and 88 deletions
+16 -9
View File
@@ -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
View File
@@ -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