Skip to content

Commit e5b6743

Browse files
authored
feat: add wildcard glob support to file manager and transfer history search (#5767)
1 parent 7b1ece8 commit e5b6743

3 files changed

Lines changed: 87 additions & 47 deletions

File tree

app/api/endpoints/history.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,14 @@ async def delete_download_history(
235235
return schemas.Response(success=True)
236236

237237

238+
def _glob_to_like(pattern: str) -> str:
239+
"""
240+
将 glob 通配符模式转换为 SQL LIKE 模式(使用 \\ 作为转义字符)
241+
"""
242+
result = pattern.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
243+
return result.replace("*", "%").replace("?", "_")
244+
245+
238246
@router.get("/transfer", summary="查询整理记录", response_model=schemas.Response)
239247
async def transfer_history(
240248
title: Optional[str] = None,
@@ -245,7 +253,7 @@ async def transfer_history(
245253
_: schemas.TokenPayload = Depends(verify_token),
246254
) -> Any:
247255
"""
248-
查询整理记录
256+
查询整理记录,title 支持通配符 * 和 ?(如 *.mkv、*2024*)
249257
"""
250258
if title == "失败":
251259
title = None
@@ -255,14 +263,23 @@ async def transfer_history(
255263
status = True
256264

257265
if title:
258-
words = jieba.cut(title, HMM=False)
259-
title = "%".join(words)
260-
total = await TransferHistory.async_count_by_title(
261-
db, title=title, status=status
262-
)
263-
result = await TransferHistory.async_list_by_title(
264-
db, title=title, page=page, count=count, status=status
265-
)
266+
if "*" in title or "?" in title:
267+
like_pattern = _glob_to_like(title)
268+
total = await TransferHistory.async_count_by_title(
269+
db, title=like_pattern, status=status, wildcard=True
270+
)
271+
result = await TransferHistory.async_list_by_title(
272+
db, title=like_pattern, page=page, count=count, status=status, wildcard=True
273+
)
274+
else:
275+
words = jieba.cut(title, HMM=False)
276+
like_pattern = "%".join(words)
277+
total = await TransferHistory.async_count_by_title(
278+
db, title=like_pattern, status=status
279+
)
280+
result = await TransferHistory.async_list_by_title(
281+
db, title=like_pattern, page=page, count=count, status=status
282+
)
266283
else:
267284
result = await TransferHistory.async_list_by_page(
268285
db, page=page, count=count, status=status

app/api/endpoints/storage.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import fnmatch
12
import math
3+
import re
24
from pathlib import Path
35
from typing import Any, List, Optional
46

@@ -88,17 +90,22 @@ def reset(name: str, _: User = Depends(get_current_active_superuser)) -> Any:
8890
def list_files(
8991
fileitem: schemas.FileItem,
9092
sort: Optional[str] = "updated_at",
93+
keyword: Optional[str] = None,
9194
_: User = Depends(get_current_active_superuser),
9295
) -> Any:
9396
"""
9497
查询当前目录下所有目录和文件
9598
:param fileitem: 文件项
9699
:param sort: 排序方式,name:按名称排序,time:按修改时间排序
100+
:param keyword: 通配符过滤,支持 * 和 ?,如 *.mkv、movie?.*
97101
:param _: token
98102
:return: 所有目录和文件
99103
"""
100104
file_list = StorageChain().list_files(fileitem)
101105
if file_list:
106+
if keyword:
107+
_pat = re.compile(fnmatch.translate(keyword), re.IGNORECASE)
108+
file_list = [f for f in file_list if _pat.match(f.name or "")]
102109
if sort == "name":
103110
file_list.sort(key=lambda x: StringUtils.natural_sort_key(x.name or ""))
104111
else:

app/db/models/transferhistory.py

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -68,51 +68,55 @@ class TransferHistory(Base):
6868
@classmethod
6969
@db_query
7070
def list_by_title(cls, db: Session, title: str, page: Optional[int] = 1, count: Optional[int] = 30,
71-
status: bool = None):
72-
if status is not None:
73-
query = db.query(cls).filter(
74-
cls.status == status
75-
).order_by(
76-
cls.date.desc()
71+
status: bool = None, wildcard: bool = False):
72+
if wildcard:
73+
text_filter = or_(
74+
cls.title.like(title, escape='\\'),
75+
cls.src.like(title, escape='\\'),
76+
cls.dest.like(title, escape='\\'),
7777
)
7878
else:
79-
query = db.query(cls).filter(or_(
79+
text_filter = or_(
8080
cls.title.like(f'%{title}%'),
8181
cls.src.like(f'%{title}%'),
8282
cls.dest.like(f'%{title}%'),
83-
)).order_by(
84-
cls.date.desc()
8583
)
86-
84+
query = db.query(cls).filter(text_filter)
85+
if status is not None:
86+
query = query.filter(cls.status == status)
87+
query = query.order_by(cls.date.desc())
88+
8789
# 当count为负数时,不限制页数查询所有
8890
if count >= 0:
8991
query = query.offset((page - 1) * count).limit(count)
90-
92+
9193
return query.all()
9294

9395
@classmethod
9496
@async_db_query
9597
async def async_list_by_title(cls, db: AsyncSession, title: str, page: Optional[int] = 1, count: Optional[int] = 30,
96-
status: bool = None):
97-
if status is not None:
98-
query = select(cls).filter(
99-
cls.status == status
100-
).order_by(
101-
cls.date.desc()
98+
status: bool = None, wildcard: bool = False):
99+
if wildcard:
100+
text_filter = or_(
101+
cls.title.like(title, escape='\\'),
102+
cls.src.like(title, escape='\\'),
103+
cls.dest.like(title, escape='\\'),
102104
)
103105
else:
104-
query = select(cls).filter(or_(
106+
text_filter = or_(
105107
cls.title.like(f'%{title}%'),
106108
cls.src.like(f'%{title}%'),
107109
cls.dest.like(f'%{title}%'),
108-
)).order_by(
109-
cls.date.desc()
110110
)
111-
111+
query = select(cls).filter(text_filter)
112+
if status is not None:
113+
query = query.filter(cls.status == status)
114+
query = query.order_by(cls.date.desc())
115+
112116
# 当count为负数时,不限制页数查询所有
113117
if count >= 0:
114118
query = query.offset((page - 1) * count).limit(count)
115-
119+
116120
result = await db.execute(query)
117121
return result.scalars().all()
118122

@@ -232,31 +236,43 @@ async def async_count(cls, db: AsyncSession, status: bool = None):
232236

233237
@classmethod
234238
@db_query
235-
def count_by_title(cls, db: Session, title: str, status: bool = None):
236-
if status is not None:
237-
return db.query(func.count(cls.id)).filter(cls.status == status).first()[0]
239+
def count_by_title(cls, db: Session, title: str, status: bool = None, wildcard: bool = False):
240+
if wildcard:
241+
text_filter = or_(
242+
cls.title.like(title, escape='\\'),
243+
cls.src.like(title, escape='\\'),
244+
cls.dest.like(title, escape='\\'),
245+
)
238246
else:
239-
return db.query(func.count(cls.id)).filter(or_(
247+
text_filter = or_(
240248
cls.title.like(f'%{title}%'),
241249
cls.src.like(f'%{title}%'),
242-
cls.dest.like(f'%{title}%')
243-
)).first()[0]
250+
cls.dest.like(f'%{title}%'),
251+
)
252+
query = db.query(func.count(cls.id)).filter(text_filter)
253+
if status is not None:
254+
query = query.filter(cls.status == status)
255+
return query.first()[0]
244256

245257
@classmethod
246258
@async_db_query
247-
async def async_count_by_title(cls, db: AsyncSession, title: str, status: bool = None):
248-
if status is not None:
249-
result = await db.execute(
250-
select(func.count(cls.id)).filter(cls.status == status)
259+
async def async_count_by_title(cls, db: AsyncSession, title: str, status: bool = None, wildcard: bool = False):
260+
if wildcard:
261+
text_filter = or_(
262+
cls.title.like(title, escape='\\'),
263+
cls.src.like(title, escape='\\'),
264+
cls.dest.like(title, escape='\\'),
251265
)
252266
else:
253-
result = await db.execute(
254-
select(func.count(cls.id)).filter(or_(
255-
cls.title.like(f'%{title}%'),
256-
cls.src.like(f'%{title}%'),
257-
cls.dest.like(f'%{title}%')
258-
))
267+
text_filter = or_(
268+
cls.title.like(f'%{title}%'),
269+
cls.src.like(f'%{title}%'),
270+
cls.dest.like(f'%{title}%'),
259271
)
272+
stmt = select(func.count(cls.id)).filter(text_filter)
273+
if status is not None:
274+
stmt = stmt.filter(cls.status == status)
275+
result = await db.execute(stmt)
260276
return result.scalar()
261277

262278
@classmethod

0 commit comments

Comments
 (0)