Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: 2
updates:
# Runtime deps (requirements.txt is the single source; once a lockfile lands, keep raising version PRs here)
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
9 changes: 2 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ docs/_build/
.pybuilder/
target/
*.db
./filecodebox.db-shm
./filecodebox.db-wal
*.db-shm
*.db-wal
# Jupyter Notebook
.ipynb_checkpoints

Expand Down Expand Up @@ -147,13 +147,8 @@ cython_debug/
# Project
.vscode
.DS_Store
for_test.py
.html
/evaluate/temp.py
/evaluation/back.json
data/.env
.backup/
/cloc-1.64.exe

# Ignore node_modules
node_modules/
Expand Down
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# First-time setup: pre-commit install
# Behind a proxy: HTTPS_PROXY=http://<proxy-host>:<port> pre-commit run --all-files
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.6
hooks:
- id: ruff-check
args: [--fix]
2 changes: 1 addition & 1 deletion apps/admin/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# @Author : Lan
# @File : depends.py
# @Software: PyCharm
from fastapi import Header, HTTPException, Depends
from fastapi import Header, HTTPException
from fastapi.requests import Request
import base64
import hmac
Expand Down
1 change: 0 additions & 1 deletion apps/base/schemas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from pydantic import BaseModel
from typing import Optional


class SelectFileModel(BaseModel):
Expand Down
40 changes: 15 additions & 25 deletions apps/base/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import datetime
import hashlib
import os
import uuid
from urllib.parse import unquote
Expand All @@ -26,29 +25,32 @@ def validate_expire_style(expire_style: str) -> str:
return expire_style


async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, str, str]:
async def build_file_path(
file_name: str, file_uuid: str
) -> Tuple[str, str, str, str, str]:
"""Single source of storage path generation (date dir + UUID), shared by
regular, chunked, and presigned uploads.

Always use get_now() (UTC+8); do not switch to server-local time.
"""
today = await get_now()
storage_path = settings.storage_path.strip("/")
file_uuid = uuid.uuid4().hex
filename = await sanitize_filename(unquote(file.filename or ""))
filename = await sanitize_filename(unquote(file_name or ""))
base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{file_uuid}"
path = f"{storage_path}/{base_path}" if storage_path else base_path
prefix, suffix = os.path.splitext(filename)
save_path = f"{path}/{filename}"
save_path = f"{path}/{prefix}{suffix}"
return path, suffix, prefix, filename, save_path


async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, str, str]:
return await build_file_path(file.filename or "", uuid.uuid4().hex)


async def get_chunk_file_path_name(
file_name: str, upload_id: str
) -> Tuple[str, str, str, str, str]:
today = await get_now()
storage_path = settings.storage_path.strip("/")
file_name = await sanitize_filename(unquote(file_name or ""))
base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{upload_id}"
path = f"{storage_path}/{base_path}" if storage_path else base_path
prefix, suffix = os.path.splitext(file_name)
save_path = f"{path}/{prefix}{suffix}"
return path, suffix, prefix, file_name, save_path
return await build_file_path(file_name, upload_id)


