mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-07 02:32:41 +08:00
feat: add metadata options for file status checks and optimize permission filtering logic
This commit is contained in:
@@ -299,23 +299,23 @@ class LocalAdapter:
|
||||
|
||||
return StreamingResponse(iterator(), status_code=status, headers=headers, media_type=content_type)
|
||||
|
||||
async def stat_file(self, root: str, rel: str):
|
||||
async def stat_file(self, root: str, rel: str, include_metadata: bool = False):
|
||||
fp = _safe_join(root, rel)
|
||||
if not fp.exists():
|
||||
raise FileNotFoundError(rel)
|
||||
st = await asyncio.to_thread(fp.stat)
|
||||
is_dir = fp.is_dir()
|
||||
info = {
|
||||
"name": fp.name,
|
||||
"is_dir": fp.is_dir(),
|
||||
"is_dir": is_dir,
|
||||
"size": st.st_size,
|
||||
"mtime": int(st.st_mtime),
|
||||
"mode": stat.S_IMODE(st.st_mode),
|
||||
"type": "dir" if fp.is_dir() else "file",
|
||||
"type": "dir" if is_dir else "file",
|
||||
"path": str(fp),
|
||||
}
|
||||
# exif信息
|
||||
exif = None
|
||||
if not fp.is_dir():
|
||||
if include_metadata and not is_dir:
|
||||
exif = None
|
||||
mime, _ = mimetypes.guess_type(fp.name)
|
||||
if mime and mime.startswith("image/"):
|
||||
try:
|
||||
@@ -326,7 +326,7 @@ class LocalAdapter:
|
||||
exif = {str(k): str(v) for k, v in exif_data.items()}
|
||||
except Exception:
|
||||
exif = None
|
||||
info["exif"] = exif
|
||||
info["exif"] = exif
|
||||
return info
|
||||
|
||||
|
||||
|
||||
@@ -376,7 +376,7 @@ class WebDAVAdapter:
|
||||
|
||||
return StreamingResponse(segmented_body(), status_code=status_code, headers=resp_headers, media_type=content_type)
|
||||
|
||||
async def stat_file(self, root: str, rel: str):
|
||||
async def stat_file(self, root: str, rel: str, include_metadata: bool = False):
|
||||
url = self._build_url(rel)
|
||||
async with self._client() as client:
|
||||
# PROPFIND 获取属性
|
||||
@@ -426,9 +426,8 @@ class WebDAVAdapter:
|
||||
info["mtime"] = 0
|
||||
elif info["mtime"] is None:
|
||||
info["mtime"] = 0
|
||||
# exif信息
|
||||
exif = None
|
||||
if not info["is_dir"]:
|
||||
if include_metadata and not info["is_dir"]:
|
||||
exif = None
|
||||
mime, _ = mimetypes.guess_type(info["name"])
|
||||
if mime and mime.startswith("image/"):
|
||||
try:
|
||||
@@ -442,7 +441,7 @@ class WebDAVAdapter:
|
||||
exif = {str(k): str(v) for k, v in exif_data.items()}
|
||||
except Exception:
|
||||
exif = None
|
||||
info["exif"] = exif
|
||||
info["exif"] = exif
|
||||
return info
|
||||
|
||||
async def exists(self, root: str, rel: str) -> bool:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -17,74 +18,169 @@ from .types import (
|
||||
PERMISSION_DEFINITIONS,
|
||||
)
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PermissionContext:
|
||||
exists: bool
|
||||
is_admin: bool
|
||||
path_rules: List[PathRule]
|
||||
|
||||
|
||||
class PermissionService:
|
||||
"""权限检查服务"""
|
||||
|
||||
# 权限检查结果缓存(简单的内存缓存)
|
||||
_cache: dict[str, tuple[bool, float]] = {}
|
||||
_context_cache: dict[int, tuple[PermissionContext, float]] = {}
|
||||
_cache_ttl = 300 # 5分钟缓存
|
||||
|
||||
@classmethod
|
||||
def _now(cls) -> float:
|
||||
import time
|
||||
|
||||
return time.time()
|
||||
|
||||
@classmethod
|
||||
def _is_cache_valid(cls, timestamp: float) -> bool:
|
||||
return cls._now() - timestamp < cls._cache_ttl
|
||||
|
||||
@classmethod
|
||||
def _get_cached_result(cls, cache_key: str) -> Optional[bool]:
|
||||
cached = cls._cache.get(cache_key)
|
||||
if not cached:
|
||||
return None
|
||||
result, timestamp = cached
|
||||
if cls._is_cache_valid(timestamp):
|
||||
return result
|
||||
cls._cache.pop(cache_key, None)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _sort_path_rules(cls, rules: List[PathRule]) -> List[PathRule]:
|
||||
return sorted(
|
||||
rules,
|
||||
key=lambda r: (
|
||||
r.priority,
|
||||
PathMatcher.get_pattern_specificity(r.path_pattern, r.is_regex),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _match_sorted_path_rules(
|
||||
cls, path: str, action: str, sorted_rules: List[PathRule]
|
||||
) -> Optional[bool]:
|
||||
for rule in sorted_rules:
|
||||
if PathMatcher.match_pattern(path, rule.path_pattern, rule.is_regex):
|
||||
if action == PathAction.READ:
|
||||
return rule.can_read
|
||||
if action == PathAction.WRITE:
|
||||
return rule.can_write
|
||||
if action == PathAction.DELETE:
|
||||
return rule.can_delete
|
||||
if action == PathAction.SHARE:
|
||||
return rule.can_share
|
||||
return False
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
async def _get_permission_context(cls, user_id: int) -> PermissionContext:
|
||||
cached = cls._context_cache.get(user_id)
|
||||
if cached:
|
||||
context, timestamp = cached
|
||||
if cls._is_cache_valid(timestamp):
|
||||
return context
|
||||
cls._context_cache.pop(user_id, None)
|
||||
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
context = PermissionContext(exists=False, is_admin=False, path_rules=[])
|
||||
cls._context_cache[user_id] = (context, cls._now())
|
||||
return context
|
||||
|
||||
if user.is_admin:
|
||||
context = PermissionContext(exists=True, is_admin=True, path_rules=[])
|
||||
cls._context_cache[user_id] = (context, cls._now())
|
||||
return context
|
||||
|
||||
user_roles = await UserRole.filter(user_id=user_id)
|
||||
role_ids = [ur.role_id for ur in user_roles]
|
||||
if not role_ids:
|
||||
context = PermissionContext(exists=True, is_admin=False, path_rules=[])
|
||||
cls._context_cache[user_id] = (context, cls._now())
|
||||
return context
|
||||
|
||||
path_rules = await PathRule.filter(role_id__in=role_ids)
|
||||
context = PermissionContext(
|
||||
exists=True,
|
||||
is_admin=False,
|
||||
path_rules=cls._sort_path_rules(list(path_rules)),
|
||||
)
|
||||
cls._context_cache[user_id] = (context, cls._now())
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def _check_path_permission_with_context(
|
||||
cls,
|
||||
user_id: int,
|
||||
normalized_path: str,
|
||||
action: str,
|
||||
context: PermissionContext,
|
||||
) -> bool:
|
||||
if not context.exists:
|
||||
return False
|
||||
if context.is_admin:
|
||||
return True
|
||||
|
||||
checked_cache_keys: List[str] = []
|
||||
current_path = normalized_path
|
||||
|
||||
while True:
|
||||
cache_key = f"{user_id}:{current_path}:{action}"
|
||||
cached_result = cls._get_cached_result(cache_key)
|
||||
if cached_result is not None:
|
||||
result = cached_result
|
||||
break
|
||||
|
||||
checked_cache_keys.append(cache_key)
|
||||
result = cls._match_sorted_path_rules(current_path, action, context.path_rules)
|
||||
if result is not None:
|
||||
break
|
||||
|
||||
parent_path = PathMatcher.get_parent_path(current_path)
|
||||
if not parent_path:
|
||||
result = False
|
||||
break
|
||||
current_path = parent_path
|
||||
|
||||
timestamp = cls._now()
|
||||
for cache_key in checked_cache_keys:
|
||||
cls._cache[cache_key] = (result, timestamp)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def check_path_permission(
|
||||
cls, user_id: int, path: str, action: str
|
||||
) -> bool:
|
||||
"""
|
||||
检查用户对路径的操作权限
|
||||
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
path: 要检查的路径
|
||||
action: 操作类型 (read/write/delete/share)
|
||||
|
||||
|
||||
Returns:
|
||||
是否有权限
|
||||
"""
|
||||
import time
|
||||
|
||||
# 检查缓存
|
||||
cache_key = f"{user_id}:{path}:{action}"
|
||||
if cache_key in cls._cache:
|
||||
result, timestamp = cls._cache[cache_key]
|
||||
if time.time() - timestamp < cls._cache_ttl:
|
||||
return result
|
||||
|
||||
# 获取用户
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
return False
|
||||
|
||||
# 超级管理员直接放行
|
||||
if user.is_admin:
|
||||
cls._cache[cache_key] = (True, time.time())
|
||||
return True
|
||||
|
||||
# 获取用户所有角色
|
||||
user_roles = await UserRole.filter(user_id=user_id).prefetch_related("role")
|
||||
role_ids = [ur.role_id for ur in user_roles]
|
||||
|
||||
if not role_ids:
|
||||
cls._cache[cache_key] = (False, time.time())
|
||||
return False
|
||||
|
||||
# 获取所有角色的路径规则
|
||||
path_rules = await PathRule.filter(role_id__in=role_ids).order_by("-priority")
|
||||
|
||||
# 规范化路径
|
||||
normalized_path = PathMatcher.normalize_path(path)
|
||||
cache_key = f"{user_id}:{normalized_path}:{action}"
|
||||
cached_result = cls._get_cached_result(cache_key)
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
|
||||
# 按优先级和具体程度匹配
|
||||
result = cls._match_path_rules(normalized_path, action, list(path_rules))
|
||||
|
||||
# 如果没有匹配到规则,检查父目录(继承)
|
||||
if result is None:
|
||||
parent_path = PathMatcher.get_parent_path(normalized_path)
|
||||
if parent_path:
|
||||
result = await cls.check_path_permission(user_id, parent_path, action)
|
||||
else:
|
||||
result = False # 默认拒绝
|
||||
|
||||
cls._cache[cache_key] = (result, time.time())
|
||||
context = await cls._get_permission_context(user_id)
|
||||
result = cls._check_path_permission_with_context(user_id, normalized_path, action, context)
|
||||
cls._cache[cache_key] = (result, cls._now())
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
@@ -97,31 +193,7 @@ class PermissionService:
|
||||
Returns:
|
||||
True/False 表示明确的权限结果,None 表示没有匹配到规则
|
||||
"""
|
||||
# 按优先级和具体程度排序
|
||||
sorted_rules = sorted(
|
||||
rules,
|
||||
key=lambda r: (
|
||||
r.priority,
|
||||
PathMatcher.get_pattern_specificity(r.path_pattern, r.is_regex),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
for rule in sorted_rules:
|
||||
if PathMatcher.match_pattern(path, rule.path_pattern, rule.is_regex):
|
||||
# 匹配到规则,检查具体操作权限
|
||||
if action == PathAction.READ:
|
||||
return rule.can_read
|
||||
elif action == PathAction.WRITE:
|
||||
return rule.can_write
|
||||
elif action == PathAction.DELETE:
|
||||
return rule.can_delete
|
||||
elif action == PathAction.SHARE:
|
||||
return rule.can_share
|
||||
else:
|
||||
return False
|
||||
|
||||
return None
|
||||
return cls._match_sorted_path_rules(path, action, cls._sort_path_rules(rules))
|
||||
|
||||
@classmethod
|
||||
async def check_system_permission(cls, user_id: int, permission_code: str) -> bool:
|
||||
@@ -251,35 +323,20 @@ class PermissionService:
|
||||
cls, user_id: int, path: str, action: str
|
||||
) -> PathPermissionResult:
|
||||
"""检查路径权限并返回详细结果"""
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if not user:
|
||||
context = await cls._get_permission_context(user_id)
|
||||
if not context.exists:
|
||||
return PathPermissionResult(path=path, action=action, allowed=False)
|
||||
|
||||
# 超级管理员
|
||||
if user.is_admin:
|
||||
if context.is_admin:
|
||||
return PathPermissionResult(path=path, action=action, allowed=True)
|
||||
|
||||
# 获取用户角色
|
||||
user_roles = await UserRole.filter(user_id=user_id)
|
||||
role_ids = [ur.role_id for ur in user_roles]
|
||||
|
||||
if not role_ids:
|
||||
if not context.path_rules:
|
||||
return PathPermissionResult(path=path, action=action, allowed=False)
|
||||
|
||||
# 获取路径规则
|
||||
path_rules = await PathRule.filter(role_id__in=role_ids).order_by("-priority")
|
||||
normalized_path = PathMatcher.normalize_path(path)
|
||||
|
||||
# 查找匹配的规则
|
||||
matched_rule = None
|
||||
for rule in sorted(
|
||||
path_rules,
|
||||
key=lambda r: (
|
||||
r.priority,
|
||||
PathMatcher.get_pattern_specificity(r.path_pattern, r.is_regex),
|
||||
),
|
||||
reverse=True,
|
||||
):
|
||||
for rule in context.path_rules:
|
||||
if PathMatcher.match_pattern(
|
||||
normalized_path, rule.path_pattern, rule.is_regex
|
||||
):
|
||||
@@ -322,19 +379,30 @@ class PermissionService:
|
||||
"""清除权限缓存"""
|
||||
if user_id is None:
|
||||
cls._cache.clear()
|
||||
cls._context_cache.clear()
|
||||
else:
|
||||
# 清除特定用户的缓存
|
||||
keys_to_delete = [k for k in cls._cache if k.startswith(f"{user_id}:")]
|
||||
for k in keys_to_delete:
|
||||
del cls._cache[k]
|
||||
cls._context_cache.pop(user_id, None)
|
||||
|
||||
@classmethod
|
||||
async def filter_paths_by_permission(
|
||||
cls, user_id: int, paths: List[str], action: str
|
||||
) -> List[str]:
|
||||
"""过滤出用户有权限的路径列表"""
|
||||
if not paths:
|
||||
return []
|
||||
|
||||
context = await cls._get_permission_context(user_id)
|
||||
if not context.exists:
|
||||
return []
|
||||
if context.is_admin:
|
||||
return list(paths)
|
||||
|
||||
result = []
|
||||
for path in paths:
|
||||
if await cls.check_path_permission(user_id, path, action):
|
||||
normalized_path = PathMatcher.normalize_path(path)
|
||||
if cls._check_path_permission_with_context(user_id, normalized_path, action, context):
|
||||
result.append(path)
|
||||
return result
|
||||
|
||||
@@ -84,8 +84,9 @@ async def get_file_stat(
|
||||
full_path: str,
|
||||
request: Request,
|
||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||
verbose: bool = Query(False, description="是否返回扩展元数据"),
|
||||
):
|
||||
stat = await VirtualFSService.stat(full_path)
|
||||
stat = await VirtualFSService.stat(full_path, verbose=verbose)
|
||||
return success(stat)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import inspect
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
from fastapi import HTTPException
|
||||
@@ -14,6 +15,23 @@ from .resolver import VirtualFSResolverMixin
|
||||
|
||||
|
||||
class VirtualFSListingMixin(VirtualFSResolverMixin):
|
||||
@staticmethod
|
||||
async def _call_stat_file(
|
||||
stat_func,
|
||||
root: str,
|
||||
rel: str,
|
||||
*,
|
||||
include_metadata: bool = False,
|
||||
):
|
||||
try:
|
||||
parameters = inspect.signature(stat_func).parameters
|
||||
except (TypeError, ValueError):
|
||||
parameters = {}
|
||||
|
||||
if "include_metadata" in parameters:
|
||||
return await stat_func(root, rel, include_metadata=include_metadata)
|
||||
return await stat_func(root, rel)
|
||||
|
||||
@classmethod
|
||||
async def path_is_directory(cls, path: str) -> bool:
|
||||
adapter_instance, _, root, rel = await cls.resolve_adapter_and_rel(path)
|
||||
@@ -24,7 +42,7 @@ class VirtualFSListingMixin(VirtualFSResolverMixin):
|
||||
if not callable(stat_func):
|
||||
raise HTTPException(501, detail="Adapter does not implement stat_file")
|
||||
try:
|
||||
info = await stat_func(root, rel)
|
||||
info = await cls._call_stat_file(stat_func, root, rel, include_metadata=False)
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(404, detail="Path not found")
|
||||
if isinstance(info, dict):
|
||||
@@ -110,7 +128,12 @@ class VirtualFSListingMixin(VirtualFSResolverMixin):
|
||||
stat_file = getattr(adapter_instance, "stat_file", None)
|
||||
if callable(stat_file):
|
||||
try:
|
||||
parent_info = await stat_file(effective_root, rel)
|
||||
parent_info = await cls._call_stat_file(
|
||||
stat_file,
|
||||
effective_root,
|
||||
rel,
|
||||
include_metadata=False,
|
||||
)
|
||||
if isinstance(parent_info, dict):
|
||||
parent_info.setdefault("name", rel.split("/")[-1])
|
||||
parent_info["is_dir"] = bool(parent_info.get("is_dir", True))
|
||||
@@ -121,7 +144,12 @@ class VirtualFSListingMixin(VirtualFSResolverMixin):
|
||||
stat_file = getattr(adapter_instance, "stat_file", None)
|
||||
if callable(stat_file):
|
||||
try:
|
||||
parent_info = await stat_file(effective_root, parent_rel)
|
||||
parent_info = await cls._call_stat_file(
|
||||
stat_file,
|
||||
effective_root,
|
||||
parent_rel,
|
||||
include_metadata=False,
|
||||
)
|
||||
if isinstance(parent_info, dict):
|
||||
parent_info.setdefault("name", parent_rel.split("/")[-1])
|
||||
parent_info["is_dir"] = bool(parent_info.get("is_dir", True))
|
||||
@@ -222,13 +250,18 @@ class VirtualFSListingMixin(VirtualFSResolverMixin):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
async def stat_file(cls, path: str):
|
||||
async def stat_file(cls, path: str, verbose: bool = False):
|
||||
adapter_instance, _, root, rel = await cls.resolve_adapter_and_rel(path)
|
||||
stat_func = getattr(adapter_instance, "stat_file", None)
|
||||
if not callable(stat_func):
|
||||
raise HTTPException(501, detail="Adapter does not implement stat_file")
|
||||
try:
|
||||
info = await stat_func(root, rel)
|
||||
info = await cls._call_stat_file(
|
||||
stat_func,
|
||||
root,
|
||||
rel,
|
||||
include_metadata=verbose,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(404, detail=str(exc))
|
||||
|
||||
@@ -241,7 +274,7 @@ class VirtualFSListingMixin(VirtualFSResolverMixin):
|
||||
rel_name = rel.rstrip("/").split("/")[-1] if rel else path.rstrip("/").split("/")[-1]
|
||||
name_hint = str(info.get("name") or rel_name or "")
|
||||
info["has_thumbnail"] = bool(not is_dir and (is_image_filename(name_hint) or is_video_filename(name_hint)))
|
||||
if not is_dir:
|
||||
if verbose and not is_dir:
|
||||
vector_index = await cls._gather_vector_index(path)
|
||||
if vector_index is not None:
|
||||
info["vector_index"] = vector_index
|
||||
@@ -263,38 +296,26 @@ class VirtualFSListingMixin(VirtualFSResolverMixin):
|
||||
|
||||
过滤掉用户没有读取权限的条目
|
||||
"""
|
||||
# 首先获取完整的目录列表
|
||||
result = await cls.list_virtual_dir(path, page_num, page_size, sort_by, sort_order)
|
||||
|
||||
# 检查用户是否是管理员(管理员可以看到所有内容)
|
||||
from models.database import UserAccount
|
||||
user = await UserAccount.get_or_none(id=user_id)
|
||||
if user and user.is_admin:
|
||||
return result
|
||||
|
||||
# 过滤无权限的条目
|
||||
items = result.get("items", [])
|
||||
if not items:
|
||||
return result
|
||||
|
||||
|
||||
norm = cls._normalize_path(path).rstrip("/") or "/"
|
||||
filtered_items = []
|
||||
|
||||
path_pairs: List[Tuple[str, Dict]] = []
|
||||
for item in items:
|
||||
item_name = item.get("name", "")
|
||||
if norm == "/":
|
||||
item_path = f"/{item_name}"
|
||||
else:
|
||||
item_path = f"{norm}/{item_name}"
|
||||
|
||||
# 检查用户是否有读取权限
|
||||
has_permission = await PermissionService.check_path_permission(
|
||||
user_id, item_path, PathAction.READ
|
||||
)
|
||||
if has_permission:
|
||||
filtered_items.append(item)
|
||||
|
||||
# 更新结果
|
||||
result["items"] = filtered_items
|
||||
|
||||
path_pairs.append((item_path, item))
|
||||
|
||||
allowed_paths = await PermissionService.filter_paths_by_permission(
|
||||
user_id,
|
||||
[item_path for item_path, _ in path_pairs],
|
||||
PathAction.READ,
|
||||
)
|
||||
allowed_set = set(allowed_paths)
|
||||
result["items"] = [item for item_path, item in path_pairs if item_path in allowed_set]
|
||||
return result
|
||||
|
||||
@@ -144,9 +144,9 @@ class VirtualFSRouteMixin(VirtualFSTempLinkMixin):
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
async def stat(cls, full_path: str):
|
||||
async def stat(cls, full_path: str, verbose: bool = False):
|
||||
full_path = cls._normalize_path(full_path)
|
||||
return await cls.stat_file(full_path)
|
||||
return await cls.stat_file(full_path, verbose=verbose)
|
||||
|
||||
@classmethod
|
||||
async def write_uploaded_file(cls, full_path: str, data: bytes):
|
||||
|
||||
@@ -28,12 +28,12 @@ async def search_files(
|
||||
data = await VirtualFSSearchService.search(q, top_k, mode, page, page_size)
|
||||
items = data.get("items") if isinstance(data, dict) else None
|
||||
if isinstance(items, list) and items:
|
||||
filtered = []
|
||||
for item in items:
|
||||
path = getattr(item, "path", None)
|
||||
if not path:
|
||||
continue
|
||||
if await PermissionService.check_path_permission(user.id, str(path), PathAction.READ):
|
||||
filtered.append(item)
|
||||
data["items"] = filtered
|
||||
path_pairs = [(str(item.path), item) for item in items if getattr(item, "path", None)]
|
||||
allowed_paths = await PermissionService.filter_paths_by_permission(
|
||||
user.id,
|
||||
[path for path, _ in path_pairs],
|
||||
PathAction.READ,
|
||||
)
|
||||
allowed_set = set(allowed_paths)
|
||||
data["items"] = [item for path, item in path_pairs if path in allowed_set]
|
||||
return success(data)
|
||||
|
||||
@@ -86,7 +86,12 @@ export const vfsApi = {
|
||||
thumb: (path: string, w=256, h=256, fit='cover') =>
|
||||
request<ArrayBuffer>(`/fs/thumb/${encodeURI(path.replace(/^\/+/, ''))}?w=${w}&h=${h}&fit=${fit}`),
|
||||
streamUrl: (path: string) => `${API_BASE_URL}/fs/stream/${encodeURI(path.replace(/^\/+/, ''))}`,
|
||||
stat: (path: string) => request(`/fs/stat/${encodeURI(path.replace(/^\/+/, ''))}`),
|
||||
stat: (path: string, options?: { verbose?: boolean }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.verbose) params.set('verbose', 'true');
|
||||
const query = params.toString();
|
||||
return request(`/fs/stat/${encodeURI(path.replace(/^\/+/, ''))}${query ? `?${query}` : ''}`);
|
||||
},
|
||||
getTempLinkToken: (path: string, expiresIn: number = 3600) =>
|
||||
request<{token: string, path: string, url: string}>(`/fs/temp-link/${encodeURI(path.replace(/^\/+/, ''))}?expires_in=${expiresIn}`),
|
||||
getTempPublicUrl: (token: string) => `${API_BASE_URL}/fs/public/${token}`,
|
||||
|
||||
@@ -181,7 +181,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const fullPath = (entryBasePath === '/' ? '' : entryBasePath) + '/' + entry.name;
|
||||
const stat = await vfsApi.stat(fullPath);
|
||||
const stat = await vfsApi.stat(fullPath, { verbose: true });
|
||||
setDetailData(stat as Record<string, unknown>);
|
||||
} catch (error) {
|
||||
const messageText = error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -168,22 +168,19 @@ export function useFileActions({ path, refresh, clearSelection, onShare, onGetDi
|
||||
refresh();
|
||||
}, [normalizeDestination, normalizeFullPath, t, buildEntryDestination, refresh]);
|
||||
|
||||
const doDownload = useCallback(async (entry: VfsEntry) => {
|
||||
const doDownload = useCallback((entry: VfsEntry) => {
|
||||
if (entry.is_dir) {
|
||||
message.warning(t('Downloading folders is not supported'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const buf = await vfsApi.readFile((path === '/' ? '' : path) + '/' + entry.name);
|
||||
const blob = new Blob([buf]);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const url = vfsApi.streamUrl((path === '/' ? '' : path) + '/' + entry.name);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = entry.name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('Download failed'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user