feat: add wildcard glob support to file manager and transfer history search (#5767)

This commit is contained in:
DDSRem
2026-05-13 21:08:51 +08:00
committed by GitHub
parent 7b1ece8b83
commit e5b67438d9
3 changed files with 87 additions and 47 deletions
+26 -9
View File
@@ -235,6 +235,14 @@ async def delete_download_history(
return schemas.Response(success=True) return schemas.Response(success=True)
def _glob_to_like(pattern: str) -> str:
"""
将 glob 通配符模式转换为 SQL LIKE 模式(使用 \\ 作为转义字符)
"""
result = pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
return result.replace("*", "%").replace("?", "_")
@router.get("/transfer", summary="查询整理记录", response_model=schemas.Response) @router.get("/transfer", summary="查询整理记录", response_model=schemas.Response)
async def transfer_history( async def transfer_history(
title: Optional[str] = None, title: Optional[str] = None,
@@ -245,7 +253,7 @@ async def transfer_history(
_: schemas.TokenPayload = Depends(verify_token), _: schemas.TokenPayload = Depends(verify_token),
) -> Any: ) -> Any:
""" """
查询整理记录 查询整理记录,title 支持通配符 * 和 ?(如 *.mkv、*2024*
""" """
if title == "失败": if title == "失败":
title = None title = None
@@ -255,14 +263,23 @@ async def transfer_history(
status = True status = True
if title: if title:
words = jieba.cut(title, HMM=False) if "*" in title or "?" in title:
title = "%".join(words) like_pattern = _glob_to_like(title)
total = await TransferHistory.async_count_by_title( total = await TransferHistory.async_count_by_title(
db, title=title, status=status db, title=like_pattern, status=status, wildcard=True
) )
result = await TransferHistory.async_list_by_title( result = await TransferHistory.async_list_by_title(
db, title=title, page=page, count=count, status=status db, title=like_pattern, page=page, count=count, status=status, wildcard=True
) )
else:
words = jieba.cut(title, HMM=False)
like_pattern = "%".join(words)
total = await TransferHistory.async_count_by_title(
db, title=like_pattern, status=status
)
result = await TransferHistory.async_list_by_title(
db, title=like_pattern, page=page, count=count, status=status
)
else: else:
result = await TransferHistory.async_list_by_page( result = await TransferHistory.async_list_by_page(
db, page=page, count=count, status=status db, page=page, count=count, status=status
+7
View File
@@ -1,4 +1,6 @@
import fnmatch
import math import math
import re
from pathlib import Path from pathlib import Path
from typing import Any, List, Optional from typing import Any, List, Optional
@@ -88,17 +90,22 @@ def reset(name: str, _: User = Depends(get_current_active_superuser)) -> Any:
def list_files( def list_files(
fileitem: schemas.FileItem, fileitem: schemas.FileItem,
sort: Optional[str] = "updated_at", sort: Optional[str] = "updated_at",
keyword: Optional[str] = None,
_: User = Depends(get_current_active_superuser), _: User = Depends(get_current_active_superuser),
) -> Any: ) -> Any:
""" """
查询当前目录下所有目录和文件 查询当前目录下所有目录和文件
:param fileitem: 文件项 :param fileitem: 文件项
:param sort: 排序方式,name:按名称排序,time:按修改时间排序 :param sort: 排序方式,name:按名称排序,time:按修改时间排序
:param keyword: 通配符过滤,支持 * 和 ?,如 *.mkv、movie?.*
:param _: token :param _: token
:return: 所有目录和文件 :return: 所有目录和文件
""" """
file_list = StorageChain().list_files(fileitem) file_list = StorageChain().list_files(fileitem)
if file_list: if file_list:
if keyword:
_pat = re.compile(fnmatch.translate(keyword), re.IGNORECASE)
file_list = [f for f in file_list if _pat.match(f.name or "")]
if sort == "name": if sort == "name":
file_list.sort(key=lambda x: StringUtils.natural_sort_key(x.name or "")) file_list.sort(key=lambda x: StringUtils.natural_sort_key(x.name or ""))
else: else:
+50 -34
View File
@@ -68,21 +68,23 @@ class TransferHistory(Base):
@classmethod @classmethod
@db_query @db_query
def list_by_title(cls, db: Session, title: str, page: Optional[int] = 1, count: Optional[int] = 30, def list_by_title(cls, db: Session, title: str, page: Optional[int] = 1, count: Optional[int] = 30,
status: bool = None): status: bool = None, wildcard: bool = False):
if status is not None: if wildcard:
query = db.query(cls).filter( text_filter = or_(
cls.status == status cls.title.like(title, escape='\\'),
).order_by( cls.src.like(title, escape='\\'),
cls.date.desc() cls.dest.like(title, escape='\\'),
) )
else: else:
query = db.query(cls).filter(or_( text_filter = or_(
cls.title.like(f'%{title}%'), cls.title.like(f'%{title}%'),
cls.src.like(f'%{title}%'), cls.src.like(f'%{title}%'),
cls.dest.like(f'%{title}%'), cls.dest.like(f'%{title}%'),
)).order_by(
cls.date.desc()
) )
query = db.query(cls).filter(text_filter)
if status is not None:
query = query.filter(cls.status == status)
query = query.order_by(cls.date.desc())
# 当count为负数时,不限制页数查询所有 # 当count为负数时,不限制页数查询所有
if count >= 0: if count >= 0:
@@ -93,21 +95,23 @@ class TransferHistory(Base):
@classmethod @classmethod
@async_db_query @async_db_query
async def async_list_by_title(cls, db: AsyncSession, title: str, page: Optional[int] = 1, count: Optional[int] = 30, async def async_list_by_title(cls, db: AsyncSession, title: str, page: Optional[int] = 1, count: Optional[int] = 30,
status: bool = None): status: bool = None, wildcard: bool = False):
if status is not None: if wildcard:
query = select(cls).filter( text_filter = or_(
cls.status == status cls.title.like(title, escape='\\'),
).order_by( cls.src.like(title, escape='\\'),
cls.date.desc() cls.dest.like(title, escape='\\'),
) )
else: else:
query = select(cls).filter(or_( text_filter = or_(
cls.title.like(f'%{title}%'), cls.title.like(f'%{title}%'),
cls.src.like(f'%{title}%'), cls.src.like(f'%{title}%'),
cls.dest.like(f'%{title}%'), cls.dest.like(f'%{title}%'),
)).order_by(
cls.date.desc()
) )
query = select(cls).filter(text_filter)
if status is not None:
query = query.filter(cls.status == status)
query = query.order_by(cls.date.desc())
# 当count为负数时,不限制页数查询所有 # 当count为负数时,不限制页数查询所有
if count >= 0: if count >= 0:
@@ -232,31 +236,43 @@ class TransferHistory(Base):
@classmethod @classmethod
@db_query @db_query
def count_by_title(cls, db: Session, title: str, status: bool = None): def count_by_title(cls, db: Session, title: str, status: bool = None, wildcard: bool = False):
if status is not None: if wildcard:
return db.query(func.count(cls.id)).filter(cls.status == status).first()[0] text_filter = or_(
cls.title.like(title, escape='\\'),
cls.src.like(title, escape='\\'),
cls.dest.like(title, escape='\\'),
)
else: else:
return db.query(func.count(cls.id)).filter(or_( text_filter = or_(
cls.title.like(f'%{title}%'), cls.title.like(f'%{title}%'),
cls.src.like(f'%{title}%'), cls.src.like(f'%{title}%'),
cls.dest.like(f'%{title}%') cls.dest.like(f'%{title}%'),
)).first()[0] )
query = db.query(func.count(cls.id)).filter(text_filter)
if status is not None:
query = query.filter(cls.status == status)
return query.first()[0]
@classmethod @classmethod
@async_db_query @async_db_query
async def async_count_by_title(cls, db: AsyncSession, title: str, status: bool = None): async def async_count_by_title(cls, db: AsyncSession, title: str, status: bool = None, wildcard: bool = False):
if status is not None: if wildcard:
result = await db.execute( text_filter = or_(
select(func.count(cls.id)).filter(cls.status == status) cls.title.like(title, escape='\\'),
cls.src.like(title, escape='\\'),
cls.dest.like(title, escape='\\'),
) )
else: else:
result = await db.execute( text_filter = or_(
select(func.count(cls.id)).filter(or_( cls.title.like(f'%{title}%'),
cls.title.like(f'%{title}%'), cls.src.like(f'%{title}%'),
cls.src.like(f'%{title}%'), cls.dest.like(f'%{title}%'),
cls.dest.like(f'%{title}%')
))
) )
stmt = select(func.count(cls.id)).filter(text_filter)
if status is not None:
stmt = stmt.filter(cls.status == status)
result = await db.execute(stmt)
return result.scalar() return result.scalar()
@classmethod @classmethod