async def get_expire_info(
Expand Down Expand Up @@ -121,18 +123,6 @@ async def get_random_code(style: str | None = None) -> str:
return str(code)


async def calculate_file_hash(file: UploadFile, chunk_size=1024 * 1024) -> str:
sha = hashlib.sha256()
await file.seek(0)
while True:
chunk = await file.read(chunk_size)
if not chunk:
break
sha.update(chunk)
await file.seek(0)
return sha.hexdigest()


ip_limit = {
"error": IPRateLimit(count=settings.errorCount, minutes=settings.errorMinute),
"metadata": IPRateLimit(count=settings.errorCount, minutes=settings.errorMinute),
Expand Down
46 changes: 12 additions & 34 deletions apps/base/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import datetime
import hashlib
import os
import uuid
Expand All @@ -16,6 +15,7 @@
from apps.admin.dependencies import share_required_login
from apps.base.models import FileCodes, UploadChunk, PresignUploadSession
from apps.base.quota import release_storage, reserve_storage
from core.logger import logger
from apps.base.schemas import (
SelectFileModel,
InitChunkUploadModel,
Expand All @@ -24,6 +24,7 @@
)
from apps.base.file_validation import validate_file_type, validate_upload_file, validate_header_bytes
from apps.base.utils import (
build_file_path,
get_expire_info,
get_file_path_name,
ip_limit,
Expand Down Expand Up @@ -51,16 +52,8 @@ class FileUploadService:
async def generate_file_path(
file_name: str, upload_id: Optional[str] = None
) -> tuple[str, str, str, str, str]:
"""统一的路径生成"""
today = datetime.datetime.now()
storage_path = settings.storage_path.strip("/")
file_uuid = upload_id or uuid.uuid4().hex
filename = await sanitize_filename(unquote(file_name))
base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{file_uuid}"
path = f"{storage_path}/{base_path}" if storage_path else base_path
prefix, suffix = os.path.splitext(filename)
save_path = f"{path}/{filename}"
return path, suffix, prefix, filename, save_path
"""Delegates path generation to apps.base.utils.build_file_path."""
return await build_file_path(file_name, upload_id or uuid.uuid4().hex)

@staticmethod
async def create_file_record(
Expand Down Expand Up @@ -186,7 +179,7 @@ async def share_file(
FileCodes(file_path=path, uuid_file_name=uuid_file_name)
)
except Exception:
pass
logger.warning("分享上传:记录创建失败,回滚删除已保存文件失败", exc_info=True)
raise
finally:
await release_storage(reservation_token)
Expand Down Expand Up @@ -404,21 +397,6 @@ async def init_chunk_upload(data: InitChunkUploadModel = Depends(parse_init_chun
status_code=403, detail=f"文件大小超过限制,最大为 {max_size_mb:.2f} MB"
)

# # 秒传检查
# existing = await FileCodes.filter(file_hash=data.file_hash).first()
# if existing:
# if await existing.is_expired():
# file_storage: FileStorageInterface = storages[settings.file_storage](
# )
# await file_storage.delete_file(existing)
# await existing.delete()
# else:
# return APIResponse(detail={
# "code": existing.code,
# "existed": True,
# "name": f'{existing.prefix}{existing.suffix}'
# })

# 断点续传:检查是否存在相同文件的未完成上传会话
existing_session = await UploadChunk.filter(
chunk_hash=data.file_hash,
Expand Down Expand Up @@ -591,8 +569,8 @@ async def cancel_upload(upload_id: str):
if save_path:
try:
await storage.clean_chunks(upload_id, save_path)
except Exception as e:
pass
except Exception:
logger.warning("取消分片上传:清理分片文件失败 upload_id=%s", upload_id, exc_info=True)

# 清理数据库记录
await UploadChunk.filter(upload_id=upload_id).delete()
Expand Down Expand Up @@ -663,7 +641,7 @@ async def complete_upload(
try:
await storage.clean_chunks(upload_id, save_path)
except Exception:
pass
logger.warning("分片超限中止:清理分片文件失败 upload_id=%s", upload_id, exc_info=True)
await UploadChunk.filter(upload_id=upload_id).delete()
await release_storage(f"chunk:{upload_id}")
max_size_mb = settings.uploadSize / (1024 * 1024)
Expand Down Expand Up @@ -711,7 +689,7 @@ async def complete_upload(
try:
await storage.clean_chunks(upload_id, save_path)
except Exception:
pass
logger.warning("分片合并失败:清理临时分片文件失败 upload_id=%s", upload_id, exc_info=True)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"文件合并失败: {str(e)}"
)
Expand Down Expand Up @@ -847,7 +825,7 @@ async def presign_upload_proxy(
)
)
except Exception:
pass
logger.warning("预签名代理上传:记录创建失败,回滚删除已保存文件失败 upload_id=%s", session.upload_id, exc_info=True)
raise

await session.delete()
Expand Down Expand Up @@ -904,7 +882,7 @@ async def presign_upload_confirm(upload_id: str, ip: str = Depends(ip_limit["upl
)
)
except Exception:
pass
logger.warning("预签名确认:记录创建失败,回滚删除已保存文件失败 upload_id=%s", session.upload_id, exc_info=True)
raise

await session.delete()
Expand Down Expand Up @@ -952,7 +930,7 @@ async def presign_upload_cancel(upload_id: str):
)
await storage.delete_file(temp_file_code)
except Exception:
pass
logger.warning("取消预签名会话:清理临时文件失败 upload_id=%s", upload_id, exc_info=True)

await session.delete()
await release_storage(f"presign:{upload_id}")
Expand Down
135 changes: 0 additions & 135 deletions docs/guide/storage-onedrive.md

This file was deleted.

Loading