Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -8,6 +8,7 @@ from apps.base.models import FileCodes, KeyValue
|
|||||||
from apps.base.utils import get_expire_info, get_file_path_name
|
from apps.base.utils import get_expire_info, get_file_path_name
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from core.settings import data_root
|
from core.settings import data_root
|
||||||
|
from core.utils import hash_password, is_password_hashed
|
||||||
|
|
||||||
|
|
||||||
class FileService:
|
class FileService:
|
||||||
@@ -76,6 +77,9 @@ class ConfigService:
|
|||||||
if admin_token is None or admin_token == "":
|
if admin_token is None or admin_token == "":
|
||||||
raise HTTPException(status_code=400, detail="管理员密码不能为空")
|
raise HTTPException(status_code=400, detail="管理员密码不能为空")
|
||||||
|
|
||||||
|
if not is_password_hashed(admin_token):
|
||||||
|
data["admin_token"] = hash_password(admin_token)
|
||||||
|
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if key not in settings.default_config:
|
if key not in settings.default_config:
|
||||||
continue
|
continue
|
||||||
|
|||||||
+34
-34
@@ -17,17 +17,16 @@ from core.response import APIResponse
|
|||||||
from apps.base.models import FileCodes, KeyValue
|
from apps.base.models import FileCodes, KeyValue
|
||||||
from apps.admin.dependencies import create_token
|
from apps.admin.dependencies import create_token
|
||||||
from core.settings import settings
|
from core.settings import settings
|
||||||
|
from core.utils import get_now, verify_password
|
||||||
|
|
||||||
admin_api = APIRouter(prefix="/admin", tags=["管理"])
|
admin_api = APIRouter(prefix="/admin", tags=["管理"])
|
||||||
|
|
||||||
|
|
||||||
@admin_api.post("/login")
|
@admin_api.post("/login")
|
||||||
async def login(data: LoginData):
|
async def login(data: LoginData):
|
||||||
# 验证管理员密码
|
if not verify_password(data.password, settings.admin_token):
|
||||||
if data.password != settings.admin_token:
|
|
||||||
raise HTTPException(status_code=401, detail="密码错误")
|
raise HTTPException(status_code=401, detail="密码错误")
|
||||||
|
|
||||||
# 生成包含管理员身份的token
|
|
||||||
token = create_token({"is_admin": True})
|
token = create_token({"is_admin": True})
|
||||||
return APIResponse(detail={"token": token, "token_type": "Bearer"})
|
return APIResponse(detail={"token": token, "token_type": "Bearer"})
|
||||||
|
|
||||||
@@ -37,16 +36,13 @@ async def dashboard(admin: bool = Depends(admin_required)):
|
|||||||
all_codes = await FileCodes.all()
|
all_codes = await FileCodes.all()
|
||||||
all_size = str(sum([code.size for code in all_codes]))
|
all_size = str(sum([code.size for code in all_codes]))
|
||||||
sys_start = await KeyValue.filter(key="sys_start").first()
|
sys_start = await KeyValue.filter(key="sys_start").first()
|
||||||
# 获取当前日期时间
|
now = await get_now()
|
||||||
now = datetime.datetime.now()
|
|
||||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
yesterday_start = today_start - datetime.timedelta(days=1)
|
yesterday_start = today_start - datetime.timedelta(days=1)
|
||||||
yesterday_end = today_start - datetime.timedelta(microseconds=1)
|
yesterday_end = today_start - datetime.timedelta(microseconds=1)
|
||||||
# 统计昨天一整天的记录数(从昨天0点到23:59:59)
|
|
||||||
yesterday_codes = FileCodes.filter(
|
yesterday_codes = FileCodes.filter(
|
||||||
created_at__gte=yesterday_start, created_at__lte=yesterday_end
|
created_at__gte=yesterday_start, created_at__lte=yesterday_end
|
||||||
)
|
)
|
||||||
# 统计今天到现在的记录数(从今天0点到现在)
|
|
||||||
today_codes = FileCodes.filter(created_at__gte=today_start)
|
today_codes = FileCodes.filter(created_at__gte=today_start)
|
||||||
return APIResponse(
|
return APIResponse(
|
||||||
detail={
|
detail={
|
||||||
@@ -63,9 +59,9 @@ async def dashboard(admin: bool = Depends(admin_required)):
|
|||||||
|
|
||||||
@admin_api.delete("/file/delete")
|
@admin_api.delete("/file/delete")
|
||||||
async def file_delete(
|
async def file_delete(
|
||||||
data: IDData,
|
data: IDData,
|
||||||
file_service: FileService = Depends(get_file_service),
|
file_service: FileService = Depends(get_file_service),
|
||||||
admin: bool = Depends(admin_required),
|
admin: bool = Depends(admin_required),
|
||||||
):
|
):
|
||||||
await file_service.delete_file(data.id)
|
await file_service.delete_file(data.id)
|
||||||
return APIResponse()
|
return APIResponse()
|
||||||
@@ -73,11 +69,11 @@ async def file_delete(
|
|||||||
|
|
||||||
@admin_api.get("/file/list")
|
@admin_api.get("/file/list")
|
||||||
async def file_list(
|
async def file_list(
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
size: int = 10,
|
size: int = 10,
|
||||||
keyword: str = "",
|
keyword: str = "",
|
||||||
file_service: FileService = Depends(get_file_service),
|
file_service: FileService = Depends(get_file_service),
|
||||||
admin: bool = Depends(admin_required),
|
admin: bool = Depends(admin_required),
|
||||||
):
|
):
|
||||||
files, total = await file_service.list_files(page, size, keyword)
|
files, total = await file_service.list_files(page, size, keyword)
|
||||||
return APIResponse(
|
return APIResponse(
|
||||||
@@ -92,17 +88,17 @@ async def file_list(
|
|||||||
|
|
||||||
@admin_api.get("/config/get")
|
@admin_api.get("/config/get")
|
||||||
async def get_config(
|
async def get_config(
|
||||||
config_service: ConfigService = Depends(get_config_service),
|
config_service: ConfigService = Depends(get_config_service),
|
||||||
admin: bool = Depends(admin_required),
|
admin: bool = Depends(admin_required),
|
||||||
):
|
):
|
||||||
return APIResponse(detail=config_service.get_config())
|
return APIResponse(detail=config_service.get_config())
|
||||||
|
|
||||||
|
|
||||||
@admin_api.patch("/config/update")
|
@admin_api.patch("/config/update")
|
||||||
async def update_config(
|
async def update_config(
|
||||||
data: dict,
|
data: dict,
|
||||||
config_service: ConfigService = Depends(get_config_service),
|
config_service: ConfigService = Depends(get_config_service),
|
||||||
admin: bool = Depends(admin_required),
|
admin: bool = Depends(admin_required),
|
||||||
):
|
):
|
||||||
data.pop("themesChoices")
|
data.pop("themesChoices")
|
||||||
await config_service.update_config(data)
|
await config_service.update_config(data)
|
||||||
@@ -111,9 +107,9 @@ async def update_config(
|
|||||||
|
|
||||||
@admin_api.get("/file/download")
|
@admin_api.get("/file/download")
|
||||||
async def file_download(
|
async def file_download(
|
||||||
id: int,
|
id: int,
|
||||||
file_service: FileService = Depends(get_file_service),
|
file_service: FileService = Depends(get_file_service),
|
||||||
admin: bool = Depends(admin_required),
|
admin: bool = Depends(admin_required),
|
||||||
):
|
):
|
||||||
file_content = await file_service.download_file(id)
|
file_content = await file_service.download_file(id)
|
||||||
return file_content
|
return file_content
|
||||||
@@ -121,8 +117,8 @@ async def file_download(
|
|||||||
|
|
||||||
@admin_api.get("/local/lists")
|
@admin_api.get("/local/lists")
|
||||||
async def get_local_lists(
|
async def get_local_lists(
|
||||||
local_file_service: LocalFileService = Depends(get_local_file_service),
|
local_file_service: LocalFileService = Depends(get_local_file_service),
|
||||||
admin: bool = Depends(admin_required),
|
admin: bool = Depends(admin_required),
|
||||||
):
|
):
|
||||||
files = await local_file_service.list_files()
|
files = await local_file_service.list_files()
|
||||||
return APIResponse(detail=files)
|
return APIResponse(detail=files)
|
||||||
@@ -130,9 +126,9 @@ async def get_local_lists(
|
|||||||
|
|
||||||
@admin_api.delete("/local/delete")
|
@admin_api.delete("/local/delete")
|
||||||
async def delete_local_file(
|
async def delete_local_file(
|
||||||
item: DeleteItem,
|
item: DeleteItem,
|
||||||
local_file_service: LocalFileService = Depends(get_local_file_service),
|
local_file_service: LocalFileService = Depends(get_local_file_service),
|
||||||
admin: bool = Depends(admin_required),
|
admin: bool = Depends(admin_required),
|
||||||
):
|
):
|
||||||
result = await local_file_service.delete_file(item.filename)
|
result = await local_file_service.delete_file(item.filename)
|
||||||
return APIResponse(detail=result)
|
return APIResponse(detail=result)
|
||||||
@@ -140,9 +136,9 @@ async def delete_local_file(
|
|||||||
|
|
||||||
@admin_api.post("/local/share")
|
@admin_api.post("/local/share")
|
||||||
async def share_local_file(
|
async def share_local_file(
|
||||||
item: ShareItem,
|
item: ShareItem,
|
||||||
file_service: FileService = Depends(get_file_service),
|
file_service: FileService = Depends(get_file_service),
|
||||||
admin: bool = Depends(admin_required),
|
admin: bool = Depends(admin_required),
|
||||||
):
|
):
|
||||||
share_info = await file_service.share_local_file(item)
|
share_info = await file_service.share_local_file(item)
|
||||||
return APIResponse(detail=share_info)
|
return APIResponse(detail=share_info)
|
||||||
@@ -150,8 +146,8 @@ async def share_local_file(
|
|||||||
|
|
||||||
@admin_api.patch("/file/update")
|
@admin_api.patch("/file/update")
|
||||||
async def update_file(
|
async def update_file(
|
||||||
data: UpdateFileData,
|
data: UpdateFileData,
|
||||||
admin: bool = Depends(admin_required),
|
admin: bool = Depends(admin_required),
|
||||||
):
|
):
|
||||||
file_code = await FileCodes.filter(id=data.id).first()
|
file_code = await FileCodes.filter(id=data.id).first()
|
||||||
if not file_code:
|
if not file_code:
|
||||||
@@ -167,7 +163,11 @@ async def update_file(
|
|||||||
update_data["prefix"] = data.prefix
|
update_data["prefix"] = data.prefix
|
||||||
if data.suffix is not None and data.suffix != file_code.suffix:
|
if data.suffix is not None and data.suffix != file_code.suffix:
|
||||||
update_data["suffix"] = data.suffix
|
update_data["suffix"] = data.suffix
|
||||||
if data.expired_at is not None and data.expired_at != "" and data.expired_at != file_code.expired_at:
|
if (
|
||||||
|
data.expired_at is not None
|
||||||
|
and data.expired_at != ""
|
||||||
|
and data.expired_at != file_code.expired_at
|
||||||
|
):
|
||||||
update_data["expired_at"] = data.expired_at
|
update_data["expired_at"] = data.expired_at
|
||||||
if data.expired_count is not None and data.expired_count != file_code.expired_count:
|
if data.expired_count is not None and data.expired_count != file_code.expired_count:
|
||||||
update_data["expired_count"] = data.expired_count
|
update_data["expired_count"] = data.expired_count
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from tortoise import connections
|
||||||
|
|
||||||
|
|
||||||
|
async def add_save_path_to_uploadchunk():
|
||||||
|
conn = connections.get("default")
|
||||||
|
await conn.execute_script(
|
||||||
|
"""
|
||||||
|
ALTER TABLE uploadchunk ADD COLUMN save_path VARCHAR(512) NULL;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def migrate():
|
||||||
|
await add_save_path_to_uploadchunk()
|
||||||
+5
-1
@@ -49,6 +49,7 @@ class UploadChunk(models.Model):
|
|||||||
file_size = fields.BigIntField()
|
file_size = fields.BigIntField()
|
||||||
chunk_size = fields.IntField()
|
chunk_size = fields.IntField()
|
||||||
file_name = fields.CharField(max_length=255)
|
file_name = fields.CharField(max_length=255)
|
||||||
|
save_path = fields.CharField(max_length=512, null=True)
|
||||||
created_at = fields.DatetimeField(auto_now_add=True)
|
created_at = fields.DatetimeField(auto_now_add=True)
|
||||||
completed = fields.BooleanField(default=False)
|
completed = fields.BooleanField(default=False)
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ class KeyValue(Model):
|
|||||||
|
|
||||||
class PresignUploadSession(models.Model):
|
class PresignUploadSession(models.Model):
|
||||||
"""预签名上传会话模型"""
|
"""预签名上传会话模型"""
|
||||||
|
|
||||||
id = fields.IntField(pk=True)
|
id = fields.IntField(pk=True)
|
||||||
upload_id = fields.CharField(max_length=36, unique=True, index=True)
|
upload_id = fields.CharField(max_length=36, unique=True, index=True)
|
||||||
file_name = fields.CharField(max_length=255)
|
file_name = fields.CharField(max_length=255)
|
||||||
@@ -85,4 +87,6 @@ class PresignUploadSession(models.Model):
|
|||||||
file_codes_pydantic = pydantic_model_creator(FileCodes, name="FileCodes")
|
file_codes_pydantic = pydantic_model_creator(FileCodes, name="FileCodes")
|
||||||
upload_chunk_pydantic = pydantic_model_creator(UploadChunk, name="UploadChunk")
|
upload_chunk_pydantic = pydantic_model_creator(UploadChunk, name="UploadChunk")
|
||||||
key_value_pydantic = pydantic_model_creator(KeyValue, name="KeyValue")
|
key_value_pydantic = pydantic_model_creator(KeyValue, name="KeyValue")
|
||||||
presign_upload_session_pydantic = pydantic_model_creator(PresignUploadSession, name="PresignUploadSession")
|
presign_upload_session_pydantic = pydantic_model_creator(
|
||||||
|
PresignUploadSession, name="PresignUploadSession"
|
||||||
|
)
|
||||||
|
|||||||
+23
-21
@@ -10,30 +10,32 @@ from typing import Optional, Tuple
|
|||||||
from apps.base.dependencies import IPRateLimit
|
from apps.base.dependencies import IPRateLimit
|
||||||
from apps.base.models import FileCodes
|
from apps.base.models import FileCodes
|
||||||
from core.settings import settings
|
from core.settings import settings
|
||||||
from core.utils import get_random_num, get_random_string, max_save_times_desc, sanitize_filename
|
from core.utils import (
|
||||||
|
get_random_num,
|
||||||
|
get_random_string,
|
||||||
|
max_save_times_desc,
|
||||||
|
sanitize_filename,
|
||||||
|
get_now,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, str, str]:
|
async def get_file_path_name(file: UploadFile) -> Tuple[str, str, str, str, str]:
|
||||||
"""获取文件路径和文件名"""
|
today = await get_now()
|
||||||
today = datetime.datetime.now()
|
storage_path = settings.storage_path.strip("/")
|
||||||
storage_path = settings.storage_path.strip("/") # 移除开头和结尾的斜杠
|
|
||||||
file_uuid = uuid.uuid4().hex
|
file_uuid = uuid.uuid4().hex
|
||||||
# 一些客户端对非ASCII字符会进行编码,需要解码
|
filename = await sanitize_filename(unquote(file.filename or ""))
|
||||||
filename = await sanitize_filename(unquote(file.filename))
|
|
||||||
# 使用 UUID 作为子目录名
|
|
||||||
base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{file_uuid}"
|
base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{file_uuid}"
|
||||||
# 如果设置了存储路径,将其添加到基础路径中
|
|
||||||
path = f"{storage_path}/{base_path}" if storage_path else base_path
|
path = f"{storage_path}/{base_path}" if storage_path else base_path
|
||||||
prefix, suffix = os.path.splitext(filename)
|
prefix, suffix = os.path.splitext(filename)
|
||||||
# 保持原始文件名
|
|
||||||
save_path = f"{path}/{filename}"
|
save_path = f"{path}/{filename}"
|
||||||
return path, suffix, prefix, filename, save_path
|
return path, suffix, prefix, filename, save_path
|
||||||
|
|
||||||
|
|
||||||
async def get_chunk_file_path_name(file_name: str, upload_id: str) -> Tuple[str, str, str, str, str]:
|
async def get_chunk_file_path_name(
|
||||||
"""获取切片文件的路径和文件名"""
|
file_name: str, upload_id: str
|
||||||
today = datetime.datetime.now()
|
) -> Tuple[str, str, str, str, str]:
|
||||||
storage_path = settings.storage_path.strip("/") # 移除开头和结尾的斜杠
|
today = await get_now()
|
||||||
|
storage_path = settings.storage_path.strip("/")
|
||||||
base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{upload_id}"
|
base_path = f"share/data/{today.strftime('%Y/%m/%d')}/{upload_id}"
|
||||||
path = f"{storage_path}/{base_path}" if storage_path else base_path
|
path = f"{storage_path}/{base_path}" if storage_path else base_path
|
||||||
prefix, suffix = os.path.splitext(file_name)
|
prefix, suffix = os.path.splitext(file_name)
|
||||||
@@ -41,10 +43,11 @@ async def get_chunk_file_path_name(file_name: str, upload_id: str) -> Tuple[str,
|
|||||||
return path, suffix, prefix, file_name, save_path
|
return path, suffix, prefix, file_name, save_path
|
||||||
|
|
||||||
|
|
||||||
async def get_expire_info(expire_value: int, expire_style: str) -> Tuple[Optional[datetime.datetime], int, int, str]:
|
async def get_expire_info(
|
||||||
"""获取过期信息"""
|
expire_value: int, expire_style: str
|
||||||
|
) -> Tuple[Optional[datetime.datetime], int, int, str]:
|
||||||
expired_count, used_count = -1, 0
|
expired_count, used_count = -1, 0
|
||||||
now = datetime.datetime.now()
|
now = await get_now()
|
||||||
code = None
|
code = None
|
||||||
|
|
||||||
max_timedelta = (
|
max_timedelta = (
|
||||||
@@ -64,7 +67,7 @@ async def get_expire_info(expire_value: int, expire_style: str) -> Tuple[Optiona
|
|||||||
"hour": lambda: now + datetime.timedelta(hours=expire_value),
|
"hour": lambda: now + datetime.timedelta(hours=expire_value),
|
||||||
"minute": lambda: now + datetime.timedelta(minutes=expire_value),
|
"minute": lambda: now + datetime.timedelta(minutes=expire_value),
|
||||||
"count": lambda: (now + datetime.timedelta(days=1), expire_value),
|
"count": lambda: (now + datetime.timedelta(days=1), expire_value),
|
||||||
"forever": lambda: (None, None), # 修改这里
|
"forever": lambda: (None, None),
|
||||||
}
|
}
|
||||||
|
|
||||||
if expire_style in expire_styles:
|
if expire_style in expire_styles:
|
||||||
@@ -74,7 +77,7 @@ async def get_expire_info(expire_value: int, expire_style: str) -> Tuple[Optiona
|
|||||||
if expire_style == "count":
|
if expire_style == "count":
|
||||||
expired_count = extra
|
expired_count = extra
|
||||||
elif expire_style == "forever":
|
elif expire_style == "forever":
|
||||||
code = await get_random_code(style="string") # 移动到这里
|
code = await get_random_code(style="string")
|
||||||
else:
|
else:
|
||||||
expired_at = result
|
expired_at = result
|
||||||
if expired_at and expired_at - now > max_timedelta:
|
if expired_at and expired_at - now > max_timedelta:
|
||||||
@@ -88,12 +91,11 @@ async def get_expire_info(expire_value: int, expire_style: str) -> Tuple[Optiona
|
|||||||
return expired_at, expired_count, used_count, code
|
return expired_at, expired_count, used_count, code
|
||||||
|
|
||||||
|
|
||||||
async def get_random_code(style="num") -> str:
|
async def get_random_code(style: str = "num") -> str:
|
||||||
"""获取随机字符串"""
|
|
||||||
while True:
|
while True:
|
||||||
code = await get_random_num() if style == "num" else await get_random_string()
|
code = await get_random_num() if style == "num" else await get_random_string()
|
||||||
if not await FileCodes.filter(code=code).exists():
|
if not await FileCodes.filter(code=code).exists():
|
||||||
return code
|
return str(code)
|
||||||
|
|
||||||
|
|
||||||
async def calculate_file_hash(file: UploadFile, chunk_size=1024 * 1024) -> str:
|
async def calculate_file_hash(file: UploadFile, chunk_size=1024 * 1024) -> str:
|
||||||
|
|||||||
+200
-129
@@ -5,13 +5,25 @@ import uuid
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from typing import Optional, Tuple, Union
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, UploadFile, File, Depends, HTTPException
|
from fastapi import APIRouter, Form, UploadFile, File, Depends, HTTPException
|
||||||
from starlette import status
|
from starlette import status
|
||||||
|
|
||||||
from apps.admin.dependencies import share_required_login
|
from apps.admin.dependencies import share_required_login
|
||||||
from apps.base.models import FileCodes, UploadChunk, PresignUploadSession
|
from apps.base.models import FileCodes, UploadChunk, PresignUploadSession
|
||||||
from apps.base.schemas import SelectFileModel, InitChunkUploadModel, CompleteUploadModel, PresignUploadInitRequest
|
from apps.base.schemas import (
|
||||||
from apps.base.utils import get_expire_info, get_file_path_name, ip_limit, get_chunk_file_path_name
|
SelectFileModel,
|
||||||
|
InitChunkUploadModel,
|
||||||
|
CompleteUploadModel,
|
||||||
|
PresignUploadInitRequest,
|
||||||
|
)
|
||||||
|
from apps.base.utils import (
|
||||||
|
get_expire_info,
|
||||||
|
get_file_path_name,
|
||||||
|
ip_limit,
|
||||||
|
get_chunk_file_path_name,
|
||||||
|
)
|
||||||
from core.response import APIResponse
|
from core.response import APIResponse
|
||||||
from core.settings import settings
|
from core.settings import settings
|
||||||
from core.storage import storages, FileStorageInterface
|
from core.storage import storages, FileStorageInterface
|
||||||
@@ -25,7 +37,9 @@ class FileUploadService:
|
|||||||
"""统一的文件上传服务"""
|
"""统一的文件上传服务"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def generate_file_path(file_name: str, upload_id: str = None) -> tuple[str, str, str, str, str]:
|
async def generate_file_path(
|
||||||
|
file_name: str, upload_id: Optional[str] = None
|
||||||
|
) -> tuple[str, str, str, str, str]:
|
||||||
"""统一的路径生成"""
|
"""统一的路径生成"""
|
||||||
today = datetime.datetime.now()
|
today = datetime.datetime.now()
|
||||||
storage_path = settings.storage_path.strip("/")
|
storage_path = settings.storage_path.strip("/")
|
||||||
@@ -44,10 +58,12 @@ class FileUploadService:
|
|||||||
file_path: str,
|
file_path: str,
|
||||||
expire_value: int,
|
expire_value: int,
|
||||||
expire_style: str,
|
expire_style: str,
|
||||||
**extra_fields
|
**extra_fields,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""统一创建FileCodes记录,返回code"""
|
"""统一创建FileCodes记录,返回code"""
|
||||||
expired_at, expired_count, used_count, code = await get_expire_info(expire_value, expire_style)
|
expired_at, expired_count, used_count, code = await get_expire_info(
|
||||||
|
expire_value, expire_style
|
||||||
|
)
|
||||||
prefix, suffix = os.path.splitext(file_name)
|
prefix, suffix = os.path.splitext(file_name)
|
||||||
|
|
||||||
await FileCodes.create(
|
await FileCodes.create(
|
||||||
@@ -60,7 +76,7 @@ class FileUploadService:
|
|||||||
expired_at=expired_at,
|
expired_at=expired_at,
|
||||||
expired_count=expired_count,
|
expired_count=expired_count,
|
||||||
used_count=used_count,
|
used_count=used_count,
|
||||||
**extra_fields
|
**extra_fields,
|
||||||
)
|
)
|
||||||
return code
|
return code
|
||||||
|
|
||||||
@@ -68,8 +84,7 @@ class FileUploadService:
|
|||||||
async def validate_file_size(file: UploadFile, max_size: int) -> int:
|
async def validate_file_size(file: UploadFile, max_size: int) -> int:
|
||||||
size = file.size
|
size = file.size
|
||||||
if size is None:
|
if size is None:
|
||||||
# 读取流计算大小,保持指针复位
|
await file.seek(0, 2) # type: ignore[arg-type]
|
||||||
await file.seek(0, 2)
|
|
||||||
size = file.file.tell()
|
size = file.file.tell()
|
||||||
await file.seek(0)
|
await file.seek(0)
|
||||||
if size > max_size:
|
if size > max_size:
|
||||||
@@ -86,10 +101,10 @@ async def create_file_code(code, **kwargs):
|
|||||||
|
|
||||||
@share_api.post("/text/", dependencies=[Depends(share_required_login)])
|
@share_api.post("/text/", dependencies=[Depends(share_required_login)])
|
||||||
async def share_text(
|
async def share_text(
|
||||||
text: str = Form(...),
|
text: str = Form(...),
|
||||||
expire_value: int = Form(default=1, gt=0),
|
expire_value: int = Form(default=1, gt=0),
|
||||||
expire_style: str = Form(default="day"),
|
expire_style: str = Form(default="day"),
|
||||||
ip: str = Depends(ip_limit["upload"]),
|
ip: str = Depends(ip_limit["upload"]),
|
||||||
):
|
):
|
||||||
text_size = len(text.encode("utf-8"))
|
text_size = len(text.encode("utf-8"))
|
||||||
max_txt_size = 222 * 1024
|
max_txt_size = 222 * 1024
|
||||||
@@ -114,15 +129,17 @@ async def share_text(
|
|||||||
|
|
||||||
@share_api.post("/file/", dependencies=[Depends(share_required_login)])
|
@share_api.post("/file/", dependencies=[Depends(share_required_login)])
|
||||||
async def share_file(
|
async def share_file(
|
||||||
expire_value: int = Form(default=1, gt=0),
|
expire_value: int = Form(default=1, gt=0),
|
||||||
expire_style: str = Form(default="day"),
|
expire_style: str = Form(default="day"),
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
ip: str = Depends(ip_limit["upload"]),
|
ip: str = Depends(ip_limit["upload"]),
|
||||||
):
|
):
|
||||||
file_size = await validate_file_size(file, settings.uploadSize)
|
file_size = await validate_file_size(file, settings.uploadSize)
|
||||||
if expire_style not in settings.expireStyle:
|
if expire_style not in settings.expireStyle:
|
||||||
raise HTTPException(status_code=400, detail="过期时间类型错误")
|
raise HTTPException(status_code=400, detail="过期时间类型错误")
|
||||||
expired_at, expired_count, used_count, code = await get_expire_info(expire_value, expire_style)
|
expired_at, expired_count, used_count, code = await get_expire_info(
|
||||||
|
expire_value, expire_style
|
||||||
|
)
|
||||||
path, suffix, prefix, uuid_file_name, save_path = await get_file_path_name(file)
|
path, suffix, prefix, uuid_file_name, save_path = await get_file_path_name(file)
|
||||||
file_storage: FileStorageInterface = storages[settings.file_storage]()
|
file_storage: FileStorageInterface = storages[settings.file_storage]()
|
||||||
await file_storage.save_file(file, save_path)
|
await file_storage.save_file(file, save_path)
|
||||||
@@ -141,7 +158,9 @@ async def share_file(
|
|||||||
return APIResponse(detail={"code": code, "name": file.filename})
|
return APIResponse(detail={"code": code, "name": file.filename})
|
||||||
|
|
||||||
|
|
||||||
async def get_code_file_by_code(code, check=True):
|
async def get_code_file_by_code(
|
||||||
|
code: str, check: bool = True
|
||||||
|
) -> Tuple[bool, Union[FileCodes, str]]:
|
||||||
file_code = await FileCodes.filter(code=code).first()
|
file_code = await FileCodes.filter(code=code).first()
|
||||||
if not file_code:
|
if not file_code:
|
||||||
return False, "文件不存在"
|
return False, "文件不存在"
|
||||||
@@ -150,7 +169,7 @@ async def get_code_file_by_code(code, check=True):
|
|||||||
return True, file_code
|
return True, file_code
|
||||||
|
|
||||||
|
|
||||||
async def update_file_usage(file_code):
|
async def update_file_usage(file_code: FileCodes) -> None:
|
||||||
file_code.used_count += 1
|
file_code.used_count += 1
|
||||||
if file_code.expired_count > 0:
|
if file_code.expired_count > 0:
|
||||||
file_code.expired_count -= 1
|
file_code.expired_count -= 1
|
||||||
@@ -165,6 +184,7 @@ async def get_code_file(code: str, ip: str = Depends(ip_limit["error"])):
|
|||||||
ip_limit["error"].add_ip(ip)
|
ip_limit["error"].add_ip(ip)
|
||||||
return APIResponse(code=404, detail=file_code)
|
return APIResponse(code=404, detail=file_code)
|
||||||
|
|
||||||
|
assert isinstance(file_code, FileCodes)
|
||||||
await update_file_usage(file_code)
|
await update_file_usage(file_code)
|
||||||
return await file_storage.get_file_response(file_code)
|
return await file_storage.get_file_response(file_code)
|
||||||
|
|
||||||
@@ -177,6 +197,7 @@ async def select_file(data: SelectFileModel, ip: str = Depends(ip_limit["error"]
|
|||||||
ip_limit["error"].add_ip(ip)
|
ip_limit["error"].add_ip(ip)
|
||||||
return APIResponse(code=404, detail=file_code)
|
return APIResponse(code=404, detail=file_code)
|
||||||
|
|
||||||
|
assert isinstance(file_code, FileCodes)
|
||||||
await update_file_usage(file_code)
|
await update_file_usage(file_code)
|
||||||
return APIResponse(
|
return APIResponse(
|
||||||
detail={
|
detail={
|
||||||
@@ -201,6 +222,7 @@ async def download_file(key: str, code: str, ip: str = Depends(ip_limit["error"]
|
|||||||
has, file_code = await get_code_file_by_code(code, False)
|
has, file_code = await get_code_file_by_code(code, False)
|
||||||
if not has:
|
if not has:
|
||||||
return APIResponse(code=404, detail="文件不存在")
|
return APIResponse(code=404, detail="文件不存在")
|
||||||
|
assert isinstance(file_code, FileCodes)
|
||||||
return (
|
return (
|
||||||
APIResponse(detail=file_code.text)
|
APIResponse(detail=file_code.text)
|
||||||
if file_code.text
|
if file_code.text
|
||||||
@@ -219,8 +241,7 @@ async def init_chunk_upload(data: InitChunkUploadModel):
|
|||||||
if max_possible_size > settings.uploadSize:
|
if max_possible_size > settings.uploadSize:
|
||||||
max_size_mb = settings.uploadSize / (1024 * 1024)
|
max_size_mb = settings.uploadSize / (1024 * 1024)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403, detail=f"文件大小超过限制,最大为 {max_size_mb:.2f} MB"
|
||||||
detail=f"文件大小超过限制,最大为 {max_size_mb:.2f} MB"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# # 秒传检查
|
# # 秒传检查
|
||||||
@@ -247,21 +268,25 @@ async def init_chunk_upload(data: InitChunkUploadModel):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if existing_session:
|
if existing_session:
|
||||||
# 复用已有会话,获取已上传的分片列表
|
if not existing_session.save_path:
|
||||||
uploaded_chunks = await UploadChunk.filter(
|
await UploadChunk.filter(upload_id=existing_session.upload_id).delete()
|
||||||
upload_id=existing_session.upload_id,
|
else:
|
||||||
completed=True
|
uploaded_chunks = await UploadChunk.filter(
|
||||||
).values_list('chunk_index', flat=True)
|
upload_id=existing_session.upload_id, completed=True
|
||||||
return APIResponse(detail={
|
).values_list("chunk_index", flat=True)
|
||||||
"existed": False,
|
return APIResponse(
|
||||||
"upload_id": existing_session.upload_id,
|
detail={
|
||||||
"chunk_size": existing_session.chunk_size,
|
"existed": False,
|
||||||
"total_chunks": existing_session.total_chunks,
|
"upload_id": existing_session.upload_id,
|
||||||
"uploaded_chunks": list(uploaded_chunks)
|
"chunk_size": existing_session.chunk_size,
|
||||||
})
|
"total_chunks": existing_session.total_chunks,
|
||||||
|
"uploaded_chunks": list(uploaded_chunks),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# 创建新的上传会话
|
# 创建新的上传会话
|
||||||
upload_id = uuid.uuid4().hex
|
upload_id = uuid.uuid4().hex
|
||||||
|
_, _, _, _, save_path = await get_chunk_file_path_name(data.file_name, upload_id)
|
||||||
await UploadChunk.create(
|
await UploadChunk.create(
|
||||||
upload_id=upload_id,
|
upload_id=upload_id,
|
||||||
chunk_index=-1,
|
chunk_index=-1,
|
||||||
@@ -270,21 +295,27 @@ async def init_chunk_upload(data: InitChunkUploadModel):
|
|||||||
chunk_size=data.chunk_size,
|
chunk_size=data.chunk_size,
|
||||||
chunk_hash=data.file_hash,
|
chunk_hash=data.file_hash,
|
||||||
file_name=data.file_name,
|
file_name=data.file_name,
|
||||||
|
save_path=save_path,
|
||||||
|
)
|
||||||
|
return APIResponse(
|
||||||
|
detail={
|
||||||
|
"existed": False,
|
||||||
|
"upload_id": upload_id,
|
||||||
|
"chunk_size": data.chunk_size,
|
||||||
|
"total_chunks": total_chunks,
|
||||||
|
"uploaded_chunks": [],
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return APIResponse(detail={
|
|
||||||
"existed": False,
|
|
||||||
"upload_id": upload_id,
|
|
||||||
"chunk_size": data.chunk_size,
|
|
||||||
"total_chunks": total_chunks,
|
|
||||||
"uploaded_chunks": []
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@chunk_api.post("/upload/chunk/{upload_id}/{chunk_index}", dependencies=[Depends(share_required_login)])
|
@chunk_api.post(
|
||||||
|
"/upload/chunk/{upload_id}/{chunk_index}",
|
||||||
|
dependencies=[Depends(share_required_login)],
|
||||||
|
)
|
||||||
async def upload_chunk(
|
async def upload_chunk(
|
||||||
upload_id: str,
|
upload_id: str,
|
||||||
chunk_index: int,
|
chunk_index: int,
|
||||||
chunk: UploadFile = File(...),
|
chunk: UploadFile = File(...),
|
||||||
):
|
):
|
||||||
# 获取上传会话信息
|
# 获取上传会话信息
|
||||||
chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
|
chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
|
||||||
@@ -297,12 +328,12 @@ async def upload_chunk(
|
|||||||
|
|
||||||
# 检查是否已上传(支持断点续传)
|
# 检查是否已上传(支持断点续传)
|
||||||
existing_chunk = await UploadChunk.filter(
|
existing_chunk = await UploadChunk.filter(
|
||||||
upload_id=upload_id,
|
upload_id=upload_id, chunk_index=chunk_index, completed=True
|
||||||
chunk_index=chunk_index,
|
|
||||||
completed=True
|
|
||||||
).first()
|
).first()
|
||||||
if existing_chunk:
|
if existing_chunk:
|
||||||
return APIResponse(detail={"chunk_hash": existing_chunk.chunk_hash, "skipped": True})
|
return APIResponse(
|
||||||
|
detail={"chunk_hash": existing_chunk.chunk_hash, "skipped": True}
|
||||||
|
)
|
||||||
|
|
||||||
# 读取分片数据并计算哈希
|
# 读取分片数据并计算哈希
|
||||||
chunk_data = await chunk.read()
|
chunk_data = await chunk.read()
|
||||||
@@ -312,47 +343,49 @@ async def upload_chunk(
|
|||||||
if chunk_size > chunk_info.chunk_size:
|
if chunk_size > chunk_info.chunk_size:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status.HTTP_400_BAD_REQUEST,
|
status.HTTP_400_BAD_REQUEST,
|
||||||
detail=f"分片大小超过声明值: 最大 {chunk_info.chunk_size}, 实际 {chunk_size}"
|
detail=f"分片大小超过声明值: 最大 {chunk_info.chunk_size}, 实际 {chunk_size}",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 计算已上传分片数,校验累计大小不超限(用分片数 * chunk_size 估算)
|
# 计算已上传分片数,校验累计大小不超限(用分片数 * chunk_size 估算)
|
||||||
uploaded_count = await UploadChunk.filter(
|
uploaded_count = await UploadChunk.filter(
|
||||||
upload_id=upload_id,
|
upload_id=upload_id, completed=True
|
||||||
completed=True
|
|
||||||
).count()
|
).count()
|
||||||
# 已上传分片的最大可能大小 + 当前分片
|
# 已上传分片的最大可能大小 + 当前分片
|
||||||
max_uploaded_size = uploaded_count * chunk_info.chunk_size + chunk_size
|
max_uploaded_size = uploaded_count * chunk_info.chunk_size + chunk_size
|
||||||
if max_uploaded_size > settings.uploadSize:
|
if max_uploaded_size > settings.uploadSize:
|
||||||
max_size_mb = settings.uploadSize / (1024 * 1024)
|
max_size_mb = settings.uploadSize / (1024 * 1024)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403, detail=f"累计上传大小超过限制,最大为 {max_size_mb:.2f} MB"
|
||||||
detail=f"累计上传大小超过限制,最大为 {max_size_mb:.2f} MB"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
chunk_hash = hashlib.sha256(chunk_data).hexdigest()
|
chunk_hash = hashlib.sha256(chunk_data).hexdigest()
|
||||||
|
|
||||||
# 获取文件路径
|
save_path = chunk_info.save_path
|
||||||
_, _, _, _, save_path = await get_chunk_file_path_name(chunk_info.file_name, upload_id)
|
|
||||||
|
|
||||||
# 保存分片到存储
|
# 保存分片到存储
|
||||||
storage = storages[settings.file_storage]()
|
storage = storages[settings.file_storage]()
|
||||||
try:
|
try:
|
||||||
await storage.save_chunk(upload_id, chunk_index, chunk_data, chunk_hash, save_path)
|
await storage.save_chunk(
|
||||||
|
upload_id, chunk_index, chunk_data, chunk_hash, save_path
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"分片保存失败: {str(e)}")
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"分片保存失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
# 更新或创建分片记录(保存成功后再记录)
|
# 更新或创建分片记录(保存成功后再记录)
|
||||||
await UploadChunk.update_or_create(
|
await UploadChunk.update_or_create(
|
||||||
upload_id=upload_id,
|
upload_id=upload_id,
|
||||||
chunk_index=chunk_index,
|
chunk_index=chunk_index,
|
||||||
defaults={
|
defaults={
|
||||||
'chunk_hash': chunk_hash,
|
"chunk_hash": chunk_hash,
|
||||||
'completed': True,
|
"completed": True,
|
||||||
'file_size': chunk_info.file_size,
|
"file_size": chunk_info.file_size,
|
||||||
'total_chunks': chunk_info.total_chunks,
|
"total_chunks": chunk_info.total_chunks,
|
||||||
'chunk_size': chunk_info.chunk_size,
|
"chunk_size": chunk_info.chunk_size,
|
||||||
'file_name': chunk_info.file_name
|
"file_name": chunk_info.file_name,
|
||||||
}
|
"save_path": chunk_info.save_path,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
return APIResponse(detail={"chunk_hash": chunk_hash})
|
return APIResponse(detail={"chunk_hash": chunk_hash})
|
||||||
|
|
||||||
@@ -364,16 +397,15 @@ async def cancel_upload(upload_id: str):
|
|||||||
if not chunk_info:
|
if not chunk_info:
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在")
|
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="上传会话不存在")
|
||||||
|
|
||||||
# 获取文件路径
|
save_path = chunk_info.save_path
|
||||||
_, _, _, _, save_path = await get_chunk_file_path_name(chunk_info.file_name, upload_id)
|
|
||||||
|
|
||||||
# 清理存储中的临时文件
|
# 清理存储中的临时文件
|
||||||
storage = storages[settings.file_storage]()
|
storage = storages[settings.file_storage]()
|
||||||
try:
|
if save_path:
|
||||||
await storage.clean_chunks(upload_id, save_path)
|
try:
|
||||||
except Exception as e:
|
await storage.clean_chunks(upload_id, save_path)
|
||||||
# 记录错误但不阻止删除数据库记录
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 清理数据库记录
|
# 清理数据库记录
|
||||||
await UploadChunk.filter(upload_id=upload_id).delete()
|
await UploadChunk.filter(upload_id=upload_id).delete()
|
||||||
@@ -381,7 +413,9 @@ async def cancel_upload(upload_id: str):
|
|||||||
return APIResponse(detail={"message": "上传已取消"})
|
return APIResponse(detail={"message": "上传已取消"})
|
||||||
|
|
||||||
|
|
||||||
@chunk_api.get("/upload/status/{upload_id}", dependencies=[Depends(share_required_login)])
|
@chunk_api.get(
|
||||||
|
"/upload/status/{upload_id}", dependencies=[Depends(share_required_login)]
|
||||||
|
)
|
||||||
async def get_upload_status(upload_id: str):
|
async def get_upload_status(upload_id: str):
|
||||||
"""获取上传状态"""
|
"""获取上传状态"""
|
||||||
chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
|
chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
|
||||||
@@ -390,23 +424,28 @@ async def get_upload_status(upload_id: str):
|
|||||||
|
|
||||||
# 获取已上传的分片列表
|
# 获取已上传的分片列表
|
||||||
uploaded_chunks = await UploadChunk.filter(
|
uploaded_chunks = await UploadChunk.filter(
|
||||||
upload_id=upload_id,
|
upload_id=upload_id, completed=True
|
||||||
completed=True
|
).values_list("chunk_index", flat=True)
|
||||||
).values_list('chunk_index', flat=True)
|
|
||||||
|
|
||||||
return APIResponse(detail={
|
return APIResponse(
|
||||||
"upload_id": upload_id,
|
detail={
|
||||||
"file_name": chunk_info.file_name,
|
"upload_id": upload_id,
|
||||||
"file_size": chunk_info.file_size,
|
"file_name": chunk_info.file_name,
|
||||||
"chunk_size": chunk_info.chunk_size,
|
"file_size": chunk_info.file_size,
|
||||||
"total_chunks": chunk_info.total_chunks,
|
"chunk_size": chunk_info.chunk_size,
|
||||||
"uploaded_chunks": list(uploaded_chunks),
|
"total_chunks": chunk_info.total_chunks,
|
||||||
"progress": len(uploaded_chunks) / chunk_info.total_chunks * 100
|
"uploaded_chunks": list(uploaded_chunks),
|
||||||
})
|
"progress": len(uploaded_chunks) / chunk_info.total_chunks * 100,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@chunk_api.post("/upload/complete/{upload_id}", dependencies=[Depends(share_required_login)])
|
@chunk_api.post(
|
||||||
async def complete_upload(upload_id: str, data: CompleteUploadModel, ip: str = Depends(ip_limit["upload"])):
|
"/upload/complete/{upload_id}", dependencies=[Depends(share_required_login)]
|
||||||
|
)
|
||||||
|
async def complete_upload(
|
||||||
|
upload_id: str, data: CompleteUploadModel, ip: str = Depends(ip_limit["upload"])
|
||||||
|
):
|
||||||
# 获取上传基本信息
|
# 获取上传基本信息
|
||||||
chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
|
chunk_info = await UploadChunk.filter(upload_id=upload_id, chunk_index=-1).first()
|
||||||
if not chunk_info:
|
if not chunk_info:
|
||||||
@@ -415,8 +454,7 @@ async def complete_upload(upload_id: str, data: CompleteUploadModel, ip: str = D
|
|||||||
storage = storages[settings.file_storage]()
|
storage = storages[settings.file_storage]()
|
||||||
# 验证所有分片
|
# 验证所有分片
|
||||||
completed_chunks_list = await UploadChunk.filter(
|
completed_chunks_list = await UploadChunk.filter(
|
||||||
upload_id=upload_id,
|
upload_id=upload_id, completed=True
|
||||||
completed=True
|
|
||||||
).all()
|
).all()
|
||||||
if len(completed_chunks_list) != chunk_info.total_chunks:
|
if len(completed_chunks_list) != chunk_info.total_chunks:
|
||||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="分片不完整")
|
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="分片不完整")
|
||||||
@@ -424,27 +462,29 @@ async def complete_upload(upload_id: str, data: CompleteUploadModel, ip: str = D
|
|||||||
# 用分片数 * chunk_size 校验最大可能大小
|
# 用分片数 * chunk_size 校验最大可能大小
|
||||||
max_total_size = len(completed_chunks_list) * chunk_info.chunk_size
|
max_total_size = len(completed_chunks_list) * chunk_info.chunk_size
|
||||||
if max_total_size > settings.uploadSize:
|
if max_total_size > settings.uploadSize:
|
||||||
# 清理已上传的分片
|
save_path = chunk_info.save_path
|
||||||
_, _, _, _, save_path = await get_chunk_file_path_name(chunk_info.file_name, upload_id)
|
if save_path:
|
||||||
try:
|
try:
|
||||||
await storage.clean_chunks(upload_id, save_path)
|
await storage.clean_chunks(upload_id, save_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
await UploadChunk.filter(upload_id=upload_id).delete()
|
await UploadChunk.filter(upload_id=upload_id).delete()
|
||||||
max_size_mb = settings.uploadSize / (1024 * 1024)
|
max_size_mb = settings.uploadSize / (1024 * 1024)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403, detail=f"实际上传大小超过限制,最大为 {max_size_mb:.2f} MB"
|
||||||
detail=f"实际上传大小超过限制,最大为 {max_size_mb:.2f} MB"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 获取文件路径
|
save_path = chunk_info.save_path
|
||||||
path, suffix, prefix, _, save_path = await get_chunk_file_path_name(chunk_info.file_name, upload_id)
|
path = os.path.dirname(save_path) if save_path else ""
|
||||||
|
prefix, suffix = os.path.splitext(chunk_info.file_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 合并文件并计算哈希
|
# 合并文件并计算哈希
|
||||||
_, file_hash = await storage.merge_chunks(upload_id, chunk_info, save_path)
|
_, file_hash = await storage.merge_chunks(upload_id, chunk_info, save_path)
|
||||||
# 创建文件记录
|
# 创建文件记录
|
||||||
expired_at, expired_count, used_count, code = await get_expire_info(data.expire_value, data.expire_style)
|
expired_at, expired_count, used_count, code = await get_expire_info(
|
||||||
|
data.expire_value, data.expire_style
|
||||||
|
)
|
||||||
await FileCodes.create(
|
await FileCodes.create(
|
||||||
code=code,
|
code=code,
|
||||||
file_hash=file_hash, # 使用合并后计算的哈希
|
file_hash=file_hash, # 使用合并后计算的哈希
|
||||||
@@ -457,7 +497,7 @@ async def complete_upload(upload_id: str, data: CompleteUploadModel, ip: str = D
|
|||||||
file_path=path,
|
file_path=path,
|
||||||
uuid_file_name=f"{prefix}{suffix}",
|
uuid_file_name=f"{prefix}{suffix}",
|
||||||
prefix=prefix,
|
prefix=prefix,
|
||||||
suffix=suffix
|
suffix=suffix,
|
||||||
)
|
)
|
||||||
# 清理临时文件
|
# 清理临时文件
|
||||||
await storage.clean_chunks(upload_id, save_path)
|
await storage.clean_chunks(upload_id, save_path)
|
||||||
@@ -473,7 +513,9 @@ async def complete_upload(upload_id: str, data: CompleteUploadModel, ip: str = D
|
|||||||
await storage.clean_chunks(upload_id, save_path)
|
await storage.clean_chunks(upload_id, save_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"文件合并失败: {str(e)}")
|
raise HTTPException(
|
||||||
|
status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"文件合并失败: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ============ 预签名上传API ============
|
# ============ 预签名上传API ============
|
||||||
@@ -482,7 +524,9 @@ presign_api = APIRouter(prefix="/presign", tags=["预签名上传"])
|
|||||||
PRESIGN_SESSION_EXPIRES = 900 # 15分钟
|
PRESIGN_SESSION_EXPIRES = 900 # 15分钟
|
||||||
|
|
||||||
|
|
||||||
async def _get_valid_session(upload_id: str, expected_mode: str = None) -> PresignUploadSession:
|
async def _get_valid_session(
|
||||||
|
upload_id: str, expected_mode: Optional[str] = None
|
||||||
|
) -> PresignUploadSession:
|
||||||
"""获取并验证会话"""
|
"""获取并验证会话"""
|
||||||
session = await PresignUploadSession.filter(upload_id=upload_id).first()
|
session = await PresignUploadSession.filter(upload_id=upload_id).first()
|
||||||
if not session:
|
if not session:
|
||||||
@@ -496,18 +540,27 @@ async def _get_valid_session(upload_id: str, expected_mode: str = None) -> Presi
|
|||||||
|
|
||||||
|
|
||||||
@presign_api.post("/upload/init", dependencies=[Depends(share_required_login)])
|
@presign_api.post("/upload/init", dependencies=[Depends(share_required_login)])
|
||||||
async def presign_upload_init(data: PresignUploadInitRequest, ip: str = Depends(ip_limit["upload"])):
|
async def presign_upload_init(
|
||||||
|
data: PresignUploadInitRequest, ip: str = Depends(ip_limit["upload"])
|
||||||
|
):
|
||||||
"""初始化预签名上传,S3返回直传URL,其他存储返回代理URL"""
|
"""初始化预签名上传,S3返回直传URL,其他存储返回代理URL"""
|
||||||
if data.file_size > settings.uploadSize:
|
if data.file_size > settings.uploadSize:
|
||||||
raise HTTPException(403, f"文件大小超过限制,最大为 {settings.uploadSize / (1024*1024):.2f} MB")
|
raise HTTPException(
|
||||||
|
403,
|
||||||
|
f"文件大小超过限制,最大为 {settings.uploadSize / (1024 * 1024):.2f} MB",
|
||||||
|
)
|
||||||
if data.expire_style not in settings.expireStyle:
|
if data.expire_style not in settings.expireStyle:
|
||||||
raise HTTPException(400, "过期时间类型错误")
|
raise HTTPException(400, "过期时间类型错误")
|
||||||
|
|
||||||
upload_id = uuid.uuid4().hex
|
upload_id = uuid.uuid4().hex
|
||||||
path, _, _, filename, save_path = await FileUploadService.generate_file_path(data.file_name, upload_id)
|
path, _, _, filename, save_path = await FileUploadService.generate_file_path(
|
||||||
|
data.file_name, upload_id
|
||||||
|
)
|
||||||
|
|
||||||
storage: FileStorageInterface = storages[settings.file_storage]()
|
storage: FileStorageInterface = storages[settings.file_storage]()
|
||||||
presigned_url = await storage.generate_presigned_upload_url(save_path, PRESIGN_SESSION_EXPIRES)
|
presigned_url = await storage.generate_presigned_upload_url(
|
||||||
|
save_path, PRESIGN_SESSION_EXPIRES
|
||||||
|
)
|
||||||
|
|
||||||
mode = "direct" if presigned_url else "proxy"
|
mode = "direct" if presigned_url else "proxy"
|
||||||
upload_url = presigned_url or f"/api/presign/upload/proxy/{upload_id}"
|
upload_url = presigned_url or f"/api/presign/upload/proxy/{upload_id}"
|
||||||
@@ -524,16 +577,22 @@ async def presign_upload_init(data: PresignUploadInitRequest, ip: str = Depends(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ip_limit["upload"].add_ip(ip)
|
ip_limit["upload"].add_ip(ip)
|
||||||
return APIResponse(detail={
|
return APIResponse(
|
||||||
"upload_id": upload_id,
|
detail={
|
||||||
"upload_url": upload_url,
|
"upload_id": upload_id,
|
||||||
"mode": mode,
|
"upload_url": upload_url,
|
||||||
"expires_in": PRESIGN_SESSION_EXPIRES,
|
"mode": mode,
|
||||||
})
|
"expires_in": PRESIGN_SESSION_EXPIRES,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@presign_api.put("/upload/proxy/{upload_id}", dependencies=[Depends(share_required_login)])
|
@presign_api.put(
|
||||||
async def presign_upload_proxy(upload_id: str, file: UploadFile = File(...), ip: str = Depends(ip_limit["upload"])):
|
"/upload/proxy/{upload_id}", dependencies=[Depends(share_required_login)]
|
||||||
|
)
|
||||||
|
async def presign_upload_proxy(
|
||||||
|
upload_id: str, file: UploadFile = File(...), ip: str = Depends(ip_limit["upload"])
|
||||||
|
):
|
||||||
"""代理模式上传,服务器转存到存储后端"""
|
"""代理模式上传,服务器转存到存储后端"""
|
||||||
session = await _get_valid_session(upload_id, expected_mode="proxy")
|
session = await _get_valid_session(upload_id, expected_mode="proxy")
|
||||||
|
|
||||||
@@ -548,8 +607,11 @@ async def presign_upload_proxy(upload_id: str, file: UploadFile = File(...), ip:
|
|||||||
raise HTTPException(500, f"文件保存失败: {str(e)}")
|
raise HTTPException(500, f"文件保存失败: {str(e)}")
|
||||||
|
|
||||||
code = await FileUploadService.create_file_record(
|
code = await FileUploadService.create_file_record(
|
||||||
session.file_name, file_size, os.path.dirname(session.save_path),
|
session.file_name,
|
||||||
session.expire_value, session.expire_style
|
file_size,
|
||||||
|
os.path.dirname(session.save_path),
|
||||||
|
session.expire_value,
|
||||||
|
session.expire_style,
|
||||||
)
|
)
|
||||||
|
|
||||||
await session.delete()
|
await session.delete()
|
||||||
@@ -557,7 +619,9 @@ async def presign_upload_proxy(upload_id: str, file: UploadFile = File(...), ip:
|
|||||||
return APIResponse(detail={"code": code, "name": session.file_name})
|
return APIResponse(detail={"code": code, "name": session.file_name})
|
||||||
|
|
||||||
|
|
||||||
@presign_api.post("/upload/confirm/{upload_id}", dependencies=[Depends(share_required_login)])
|
@presign_api.post(
|
||||||
|
"/upload/confirm/{upload_id}", dependencies=[Depends(share_required_login)]
|
||||||
|
)
|
||||||
async def presign_upload_confirm(upload_id: str, ip: str = Depends(ip_limit["upload"])):
|
async def presign_upload_confirm(upload_id: str, ip: str = Depends(ip_limit["upload"])):
|
||||||
"""直传确认,客户端完成S3直传后调用获取分享码"""
|
"""直传确认,客户端完成S3直传后调用获取分享码"""
|
||||||
session = await _get_valid_session(upload_id, expected_mode="direct")
|
session = await _get_valid_session(upload_id, expected_mode="direct")
|
||||||
@@ -567,8 +631,11 @@ async def presign_upload_confirm(upload_id: str, ip: str = Depends(ip_limit["upl
|
|||||||
raise HTTPException(404, "文件未上传或上传失败")
|
raise HTTPException(404, "文件未上传或上传失败")
|
||||||
|
|
||||||
code = await FileUploadService.create_file_record(
|
code = await FileUploadService.create_file_record(
|
||||||
session.file_name, session.file_size, os.path.dirname(session.save_path),
|
session.file_name,
|
||||||
session.expire_value, session.expire_style
|
session.file_size,
|
||||||
|
os.path.dirname(session.save_path),
|
||||||
|
session.expire_value,
|
||||||
|
session.expire_style,
|
||||||
)
|
)
|
||||||
|
|
||||||
await session.delete()
|
await session.delete()
|
||||||
@@ -576,22 +643,26 @@ async def presign_upload_confirm(upload_id: str, ip: str = Depends(ip_limit["upl
|
|||||||
return APIResponse(detail={"code": code, "name": session.file_name})
|
return APIResponse(detail={"code": code, "name": session.file_name})
|
||||||
|
|
||||||
|
|
||||||
@presign_api.get("/upload/status/{upload_id}", dependencies=[Depends(share_required_login)])
|
@presign_api.get(
|
||||||
|
"/upload/status/{upload_id}", dependencies=[Depends(share_required_login)]
|
||||||
|
)
|
||||||
async def presign_upload_status(upload_id: str):
|
async def presign_upload_status(upload_id: str):
|
||||||
"""查询上传会话状态"""
|
"""查询上传会话状态"""
|
||||||
session = await PresignUploadSession.filter(upload_id=upload_id).first()
|
session = await PresignUploadSession.filter(upload_id=upload_id).first()
|
||||||
if not session:
|
if not session:
|
||||||
raise HTTPException(404, "上传会话不存在")
|
raise HTTPException(404, "上传会话不存在")
|
||||||
|
|
||||||
return APIResponse(detail={
|
return APIResponse(
|
||||||
"upload_id": session.upload_id,
|
detail={
|
||||||
"file_name": session.file_name,
|
"upload_id": session.upload_id,
|
||||||
"file_size": session.file_size,
|
"file_name": session.file_name,
|
||||||
"mode": session.mode,
|
"file_size": session.file_size,
|
||||||
"created_at": session.created_at.isoformat(),
|
"mode": session.mode,
|
||||||
"expires_at": session.expires_at.isoformat(),
|
"created_at": session.created_at.isoformat(),
|
||||||
"is_expired": await session.is_expired(),
|
"expires_at": session.expires_at.isoformat(),
|
||||||
})
|
"is_expired": await session.is_expired(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@presign_api.delete("/upload/{upload_id}", dependencies=[Depends(share_required_login)])
|
@presign_api.delete("/upload/{upload_id}", dependencies=[Depends(share_required_login)])
|
||||||
|
|||||||
+16
-16
@@ -46,38 +46,38 @@ async def delete_expire_files():
|
|||||||
|
|
||||||
|
|
||||||
async def clean_incomplete_uploads():
|
async def clean_incomplete_uploads():
|
||||||
"""清理超时未完成的分片上传"""
|
|
||||||
file_storage: FileStorageInterface = storages[settings.file_storage]()
|
file_storage: FileStorageInterface = storages[settings.file_storage]()
|
||||||
# 默认 24 小时未完成的上传视为过期
|
expire_hours = getattr(settings, "chunk_expire_hours", 24)
|
||||||
expire_hours = getattr(settings, 'chunk_expire_hours', 24)
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
expire_time = datetime.datetime.now() - datetime.timedelta(hours=expire_hours)
|
now = await get_now()
|
||||||
# 查找所有过期的上传会话(chunk_index=-1 的记录)
|
expire_time = now - datetime.timedelta(hours=expire_hours)
|
||||||
expired_sessions = await UploadChunk.filter(
|
expired_sessions = await UploadChunk.filter(
|
||||||
chunk_index=-1,
|
chunk_index=-1, created_at__lt=expire_time
|
||||||
created_at__lt=expire_time
|
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
for session in expired_sessions:
|
for session in expired_sessions:
|
||||||
try:
|
try:
|
||||||
# 获取分片存储路径
|
save_path = session.save_path
|
||||||
_, _, _, _, save_path = await get_chunk_file_path_name(
|
if not save_path:
|
||||||
session.file_name, session.upload_id
|
_, _, _, _, save_path = await get_chunk_file_path_name(
|
||||||
)
|
session.file_name, session.upload_id
|
||||||
# 清理存储中的临时文件
|
)
|
||||||
await file_storage.clean_chunks(session.upload_id, save_path)
|
await file_storage.clean_chunks(session.upload_id, save_path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"清理分片文件失败 upload_id={session.upload_id}: {e}")
|
logging.error(
|
||||||
|
f"清理分片文件失败 upload_id={session.upload_id}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 删除该会话的所有数据库记录
|
|
||||||
await UploadChunk.filter(upload_id=session.upload_id).delete()
|
await UploadChunk.filter(upload_id=session.upload_id).delete()
|
||||||
logging.info(f"已清理过期上传会话 upload_id={session.upload_id}")
|
logging.info(f"已清理过期上传会话 upload_id={session.upload_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"删除分片记录失败 upload_id={session.upload_id}: {e}")
|
logging.error(
|
||||||
|
f"删除分片记录失败 upload_id={session.upload_id}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"清理未完成上传任务异常: {e}")
|
logging.error(f"清理未完成上传任务异常: {e}")
|
||||||
finally:
|
finally:
|
||||||
await asyncio.sleep(3600) # 每小时执行一次
|
await asyncio.sleep(3600)
|
||||||
|
|||||||
+46
-6
@@ -47,7 +47,9 @@ async def get_select_token(code: str):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
token = settings.admin_token
|
token = settings.admin_token
|
||||||
return hashlib.sha256(f"{code}{int(time.time() / 1000)}000{token}".encode()).hexdigest()
|
return hashlib.sha256(
|
||||||
|
f"{code}{int(time.time() / 1000)}000{token}".encode()
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
async def get_file_url(code: str):
|
async def get_file_url(code: str):
|
||||||
@@ -95,6 +97,44 @@ async def max_save_times_desc(max_save_seconds: int):
|
|||||||
return desc_zh, desc_en
|
return desc_zh, desc_en
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
"""
|
||||||
|
使用 SHA256 + salt 哈希密码
|
||||||
|
返回格式: sha256$<salt>$<hash>
|
||||||
|
"""
|
||||||
|
salt = os.urandom(16).hex()
|
||||||
|
password_hash = hashlib.sha256(f"{salt}{password}".encode()).hexdigest()
|
||||||
|
return f"sha256${salt}${password_hash}"
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(password: str, hashed: str) -> bool:
|
||||||
|
"""
|
||||||
|
验证密码是否匹配
|
||||||
|
支持新格式 (sha256$salt$hash) 和旧格式 (明文)
|
||||||
|
"""
|
||||||
|
if not hashed:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 新格式: sha256$salt$hash
|
||||||
|
if hashed.startswith("sha256$"):
|
||||||
|
parts = hashed.split("$")
|
||||||
|
if len(parts) != 3:
|
||||||
|
return False
|
||||||
|
_, salt, stored_hash = parts
|
||||||
|
password_hash = hashlib.sha256(f"{salt}{password}".encode()).hexdigest()
|
||||||
|
return password_hash == stored_hash
|
||||||
|
|
||||||
|
# 旧格式: 明文比较 (兼容迁移前的数据)
|
||||||
|
return password == hashed
|
||||||
|
|
||||||
|
|
||||||
|
def is_password_hashed(password: str) -> bool:
|
||||||
|
"""
|
||||||
|
检查密码是否已经是哈希格式
|
||||||
|
"""
|
||||||
|
return password.startswith("sha256$") and len(password.split("$")) == 3
|
||||||
|
|
||||||
|
|
||||||
async def sanitize_filename(filename: str) -> str:
|
async def sanitize_filename(filename: str) -> str:
|
||||||
"""
|
"""
|
||||||
安全处理文件名:
|
安全处理文件名:
|
||||||
@@ -105,15 +145,15 @@ async def sanitize_filename(filename: str) -> str:
|
|||||||
filename = os.path.basename(filename)
|
filename = os.path.basename(filename)
|
||||||
illegal_chars = r'[\\/*?:"<>|\x00-\x1F]' # 包含控制字符
|
illegal_chars = r'[\\/*?:"<>|\x00-\x1F]' # 包含控制字符
|
||||||
# 替换非法字符为下划线
|
# 替换非法字符为下划线
|
||||||
cleaned = re.sub(illegal_chars, '_', filename)
|
cleaned = re.sub(illegal_chars, "_", filename)
|
||||||
# 处理空格(可选替换为_)
|
# 处理空格(可选替换为_)
|
||||||
cleaned = cleaned.replace(' ', '_')
|
cleaned = cleaned.replace(" ", "_")
|
||||||
# 处理连续下划线
|
# 处理连续下划线
|
||||||
cleaned = re.sub(r'_+', '_', cleaned)
|
cleaned = re.sub(r"_+", "_", cleaned)
|
||||||
# 处理首尾特殊字符
|
# 处理首尾特殊字符
|
||||||
cleaned = cleaned.strip('._')
|
cleaned = cleaned.strip("._")
|
||||||
# 处理空文件名情况
|
# 处理空文件名情况
|
||||||
if not cleaned:
|
if not cleaned:
|
||||||
cleaned = 'unnamed_file'
|
cleaned = "unnamed_file"
|
||||||
# 长度限制(按需调整)
|
# 长度限制(按需调整)
|
||||||
return cleaned[:255]
|
return cleaned[:255]
|
||||||
|
|||||||
+74
-5
@@ -15,6 +15,29 @@
|
|||||||
Authorization: Bearer <token>
|
Authorization: Bearer <token>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 获取 Token
|
||||||
|
|
||||||
|
当管理面板关闭了游客上传(`openUpload=0`)时,需要先登录获取 token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:12345/admin/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password": "FileCodeBox2023"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"detail": {
|
||||||
|
"token": "xxx.xxx.xxx",
|
||||||
|
"token_type": "Bearer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 分享接口
|
## 分享接口
|
||||||
|
|
||||||
### 分享文本
|
### 分享文本
|
||||||
@@ -29,7 +52,22 @@ Authorization: Bearer <token>
|
|||||||
|-------|------|------|--------|------|
|
|-------|------|------|--------|------|
|
||||||
| text | string | 是 | - | 要分享的文本内容 |
|
| text | string | 是 | - | 要分享的文本内容 |
|
||||||
| expire_value | integer | 否 | 1 | 过期时间值 |
|
| expire_value | integer | 否 | 1 | 过期时间值 |
|
||||||
| expire_style | string | 否 | "day" | 过期时间单位(day/hour/minute) |
|
| expire_style | string | 否 | "day" | 过期时间单位(day/hour/minute/count/forever) |
|
||||||
|
|
||||||
|
**cURL 示例:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 游客上传(openUpload=1 时)
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-F "text=这是要分享的文本内容" \
|
||||||
|
-F "expire_value=1" \
|
||||||
|
-F "expire_style=day"
|
||||||
|
|
||||||
|
# 需要认证时(openUpload=0 时)
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "text=这是要分享的文本内容"
|
||||||
|
```
|
||||||
|
|
||||||
**响应示例:**
|
**响应示例:**
|
||||||
|
|
||||||
@@ -38,8 +76,7 @@ Authorization: Bearer <token>
|
|||||||
"code": 200,
|
"code": 200,
|
||||||
"msg": "success",
|
"msg": "success",
|
||||||
"detail": {
|
"detail": {
|
||||||
"code": "abc123",
|
"code": "abc123"
|
||||||
"name": "text.txt"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -56,7 +93,32 @@ Authorization: Bearer <token>
|
|||||||
|-------|------|------|--------|------|
|
|-------|------|------|--------|------|
|
||||||
| file | file | 是 | - | 要上传的文件 |
|
| file | file | 是 | - | 要上传的文件 |
|
||||||
| expire_value | integer | 否 | 1 | 过期时间值 |
|
| expire_value | integer | 否 | 1 | 过期时间值 |
|
||||||
| expire_style | string | 否 | "day" | 过期时间单位(day/hour/minute) |
|
| expire_style | string | 否 | "day" | 过期时间单位(day/hour/minute/count/forever) |
|
||||||
|
|
||||||
|
**cURL 示例:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 上传文件(默认1天有效期)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.txt"
|
||||||
|
|
||||||
|
# 上传文件(7天有效期)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.txt" \
|
||||||
|
-F "expire_value=7" \
|
||||||
|
-F "expire_style=day"
|
||||||
|
|
||||||
|
# 上传文件(可下载10次)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.txt" \
|
||||||
|
-F "expire_value=10" \
|
||||||
|
-F "expire_style=count"
|
||||||
|
|
||||||
|
# 需要认证时
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "file=@/path/to/file.txt"
|
||||||
|
```
|
||||||
|
|
||||||
**响应示例:**
|
**响应示例:**
|
||||||
|
|
||||||
@@ -75,7 +137,7 @@ Authorization: Bearer <token>
|
|||||||
|
|
||||||
**GET** `/share/select/`
|
**GET** `/share/select/`
|
||||||
|
|
||||||
通过分享码获取文件信息。
|
通过分享码获取文件信息(直接下载文件)。
|
||||||
|
|
||||||
**请求参数:**
|
**请求参数:**
|
||||||
|
|
||||||
@@ -83,6 +145,13 @@ Authorization: Bearer <token>
|
|||||||
|-------|------|------|------|
|
|-------|------|------|------|
|
||||||
| code | string | 是 | 文件分享码 |
|
| code | string | 是 | 文件分享码 |
|
||||||
|
|
||||||
|
**cURL 示例:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 通过取件码下载文件
|
||||||
|
curl -L "http://localhost:12345/share/select/?code=abc123" -o downloaded_file
|
||||||
|
```
|
||||||
|
|
||||||
**响应示例:**
|
**响应示例:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
+74
-5
@@ -15,6 +15,29 @@ Some APIs require `Authorization` header for authentication:
|
|||||||
Authorization: Bearer <token>
|
Authorization: Bearer <token>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Get Token
|
||||||
|
|
||||||
|
When guest upload is disabled in admin panel (`openUpload=0`), you need to login first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:12345/admin/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password": "FileCodeBox2023"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Example:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"detail": {
|
||||||
|
"token": "xxx.xxx.xxx",
|
||||||
|
"token_type": "Bearer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Share API
|
## Share API
|
||||||
|
|
||||||
### Share Text
|
### Share Text
|
||||||
@@ -29,7 +52,22 @@ Share text content and get a share code.
|
|||||||
|-----------|------|----------|---------|-------------|
|
|-----------|------|----------|---------|-------------|
|
||||||
| text | string | Yes | - | Text content to share |
|
| text | string | Yes | - | Text content to share |
|
||||||
| expire_value | integer | No | 1 | Expiration time value |
|
| expire_value | integer | No | 1 | Expiration time value |
|
||||||
| expire_style | string | No | "day" | Expiration time unit(day/hour/minute) |
|
| expire_style | string | No | "day" | Expiration time unit(day/hour/minute/count/forever) |
|
||||||
|
|
||||||
|
**cURL Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Guest upload (when openUpload=1)
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-F "text=This is the text content to share" \
|
||||||
|
-F "expire_value=1" \
|
||||||
|
-F "expire_style=day"
|
||||||
|
|
||||||
|
# When authentication required (openUpload=0)
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "text=This is the text content to share"
|
||||||
|
```
|
||||||
|
|
||||||
**Response Example:**
|
**Response Example:**
|
||||||
|
|
||||||
@@ -38,8 +76,7 @@ Share text content and get a share code.
|
|||||||
"code": 200,
|
"code": 200,
|
||||||
"msg": "success",
|
"msg": "success",
|
||||||
"detail": {
|
"detail": {
|
||||||
"code": "abc123",
|
"code": "abc123"
|
||||||
"name": "text.txt"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -56,7 +93,32 @@ Upload and share a file, get a share code.
|
|||||||
|-----------|------|----------|---------|-------------|
|
|-----------|------|----------|---------|-------------|
|
||||||
| file | file | Yes | - | File to upload |
|
| file | file | Yes | - | File to upload |
|
||||||
| expire_value | integer | No | 1 | Expiration time value |
|
| expire_value | integer | No | 1 | Expiration time value |
|
||||||
| expire_style | string | No | "day" | Expiration time unit(day/hour/minute) |
|
| expire_style | string | No | "day" | Expiration time unit(day/hour/minute/count/forever) |
|
||||||
|
|
||||||
|
**cURL Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Upload file (default 1 day expiration)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.txt"
|
||||||
|
|
||||||
|
# Upload file (7 days expiration)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.txt" \
|
||||||
|
-F "expire_value=7" \
|
||||||
|
-F "expire_style=day"
|
||||||
|
|
||||||
|
# Upload file (10 downloads limit)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.txt" \
|
||||||
|
-F "expire_value=10" \
|
||||||
|
-F "expire_style=count"
|
||||||
|
|
||||||
|
# When authentication required
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "file=@/path/to/file.txt"
|
||||||
|
```
|
||||||
|
|
||||||
**Response Example:**
|
**Response Example:**
|
||||||
|
|
||||||
@@ -75,7 +137,7 @@ Upload and share a file, get a share code.
|
|||||||
|
|
||||||
**GET** `/share/select/`
|
**GET** `/share/select/`
|
||||||
|
|
||||||
Get file information by share code.
|
Get file information by share code (direct file download).
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
@@ -83,6 +145,13 @@ Get file information by share code.
|
|||||||
|-----------|------|----------|-------------|
|
|-----------|------|----------|-------------|
|
||||||
| code | string | Yes | File share code |
|
| code | string | Yes | File share code |
|
||||||
|
|
||||||
|
**cURL Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download file by extraction code
|
||||||
|
curl -L "http://localhost:12345/share/select/?code=abc123" -o downloaded_file
|
||||||
|
```
|
||||||
|
|
||||||
**Response Example:**
|
**Response Example:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -118,12 +118,53 @@ Content-Type: `multipart/form-data`
|
|||||||
**cURL example:**
|
**cURL example:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Upload file (default 1 day expiration)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.pdf"
|
||||||
|
|
||||||
|
# Upload file with 7 days expiration
|
||||||
curl -X POST "http://localhost:12345/share/file/" \
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
-F "file=@/path/to/file.pdf" \
|
-F "file=@/path/to/file.pdf" \
|
||||||
-F "expire_value=7" \
|
-F "expire_value=7" \
|
||||||
-F "expire_style=day"
|
-F "expire_style=day"
|
||||||
|
|
||||||
|
# Upload file with 10 downloads limit
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.pdf" \
|
||||||
|
-F "expire_value=10" \
|
||||||
|
-F "expire_style=count"
|
||||||
|
|
||||||
|
# Share text
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-F "text=This is the text content to share"
|
||||||
|
|
||||||
|
# Download file by extraction code
|
||||||
|
curl -L "http://localhost:12345/share/select/?code=YOUR_CODE" -o downloaded_file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
::: tip When Authentication Required
|
||||||
|
If guest upload is disabled in admin panel (`openUpload=0`), you need to login first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Login to get token
|
||||||
|
curl -X POST "http://localhost:12345/admin/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password": "FileCodeBox2023"}'
|
||||||
|
|
||||||
|
# Returns: {"code":200,"msg":"success","detail":{"token":"xxx.xxx.xxx","token_type":"Bearer"}}
|
||||||
|
|
||||||
|
# 2. Upload file with token
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "file=@/path/to/file.pdf"
|
||||||
|
|
||||||
|
# 3. Share text with token
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "text=This is the text content to share"
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
|
|
||||||
## Chunked Upload API
|
## Chunked Upload API
|
||||||
|
|
||||||
|
|||||||
@@ -118,12 +118,53 @@ Content-Type: `multipart/form-data`
|
|||||||
**cURL 示例:**
|
**cURL 示例:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 上传文件(默认1天有效期)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.pdf"
|
||||||
|
|
||||||
|
# 上传文件并指定有效期(7天)
|
||||||
curl -X POST "http://localhost:12345/share/file/" \
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
-F "file=@/path/to/file.pdf" \
|
-F "file=@/path/to/file.pdf" \
|
||||||
-F "expire_value=7" \
|
-F "expire_value=7" \
|
||||||
-F "expire_style=day"
|
-F "expire_style=day"
|
||||||
|
|
||||||
|
# 上传文件并指定有效期(可下载10次)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/file.pdf" \
|
||||||
|
-F "expire_value=10" \
|
||||||
|
-F "expire_style=count"
|
||||||
|
|
||||||
|
# 分享文本
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-F "text=这是要分享的文本内容"
|
||||||
|
|
||||||
|
# 通过取件码下载文件
|
||||||
|
curl -L "http://localhost:12345/share/select/?code=取件码" -o downloaded_file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
::: tip 需要认证时
|
||||||
|
如果管理面板关闭了游客上传(`openUpload=0`),需要先登录获取 token:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 登录获取 token
|
||||||
|
curl -X POST "http://localhost:12345/admin/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password": "FileCodeBox2023"}'
|
||||||
|
|
||||||
|
# 返回: {"code":200,"msg":"success","detail":{"token":"xxx.xxx.xxx","token_type":"Bearer"}}
|
||||||
|
|
||||||
|
# 2. 使用 token 上传文件
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "file=@/path/to/file.pdf"
|
||||||
|
|
||||||
|
# 3. 使用 token 分享文本
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "text=这是要分享的文本内容"
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
|
||||||
## 分片上传 API
|
## 分片上传 API
|
||||||
|
|
||||||
对于大文件,FileCodeBox 支持分片上传功能。分片上传将大文件分割成多个小块分别上传,支持断点续传。
|
对于大文件,FileCodeBox 支持分片上传功能。分片上传将大文件分割成多个小块分别上传,支持断点续传。
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from core.logger import logger
|
|||||||
from core.response import APIResponse
|
from core.response import APIResponse
|
||||||
from core.settings import data_root, settings, BASE_DIR, DEFAULT_CONFIG
|
from core.settings import data_root, settings, BASE_DIR, DEFAULT_CONFIG
|
||||||
from core.tasks import delete_expire_files, clean_incomplete_uploads
|
from core.tasks import delete_expire_files, clean_incomplete_uploads
|
||||||
|
from core.utils import hash_password, is_password_hashed
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -63,13 +64,26 @@ async def load_config():
|
|||||||
key="sys_start", defaults={"value": int(time.time() * 1000)}
|
key="sys_start", defaults={"value": int(time.time() * 1000)}
|
||||||
)
|
)
|
||||||
settings.user_config = user_config.value
|
settings.user_config = user_config.value
|
||||||
# 更新 ip_limit 配置
|
|
||||||
|
await migrate_password_to_hash()
|
||||||
|
|
||||||
ip_limit["error"].minutes = settings.errorMinute
|
ip_limit["error"].minutes = settings.errorMinute
|
||||||
ip_limit["error"].count = settings.errorCount
|
ip_limit["error"].count = settings.errorCount
|
||||||
ip_limit["upload"].minutes = settings.uploadMinute
|
ip_limit["upload"].minutes = settings.uploadMinute
|
||||||
ip_limit["upload"].count = settings.uploadCount
|
ip_limit["upload"].count = settings.uploadCount
|
||||||
|
|
||||||
|
|
||||||
|
async def migrate_password_to_hash():
|
||||||
|
if not is_password_hashed(settings.admin_token):
|
||||||
|
hashed = hash_password(settings.admin_token)
|
||||||
|
settings.admin_token = hashed
|
||||||
|
config_record = await KeyValue.filter(key="settings").first()
|
||||||
|
if config_record and config_record.value:
|
||||||
|
config_record.value["admin_token"] = hashed
|
||||||
|
await config_record.save()
|
||||||
|
logger.info("已将管理员密码迁移为哈希存储")
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@@ -149,5 +163,9 @@ if __name__ == "__main__":
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
app="main:app", host=settings.serverHost, port=settings.serverPort, reload=False, workers=settings.serverWorkers
|
app="main:app",
|
||||||
|
host=settings.serverHost,
|
||||||
|
port=settings.serverPort,
|
||||||
|
reload=False,
|
||||||
|
workers=settings.serverWorkers,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -267,6 +267,80 @@ python main.py
|
|||||||
2. 输入管理员密码 `FileCodeBox2023`
|
2. 输入管理员密码 `FileCodeBox2023`
|
||||||
3. 管理文件和配置
|
3. 管理文件和配置
|
||||||
|
|
||||||
|
### 命令行上传(curl)
|
||||||
|
|
||||||
|
支持通过 curl 命令上传文件并获取取件码:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 上传文件(默认1天有效期)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/your/file.txt"
|
||||||
|
|
||||||
|
# 上传文件并指定有效期(1小时)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/your/file.txt" \
|
||||||
|
-F "expire_value=1" \
|
||||||
|
-F "expire_style=hour"
|
||||||
|
|
||||||
|
# 上传文件并指定有效期(可下载10次)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/your/file.txt" \
|
||||||
|
-F "expire_value=10" \
|
||||||
|
-F "expire_style=count"
|
||||||
|
|
||||||
|
# 分享文本
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-F "text=这是要分享的文本内容"
|
||||||
|
|
||||||
|
# 通过取件码下载文件
|
||||||
|
curl -L "http://localhost:12345/share/select/?code=取件码" -o downloaded_file
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明:**
|
||||||
|
- `expire_value`: 有效期数值(默认1)
|
||||||
|
- `expire_style`: 有效期类型
|
||||||
|
- `day` - 天数
|
||||||
|
- `hour` - 小时
|
||||||
|
- `minute` - 分钟
|
||||||
|
- `count` - 下载次数
|
||||||
|
- `forever` - 永久有效
|
||||||
|
|
||||||
|
**返回示例:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"detail": {
|
||||||
|
"code": "abcd1234",
|
||||||
|
"name": "file.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注意:如果管理面板关闭了游客上传(`openUpload=false`),需要先登录获取 token,然后在请求中添加 `Authorization: Bearer <token>` 头。
|
||||||
|
|
||||||
|
**需要认证时的用法:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 先登录获取 token
|
||||||
|
curl -X POST "http://localhost:12345/admin/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password": "FileCodeBox2023"}'
|
||||||
|
|
||||||
|
# 返回示例:
|
||||||
|
# {"code":200,"msg":"success","detail":{"token":"xxx.xxx.xxx","token_type":"Bearer"}}
|
||||||
|
|
||||||
|
# 2. 使用 token 上传文件
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "file=@/path/to/your/file.txt"
|
||||||
|
|
||||||
|
# 2. 使用 token 分享文本
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "text=这是要分享的文本内容"
|
||||||
|
```
|
||||||
|
|
||||||
## 🛠 开发指南
|
## 🛠 开发指南
|
||||||
|
|
||||||
### 项目结构
|
### 项目结构
|
||||||
|
|||||||
+76
-2
@@ -259,10 +259,84 @@ python main.py
|
|||||||
|
|
||||||
### Admin Panel
|
### Admin Panel
|
||||||
|
|
||||||
1. Visit `/admin`
|
1. Visit `/#/admin`
|
||||||
2. Enter admin password
|
2. Enter admin password `FileCodeBox2023`
|
||||||
3. Manage files and settings
|
3. Manage files and settings
|
||||||
|
|
||||||
|
### Command Line Upload (curl)
|
||||||
|
|
||||||
|
Upload files and get extraction codes via curl:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Upload file (default 1 day expiration)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/your/file.txt"
|
||||||
|
|
||||||
|
# Upload file with expiration (1 hour)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/your/file.txt" \
|
||||||
|
-F "expire_value=1" \
|
||||||
|
-F "expire_style=hour"
|
||||||
|
|
||||||
|
# Upload file with download limit (10 downloads)
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-F "file=@/path/to/your/file.txt" \
|
||||||
|
-F "expire_value=10" \
|
||||||
|
-F "expire_style=count"
|
||||||
|
|
||||||
|
# Share text
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-F "text=This is the text content to share"
|
||||||
|
|
||||||
|
# Download file by extraction code
|
||||||
|
curl -L "http://localhost:12345/share/select/?code=YOUR_CODE" -o downloaded_file
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `expire_value`: Expiration value (default 1)
|
||||||
|
- `expire_style`: Expiration type
|
||||||
|
- `day` - Days
|
||||||
|
- `hour` - Hours
|
||||||
|
- `minute` - Minutes
|
||||||
|
- `count` - Download count
|
||||||
|
- `forever` - Never expire
|
||||||
|
|
||||||
|
**Response Example:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"detail": {
|
||||||
|
"code": "abcd1234",
|
||||||
|
"name": "file.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: If guest upload is disabled in admin panel (`openUpload=false`), you need to login first to get a token, then add `Authorization: Bearer <token>` header to your requests.
|
||||||
|
|
||||||
|
**When Authentication Required:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Login to get token
|
||||||
|
curl -X POST "http://localhost:12345/admin/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"password": "FileCodeBox2023"}'
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
# {"code":200,"msg":"success","detail":{"token":"xxx.xxx.xxx","token_type":"Bearer"}}
|
||||||
|
|
||||||
|
# 2. Upload file with token
|
||||||
|
curl -X POST "http://localhost:12345/share/file/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "file=@/path/to/your/file.txt"
|
||||||
|
|
||||||
|
# 3. Share text with token
|
||||||
|
curl -X POST "http://localhost:12345/share/text/" \
|
||||||
|
-H "Authorization: Bearer xxx.xxx.xxx" \
|
||||||
|
-F "text=This is the text content to share"
|
||||||
|
```
|
||||||
|
|
||||||
## 🛠 Development Guide
|
## 🛠 Development Guide
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
import{c as l,d as _,u as M,r as m,f as p,R as C,a as b,o as h,b as t,n as o,e,i as z,l as i,I as D,t as f,X as L,y as B,D as j,G as E,k as F,E as I,m as N}from"./index-B3zfsvgW.js";import{B as R}from"./box-COy3AQAQ.js";const S=l("cog",[["path",{d:"M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z",key:"sobvz5"}],["path",{d:"M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z",key:"11i496"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 22v-2",key:"1osdcq"}],["path",{d:"m17 20.66-1-1.73",key:"eq3orb"}],["path",{d:"M11 10.27 7 3.34",key:"16pf9h"}],["path",{d:"m20.66 17-1.73-1",key:"sg0v6f"}],["path",{d:"m3.34 7 1.73 1",key:"1ulond"}],["path",{d:"M14 12h8",key:"4f43i9"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"m20.66 7-1.73 1",key:"1ow05n"}],["path",{d:"m3.34 17 1.73-1",key:"nuk764"}],["path",{d:"m17 3.34-1 1.73",key:"2wel8s"}],["path",{d:"m11 13.73-4 6.93",key:"794ttg"}]]);const V=l("folder",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]]);const Z=l("layout-dashboard",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);const q=l("menu",[["path",{d:"M4 12h16",key:"1lakjw"}],["path",{d:"M4 18h16",key:"19g7jn"}],["path",{d:"M4 6h16",key:"1o0s65"}]]),A={class:"flex items-center"},$={class:"rounded-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 p-1 animate-spin-slow"},G={class:"flex-1 overflow-y-auto custom-scrollbar"},H={class:"p-4 space-y-2"},O=["onClick"],T={class:"flex-1 flex flex-col h-full"},U={class:"flex items-center justify-between h-16 px-4"},P=_({__name:"AdminLayout",setup(W){const d=D(),{t:n}=M(),a=z("isDarkMode"),x=[{id:"Dashboard",name:n("admin.dashboard.title"),icon:Z,redirect:"/admin/dashboard"},{id:"FileManage",name:n("admin.fileManage.title"),icon:V,redirect:"/admin/files"},{id:"Settings",name:n("admin.settings.title"),icon:S,redirect:"/admin/settings"}],r=m(!0),g=()=>{r.value=!r.value},c=()=>{window.innerWidth>=1024?r.value=!0:r.value=!1};p(()=>{c(),window.addEventListener("resize",c)}),C(()=>{window.removeEventListener("resize",c)});const k=m({page:1,size:10,total:0}),v=async()=>{try{k.value.total=85}catch(u){console.error("加载文件列表失败:",u)}};return p(()=>{v()}),(u,y)=>{const w=E("router-view");return h(),b("div",{class:o(["h-screen flex flex-col lg:flex-row transition-colors duration-300",[e(a)?"bg-gray-900":"bg-gray-50"]])},[t("aside",{class:o(["fixed inset-y-0 border-transparent left-0 z-50 w-64 transform transition-all duration-300 ease-in-out lg:relative lg:translate-x-0 border-r flex flex-col h-full lg:h-screen",[e(a)?"bg-gray-800 bg-opacity-90 backdrop-filter backdrop-blur-xl ":"bg-white ",{"-translate-x-full":!r.value}]])},[t("div",{class:o(["flex items-center justify-between h-16 px-4 border-b",[e(a)?"border-gray-700":"border-gray-200"]])},[t("div",A,[t("div",$,[t("div",{class:o(["rounded-full p-1",[e(a)?"bg-gray-800":"bg-white"]])},[i(e(R),{class:o(["w-6 h-6",[e(a)?"text-indigo-400":"text-indigo-600"]])},null,8,["class"])],2)]),t("h1",{onClick:y[0]||(y[0]=s=>e(d).push("/")),class:o(["ml-2 text-xl font-semibold cursor-pointer",[e(a)?"text-white":"text-gray-800"]])},f(e(n)("common.appName")),3)]),t("button",{onClick:g,class:"lg:hidden"},[i(e(L),{class:o(["w-6 h-6",[e(a)?"text-gray-400":"text-gray-600"]])},null,8,["class"])])],2),t("nav",G,[t("ul",H,[(h(),b(B,null,j(x,s=>t("li",{key:s.id},[t("a",{onClick:X=>e(d).push(s.redirect),class:o(["flex items-center p-2 rounded-lg transition-colors duration-200",[e(d).currentRoute.value.name===s.id?e(a)?"bg-indigo-900 text-indigo-400":"bg-indigo-100 text-indigo-600":e(a)?"text-gray-400 hover:bg-gray-700":"text-gray-600 hover:bg-gray-100"]])},[(h(),F(N(s.icon),{class:"w-5 h-5 mr-3"})),I(" "+f(s.name),1)],10,O)])),64))])])],2),t("div",T,[t("header",{class:o(["shadow-md border-b transition-colors duration-300 h-16",[e(a)?"bg-gray-800 border-gray-700":"bg-white border-gray-200"]])},[t("div",U,[t("button",{onClick:g,class:"lg:hidden"},[i(e(q),{class:o(["w-6 h-6",[e(a)?"text-gray-400":"text-gray-600"]])},null,8,["class"])])])],2),t("main",{class:o(["overflow-y-auto transition-colors duration-300 custom-scrollbar",[e(a)?"bg-gray-900":"bg-gray-50"]])},[i(w)],2)])],2)}}});export{P as default};
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import{c as l,d as _,u as M,r as m,f as p,R as C,a as b,o as h,b as t,n as o,e,i as z,l as i,I as D,t as f,X as L,y as B,D as j,G as E,k as F,E as I,m as N}from"./index-B9FIg8c4.js";import{B as R}from"./box-BaIzuVAS.js";/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const S=l("cog",[["path",{d:"M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z",key:"sobvz5"}],["path",{d:"M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z",key:"11i496"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M12 22v-2",key:"1osdcq"}],["path",{d:"m17 20.66-1-1.73",key:"eq3orb"}],["path",{d:"M11 10.27 7 3.34",key:"16pf9h"}],["path",{d:"m20.66 17-1.73-1",key:"sg0v6f"}],["path",{d:"m3.34 7 1.73 1",key:"1ulond"}],["path",{d:"M14 12h8",key:"4f43i9"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"m20.66 7-1.73 1",key:"1ow05n"}],["path",{d:"m3.34 17 1.73-1",key:"nuk764"}],["path",{d:"m17 3.34-1 1.73",key:"2wel8s"}],["path",{d:"m11 13.73-4 6.93",key:"794ttg"}]]);/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const V=l("folder",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}]]);/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const Z=l("layout-dashboard",[["rect",{width:"7",height:"9",x:"3",y:"3",rx:"1",key:"10lvy0"}],["rect",{width:"7",height:"5",x:"14",y:"3",rx:"1",key:"16une8"}],["rect",{width:"7",height:"9",x:"14",y:"12",rx:"1",key:"1hutg5"}],["rect",{width:"7",height:"5",x:"3",y:"16",rx:"1",key:"ldoo1y"}]]);/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const q=l("menu",[["path",{d:"M4 12h16",key:"1lakjw"}],["path",{d:"M4 18h16",key:"19g7jn"}],["path",{d:"M4 6h16",key:"1o0s65"}]]),A={class:"flex items-center"},$={class:"rounded-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 p-1 animate-spin-slow"},G={class:"flex-1 overflow-y-auto custom-scrollbar"},H={class:"p-4 space-y-2"},O=["onClick"],T={class:"flex-1 flex flex-col h-full"},U={class:"flex items-center justify-between h-16 px-4"},P=_({__name:"AdminLayout",setup(W){const d=D(),{t:n}=M(),a=z("isDarkMode"),x=[{id:"Dashboard",name:n("admin.dashboard.title"),icon:Z,redirect:"/admin/dashboard"},{id:"FileManage",name:n("admin.fileManage.title"),icon:V,redirect:"/admin/files"},{id:"Settings",name:n("admin.settings.title"),icon:S,redirect:"/admin/settings"}],r=m(!0),g=()=>{r.value=!r.value},c=()=>{window.innerWidth>=1024?r.value=!0:r.value=!1};p(()=>{c(),window.addEventListener("resize",c)}),C(()=>{window.removeEventListener("resize",c)});const k=m({page:1,size:10,total:0}),v=async()=>{try{k.value.total=85}catch(u){console.error("加载文件列表失败:",u)}};return p(()=>{v()}),(u,y)=>{const w=E("router-view");return h(),b("div",{class:o(["h-screen flex flex-col lg:flex-row transition-colors duration-300",[e(a)?"bg-gray-900":"bg-gray-50"]])},[t("aside",{class:o(["fixed inset-y-0 border-transparent left-0 z-50 w-64 transform transition-all duration-300 ease-in-out lg:relative lg:translate-x-0 border-r flex flex-col h-full lg:h-screen",[e(a)?"bg-gray-800 bg-opacity-90 backdrop-filter backdrop-blur-xl ":"bg-white ",{"-translate-x-full":!r.value}]])},[t("div",{class:o(["flex items-center justify-between h-16 px-4 border-b",[e(a)?"border-gray-700":"border-gray-200"]])},[t("div",A,[t("div",$,[t("div",{class:o(["rounded-full p-1",[e(a)?"bg-gray-800":"bg-white"]])},[i(e(R),{class:o(["w-6 h-6",[e(a)?"text-indigo-400":"text-indigo-600"]])},null,8,["class"])],2)]),t("h1",{onClick:y[0]||(y[0]=s=>e(d).push("/")),class:o(["ml-2 text-xl font-semibold cursor-pointer",[e(a)?"text-white":"text-gray-800"]])},f(e(n)("common.appName")),3)]),t("button",{onClick:g,class:"lg:hidden"},[i(e(L),{class:o(["w-6 h-6",[e(a)?"text-gray-400":"text-gray-600"]])},null,8,["class"])])],2),t("nav",G,[t("ul",H,[(h(),b(B,null,j(x,s=>t("li",{key:s.id},[t("a",{onClick:X=>e(d).push(s.redirect),class:o(["flex items-center p-2 rounded-lg transition-colors duration-200",[e(d).currentRoute.value.name===s.id?e(a)?"bg-indigo-900 text-indigo-400":"bg-indigo-100 text-indigo-600":e(a)?"text-gray-400 hover:bg-gray-700":"text-gray-600 hover:bg-gray-100"]])},[(h(),F(N(s.icon),{class:"w-5 h-5 mr-3"})),I(" "+f(s.name),1)],10,O)])),64))])])],2),t("div",T,[t("header",{class:o(["shadow-md border-b transition-colors duration-300 h-16",[e(a)?"bg-gray-800 border-gray-700":"bg-white border-gray-200"]])},[t("div",U,[t("button",{onClick:g,class:"lg:hidden"},[i(e(q),{class:o(["w-6 h-6",[e(a)?"text-gray-400":"text-gray-600"]])},null,8,["class"])])])],2),t("main",{class:o(["overflow-y-auto transition-colors duration-300 custom-scrollbar",[e(a)?"bg-gray-900":"bg-gray-50"]])},[i(w)],2)])],2)}}});export{P as default};
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
.switch{position:relative;display:inline-block;width:60px;height:34px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;inset:0;background-color:#e5e7eb;transition:.4s}.dark .slider{background-color:#4b5563}input:checked+.slider{background-color:#4f46e5}.dark input:checked+.slider{background-color:#4f46e5}.slider:before{position:absolute;content:"";height:26px;width:26px;left:4px;bottom:4px;background-color:#fff;transition:.4s}.dark .slider:before{background-color:#e5e7eb}.slider.round{border-radius:34px}.slider.round:before{border-radius:50%}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.animate-spin-slow{animation:spin 8s linear infinite}.transition-colors{transition-property:background-color,border-color,color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}.custom-scrollbar::-webkit-scrollbar{width:6px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background-color:#cbd5e0;border-radius:3px}:global(.dark) .custom-scrollbar::-webkit-scrollbar-thumb{background-color:#4a5568}
|
.switch{position:relative;display:inline-block;width:60px;height:34px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;inset:0;background-color:#e5e7eb;transition:.4s}.dark .slider{background-color:#4b5563}input:checked+.slider{background-color:#4f46e5}.dark input:checked+.slider{background-color:#4f46e5}.slider:before{position:absolute;content:"";height:26px;width:26px;left:4px;bottom:4px;background-color:#fff;transition:.4s}.dark .slider:before{background-color:#e5e7eb}.slider.round{border-radius:34px}.slider.round:before{border-radius:50%}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.animate-spin-slow{animation:spin 8s linear infinite}.transition-colors{transition-property:background-color,border-color,color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}.custom-scrollbar::-webkit-scrollbar{width:6px}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}.custom-scrollbar::-webkit-scrollbar-thumb{background-color:#cbd5e0;border-radius:3px}:is():hover{background-color:#a0aec0}:global(.dark) .custom-scrollbar::-webkit-scrollbar-thumb{background-color:#4a5568}:is():hover{background-color:#2d3748}
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{c as _,d as S,g as x,a as M,o as v,n as d,e as t,i as k,b as s,t as r,k as w,m as D,K as F,u as U,V as B,f as z,l as g,v as m,E as C,W as $}from"./index-B3zfsvgW.js";import{F as T}from"./file-Ceuyr6iv.js";import{H as V}from"./hard-drive-5GIKYPKr.js";const H=_("activity",[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]]);const N=_("users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["path",{d:"M16 3.128a4 4 0 0 1 0 7.744",key:"16gr8j"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}]]),j={class:"flex items-center justify-between"},b=S({__name:"StatCard",props:{title:{},value:{},icon:{},iconColor:{},descriptionType:{default:"neutral"}},setup(f){const l=f,e=k("isDarkMode"),a=x(()=>({indigo:e?"bg-indigo-900":"bg-indigo-100",purple:e?"bg-purple-900":"bg-purple-100",green:e?"bg-green-900":"bg-green-100",blue:e?"bg-blue-900":"bg-blue-100"})[l.iconColor]),h=x(()=>({indigo:e?"text-indigo-400":"text-indigo-600",purple:e?"text-purple-400":"text-purple-600",green:e?"text-green-400":"text-green-600",blue:e?"text-blue-400":"text-blue-600"})[l.iconColor]),p=x(()=>({success:e?"text-green-400":"text-green-600",error:e?"text-red-400":"text-red-600",neutral:e?"text-gray-400":"text-gray-600"})[l.descriptionType]);return(i,o)=>(v(),M("div",{class:d(["p-6 rounded-lg shadow-md transition-colors duration-300",[t(e)?"bg-gray-800 bg-opacity-70":"bg-white"]])},[s("div",j,[s("div",null,[s("p",{class:d(["text-sm",[t(e)?"text-gray-400":"text-gray-600"]])},r(i.title),3),s("h3",{class:d(["text-2xl font-bold mt-1",[t(e)?"text-white":"text-gray-800"]])},r(i.value),3)]),s("div",{class:d(["p-3 rounded-full",a.value])},[(v(),w(D(i.icon),{class:d(["w-6 h-6",h.value])},null,8,["class"]))],2)]),s("p",{class:d(["text-sm mt-2",p.value])},[F(i.$slots,"description")],2)],2))}}),I={class:"p-6 overflow-y-auto custom-scrollbar"},L={class:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"},A={class:"ml-2"},E={class:"ml-2"},K={class:"text-sm mt-1"},Y=S({__name:"DashboardView",setup(f){const l=k("isDarkMode"),{t:e}=U(),a=B({totalFiles:0,storageUsed:0,yesterdayCount:0,todayCount:0,yesterdaySize:0,todaySize:0,sysUptime:0}),h=o=>{const c=new Date().getTime()-o,u=Math.floor(c/(1440*60*1e3)),y=Math.floor(c%(1440*60*1e3)/(3600*1e3));return`${u}天${y}小时`},p=o=>{const n=parseInt(o)/1024,c=n/1024,u=c/1024,y=u/1024;return y>1?`${y.toFixed(2)}TB`:u>1?`${u.toFixed(2)}GB`:c>1?`${c.toFixed(2)}MB`:n>1?`${n.toFixed(2)}KB`:`${o}B`},i=async()=>{const o=await $.getDashboard();o.detail&&(a.totalFiles=o.detail.totalFiles,a.storageUsed=p(o.detail.storageUsed.toString()),a.yesterdaySize=p(o.detail.yesterdaySize.toString()),a.todaySize=p(o.detail.todaySize.toString()),a.yesterdayCount=o.detail.yesterdayCount,a.todayCount=o.detail.todayCount,a.sysUptime=h(Number(o.detail.sysUptime)))};return z(()=>{i()}),(o,n)=>(v(),M("div",I,[s("h2",{class:d(["text-2xl font-bold mb-6",[t(l)?"text-white":"text-gray-800"]])},r(t(e)("admin.dashboard.title")),3),s("div",L,[g(b,{title:t(e)("admin.dashboard.totalFiles"),value:a.totalFiles,icon:t(T),"icon-color":"indigo","description-type":"success"},{description:m(()=>[s("span",null,r(t(e)("admin.dashboard.yesterday"))+":"+r(a.yesterdayCount),1),s("span",A,r(t(e)("admin.dashboard.today"))+":"+r(a.todayCount),1)]),_:1},8,["title","value","icon"]),g(b,{title:t(e)("admin.dashboard.storageSpace"),value:a.storageUsed,icon:t(V),"icon-color":"purple","description-type":"success"},{description:m(()=>[s("span",null,r(t(e)("admin.dashboard.yesterday"))+":"+r(a.yesterdaySize),1),s("span",E,r(t(e)("admin.dashboard.today"))+":"+r(a.todaySize),1)]),_:1},8,["title","value","icon"]),g(b,{title:t(e)("admin.dashboard.activeUsers"),value:"25",icon:t(N),"icon-color":"green","description-type":"error"},{description:m(()=>[s("span",null,r(t(e)("admin.dashboard.weeklyChange")),1)]),_:1},8,["title","icon"]),g(b,{title:t(e)("admin.dashboard.systemStatus"),value:t(e)("admin.dashboard.normal"),icon:t(H),"icon-color":"blue","description-type":"neutral"},{description:m(()=>[C(r(t(e)("admin.dashboard.serverUptime"))+": "+r(a.sysUptime),1)]),_:1},8,["title","value","icon"])]),s("div",{class:d(["mt-auto text-center py-4",[t(l)?"text-gray-400":"text-gray-600"]])},[n[1]||(n[1]=s("p",{class:"text-sm"}," 版本 v2.2.1 更新时间:2025-09-04 ",-1)),s("p",K,[C(" © "+r(new Date().getFullYear())+" ",1),n[0]||(n[0]=s("a",{href:"https://github.com/vastsa/FileCodeBox"},"FileCodeBox",-1))])],2)]))}});export{Y as default};
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import{c as _,d as S,g as x,a as M,o as v,n as d,e as t,i as k,b as s,t as r,k as w,m as D,K as F,u as U,V as B,f as z,l as g,v as m,E as C,W as $}from"./index-B9FIg8c4.js";import{F as T}from"./file-CSeBUDan.js";import{H as V}from"./hard-drive-DFfw9-r7.js";/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const H=_("activity",[["path",{d:"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2",key:"169zse"}]]);/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const N=_("users",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["path",{d:"M16 3.128a4 4 0 0 1 0 7.744",key:"16gr8j"}],["path",{d:"M22 21v-2a4 4 0 0 0-3-3.87",key:"kshegd"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}]]),j={class:"flex items-center justify-between"},b=S({__name:"StatCard",props:{title:{},value:{},icon:{},iconColor:{},descriptionType:{default:"neutral"}},setup(f){const l=f,e=k("isDarkMode"),a=x(()=>({indigo:e?"bg-indigo-900":"bg-indigo-100",purple:e?"bg-purple-900":"bg-purple-100",green:e?"bg-green-900":"bg-green-100",blue:e?"bg-blue-900":"bg-blue-100"})[l.iconColor]),h=x(()=>({indigo:e?"text-indigo-400":"text-indigo-600",purple:e?"text-purple-400":"text-purple-600",green:e?"text-green-400":"text-green-600",blue:e?"text-blue-400":"text-blue-600"})[l.iconColor]),p=x(()=>({success:e?"text-green-400":"text-green-600",error:e?"text-red-400":"text-red-600",neutral:e?"text-gray-400":"text-gray-600"})[l.descriptionType]);return(i,o)=>(v(),M("div",{class:d(["p-6 rounded-lg shadow-md transition-colors duration-300",[t(e)?"bg-gray-800 bg-opacity-70":"bg-white"]])},[s("div",j,[s("div",null,[s("p",{class:d(["text-sm",[t(e)?"text-gray-400":"text-gray-600"]])},r(i.title),3),s("h3",{class:d(["text-2xl font-bold mt-1",[t(e)?"text-white":"text-gray-800"]])},r(i.value),3)]),s("div",{class:d(["p-3 rounded-full",a.value])},[(v(),w(D(i.icon),{class:d(["w-6 h-6",h.value])},null,8,["class"]))],2)]),s("p",{class:d(["text-sm mt-2",p.value])},[F(i.$slots,"description")],2)],2))}}),I={class:"p-6 overflow-y-auto custom-scrollbar"},L={class:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"},A={class:"ml-2"},E={class:"ml-2"},K={class:"text-sm mt-1"},Y=S({__name:"DashboardView",setup(f){const l=k("isDarkMode"),{t:e}=U(),a=B({totalFiles:0,storageUsed:0,yesterdayCount:0,todayCount:0,yesterdaySize:0,todaySize:0,sysUptime:0}),h=o=>{const c=new Date().getTime()-o,u=Math.floor(c/(1440*60*1e3)),y=Math.floor(c%(1440*60*1e3)/(3600*1e3));return`${u}天${y}小时`},p=o=>{const n=parseInt(o)/1024,c=n/1024,u=c/1024,y=u/1024;return y>1?`${y.toFixed(2)}TB`:u>1?`${u.toFixed(2)}GB`:c>1?`${c.toFixed(2)}MB`:n>1?`${n.toFixed(2)}KB`:`${o}B`},i=async()=>{const o=await $.getDashboard();o.detail&&(a.totalFiles=o.detail.totalFiles,a.storageUsed=p(o.detail.storageUsed.toString()),a.yesterdaySize=p(o.detail.yesterdaySize.toString()),a.todaySize=p(o.detail.todaySize.toString()),a.yesterdayCount=o.detail.yesterdayCount,a.todayCount=o.detail.todayCount,a.sysUptime=h(Number(o.detail.sysUptime)))};return z(()=>{i()}),(o,n)=>(v(),M("div",I,[s("h2",{class:d(["text-2xl font-bold mb-6",[t(l)?"text-white":"text-gray-800"]])},r(t(e)("admin.dashboard.title")),3),s("div",L,[g(b,{title:t(e)("admin.dashboard.totalFiles"),value:a.totalFiles,icon:t(T),"icon-color":"indigo","description-type":"success"},{description:m(()=>[s("span",null,r(t(e)("admin.dashboard.yesterday"))+":"+r(a.yesterdayCount),1),s("span",A,r(t(e)("admin.dashboard.today"))+":"+r(a.todayCount),1)]),_:1},8,["title","value","icon"]),g(b,{title:t(e)("admin.dashboard.storageSpace"),value:a.storageUsed,icon:t(V),"icon-color":"purple","description-type":"success"},{description:m(()=>[s("span",null,r(t(e)("admin.dashboard.yesterday"))+":"+r(a.yesterdaySize),1),s("span",E,r(t(e)("admin.dashboard.today"))+":"+r(a.todaySize),1)]),_:1},8,["title","value","icon"]),g(b,{title:t(e)("admin.dashboard.activeUsers"),value:"25",icon:t(N),"icon-color":"green","description-type":"error"},{description:m(()=>[s("span",null,r(t(e)("admin.dashboard.weeklyChange")),1)]),_:1},8,["title","icon"]),g(b,{title:t(e)("admin.dashboard.systemStatus"),value:t(e)("admin.dashboard.normal"),icon:t(H),"icon-color":"blue","description-type":"neutral"},{description:m(()=>[C(r(t(e)("admin.dashboard.serverUptime"))+": "+r(a.sysUptime),1)]),_:1},8,["title","value","icon"])]),s("div",{class:d(["mt-auto text-center py-4",[t(l)?"text-gray-400":"text-gray-600"]])},[n[1]||(n[1]=s("p",{class:"text-sm"}," 版本 v2.2.1 更新时间:2025-09-04 ",-1)),s("p",K,[C(" © "+r(new Date().getFullYear())+" ",1),n[0]||(n[0]=s("a",{href:"https://github.com/vastsa/FileCodeBox"},"FileCodeBox",-1))])],2)]))}});export{Y as default};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{Q as y,r as c,S as u,g as b,d as w,p as x,a as h,o as S,b as e,n as d,e as i,i as A,l as k,h as I,x as D,z as N,E as T,t as _,a1 as E,I as M,_ as O}from"./index-B3zfsvgW.js";import{B as V}from"./box-COy3AQAQ.js";const j=y("admin",()=>{const p=c(localStorage.getItem(u.ADMIN_PASSWORD)||""),s=c(localStorage.getItem(u.TOKEN)||""),a=c(!1),l=c(null),n=b(()=>a.value&&!!s.value),f=o=>{p.value=o,localStorage.setItem(u.ADMIN_PASSWORD,o)},g=o=>{s.value=o,localStorage.setItem(u.TOKEN,o)},m=o=>{l.value=o,a.value=!0,g(o.token)};return{adminPassword:p,token:s,isLoggedIn:a,userInfo:l,isAuthenticated:n,updateAdminPassword:f,setToken:g,setUserInfo:m,login:o=>{m(o)},logout:()=>{p.value="",s.value="",a.value=!1,l.value=null,localStorage.removeItem(u.ADMIN_PASSWORD),localStorage.removeItem(u.TOKEN)},initAuth:()=>{const o=localStorage.getItem(u.TOKEN);o&&(s.value=o,a.value=!0)}}}),B=j,K={class:"mx-auto h-16 w-16 relative"},P={class:"rounded-md shadow-sm -space-y-px"},R=["disabled"],z=w({__name:"LoginView",setup(p){const s=x(),a=c(""),l=c(!1),n=A("isDarkMode"),f=B(),g=()=>{let r=!0;return a.value?a.value.length<6&&(s.showAlert("密码长度至少为6位","error"),r=!1):(s.showAlert("无效的密码","error"),r=!1),r},m=M(),v=async()=>{if(g()){l.value=!0;try{const r=await E.login(a.value);r.detail?.token?(f.setToken(r.detail.token),m.push("/admin")):s.showAlert("登录失败:未获取到有效令牌","error")}catch(r){const t=r&&typeof r=="object"&&"response"in r&&r.response?.data?.detail||"登录失败";s.showAlert(t,"error")}finally{l.value=!1}}};return(r,t)=>(S(),h("div",{class:d(["min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 transition-colors duration-200 relative overflow-hidden",i(n)?"bg-gray-900":"bg-gray-50"])},[t[6]||(t[6]=e("div",{class:"absolute inset-0 z-0"},[e("div",{class:"cyber-grid"}),e("div",{class:"floating-particles"})],-1)),e("div",{class:d(["max-w-md w-full space-y-8 backdrop-blur-lg bg-opacity-20 p-8 rounded-xl border border-opacity-20",[i(n)?"bg-gray-800 border-gray-600":"bg-white/70 border-gray-200"]])},[e("div",null,[e("div",K,[t[1]||(t[1]=e("div",{class:"absolute inset-0 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full animate-spin-slow"},null,-1)),t[2]||(t[2]=e("div",{class:"absolute -inset-2 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full opacity-50 blur-md animate-pulse"},null,-1)),e("div",{class:d(["absolute inset-1 rounded-full flex items-center justify-center",i(n)?"bg-gray-800":"bg-white"])},[k(i(V),{class:d(["h-8 w-8",i(n)?"text-cyan-400":"text-cyan-600"])},null,8,["class"])],2)]),e("h2",{class:d(["mt-6 text-center text-3xl font-extrabold",i(n)?"text-white":"text-gray-900"])}," 登录 ",2)]),e("form",{class:"mt-8 space-y-6",onSubmit:I(v,["prevent"])},[t[5]||(t[5]=e("input",{type:"hidden",name:"remember",value:"true"},null,-1)),e("div",P,[e("div",null,[t[3]||(t[3]=e("label",{for:"password",class:"sr-only"},"密码",-1)),D(e("input",{id:"password",name:"password",type:"password",autocomplete:"current-password",required:"","onUpdate:modelValue":t[0]||(t[0]=o=>a.value=o),class:d(["appearance-none rounded-t-md relative block w-full px-4 py-3 border transition-all duration-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm backdrop-blur-sm",i(n)?"bg-gray-800/50 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500":"bg-white/50 border-gray-300 text-gray-900 hover:border-gray-400"]),placeholder:"密码"},null,2),[[N,a.value]])])]),e("div",null,[e("button",{type:"submit",class:d(["group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white transition-all duration-300 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 shadow-lg hover:shadow-cyan-500/50",i(n)?"bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600":"bg-gradient-to-r from-cyan-600 to-purple-600 hover:from-cyan-700 hover:to-purple-700",l.value?"opacity-75 cursor-not-allowed":""]),disabled:l.value},[t[4]||(t[4]=e("span",{class:"absolute left-0 inset-y-0 flex items-center pl-3"},null,-1)),T(" "+_(l.value?"登录中...":"登录"),1)],10,R)])],32)],2)],2))}}),C=O(z,[["__scopeId","data-v-894571a2"]]);export{C as default};
|
import{Q as y,r as c,S as u,g as b,d as w,p as x,a as h,o as S,b as e,n as d,e as i,i as A,l as k,h as I,x as D,z as N,E as T,t as _,a1 as E,I as M,_ as O}from"./index-B9FIg8c4.js";import{B as V}from"./box-BaIzuVAS.js";const j=y("admin",()=>{const p=c(localStorage.getItem(u.ADMIN_PASSWORD)||""),s=c(localStorage.getItem(u.TOKEN)||""),a=c(!1),l=c(null),n=b(()=>a.value&&!!s.value),f=o=>{p.value=o,localStorage.setItem(u.ADMIN_PASSWORD,o)},g=o=>{s.value=o,localStorage.setItem(u.TOKEN,o)},m=o=>{l.value=o,a.value=!0,g(o.token)};return{adminPassword:p,token:s,isLoggedIn:a,userInfo:l,isAuthenticated:n,updateAdminPassword:f,setToken:g,setUserInfo:m,login:o=>{m(o)},logout:()=>{p.value="",s.value="",a.value=!1,l.value=null,localStorage.removeItem(u.ADMIN_PASSWORD),localStorage.removeItem(u.TOKEN)},initAuth:()=>{const o=localStorage.getItem(u.TOKEN);o&&(s.value=o,a.value=!0)}}}),B=j,K={class:"mx-auto h-16 w-16 relative"},P={class:"rounded-md shadow-sm -space-y-px"},R=["disabled"],z=w({__name:"LoginView",setup(p){const s=x(),a=c(""),l=c(!1),n=A("isDarkMode"),f=B(),g=()=>{let r=!0;return a.value?a.value.length<6&&(s.showAlert("密码长度至少为6位","error"),r=!1):(s.showAlert("无效的密码","error"),r=!1),r},m=M(),v=async()=>{if(g()){l.value=!0;try{const r=await E.login(a.value);r.detail?.token?(f.setToken(r.detail.token),m.push("/admin")):s.showAlert("登录失败:未获取到有效令牌","error")}catch(r){const t=r&&typeof r=="object"&&"response"in r&&r.response?.data?.detail||"登录失败";s.showAlert(t,"error")}finally{l.value=!1}}};return(r,t)=>(S(),h("div",{class:d(["min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 transition-colors duration-200 relative overflow-hidden",i(n)?"bg-gray-900":"bg-gray-50"])},[t[6]||(t[6]=e("div",{class:"absolute inset-0 z-0"},[e("div",{class:"cyber-grid"}),e("div",{class:"floating-particles"})],-1)),e("div",{class:d(["max-w-md w-full space-y-8 backdrop-blur-lg bg-opacity-20 p-8 rounded-xl border border-opacity-20",[i(n)?"bg-gray-800 border-gray-600":"bg-white/70 border-gray-200"]])},[e("div",null,[e("div",K,[t[1]||(t[1]=e("div",{class:"absolute inset-0 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full animate-spin-slow"},null,-1)),t[2]||(t[2]=e("div",{class:"absolute -inset-2 bg-gradient-to-r from-cyan-500 via-purple-500 to-pink-500 rounded-full opacity-50 blur-md animate-pulse"},null,-1)),e("div",{class:d(["absolute inset-1 rounded-full flex items-center justify-center",i(n)?"bg-gray-800":"bg-white"])},[k(i(V),{class:d(["h-8 w-8",i(n)?"text-cyan-400":"text-cyan-600"])},null,8,["class"])],2)]),e("h2",{class:d(["mt-6 text-center text-3xl font-extrabold",i(n)?"text-white":"text-gray-900"])}," 登录 ",2)]),e("form",{class:"mt-8 space-y-6",onSubmit:I(v,["prevent"])},[t[5]||(t[5]=e("input",{type:"hidden",name:"remember",value:"true"},null,-1)),e("div",P,[e("div",null,[t[3]||(t[3]=e("label",{for:"password",class:"sr-only"},"密码",-1)),D(e("input",{id:"password",name:"password",type:"password",autocomplete:"current-password",required:"","onUpdate:modelValue":t[0]||(t[0]=o=>a.value=o),class:d(["appearance-none rounded-t-md relative block w-full px-4 py-3 border transition-all duration-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm backdrop-blur-sm",i(n)?"bg-gray-800/50 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500":"bg-white/50 border-gray-300 text-gray-900 hover:border-gray-400"]),placeholder:"密码"},null,2),[[N,a.value]])])]),e("div",null,[e("button",{type:"submit",class:d(["group relative w-full flex justify-center py-3 px-4 border border-transparent text-sm font-medium rounded-md text-white transition-all duration-300 transform hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 shadow-lg hover:shadow-cyan-500/50",i(n)?"bg-gradient-to-r from-cyan-500 to-purple-500 hover:from-cyan-600 hover:to-purple-600":"bg-gradient-to-r from-cyan-600 to-purple-600 hover:from-cyan-700 hover:to-purple-700",l.value?"opacity-75 cursor-not-allowed":""]),disabled:l.value},[t[4]||(t[4]=e("span",{class:"absolute left-0 inset-y-0 flex items-center pl-3"},null,-1)),T(" "+_(l.value?"登录中...":"登录"),1)],10,R)])],32)],2)],2))}}),C=O(z,[["__scopeId","data-v-894571a2"]]);export{C as default};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
.border-progress-container[data-v-2fbf5085]{position:relative;width:100%;height:100%}.border-progress-canvas[data-v-2fbf5085]{position:absolute;top:0;left:0;width:100%;height:100%;transition:all .3s ease}.custom-scrollbar[data-v-bebdfe92]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.3) rgba(243,244,246,.5)}.custom-scrollbar[data-v-bebdfe92]::-webkit-scrollbar{width:6px;height:6px}.custom-scrollbar[data-v-bebdfe92]::-webkit-scrollbar-track{background:#f3f4f680;border-radius:3px}.custom-scrollbar[data-v-bebdfe92]::-webkit-scrollbar-thumb{background-color:#9ca3af80;border-radius:3px;-webkit-transition:background-color .3s;transition:background-color .3s}.custom-scrollbar[data-v-bebdfe92]::-webkit-scrollbar-thumb:hover{background-color:#9ca3afb3}[data-v-bebdfe92] [class*=dark] .custom-scrollbar{scrollbar-color:rgba(75,85,99,.5) rgba(31,41,55,.5)}[data-v-bebdfe92] [class*=dark] .custom-scrollbar::-webkit-scrollbar-track{background:#1f293780}[data-v-bebdfe92] [class*=dark] .custom-scrollbar::-webkit-scrollbar-thumb{background-color:#4b556380}[data-v-bebdfe92] [class*=dark] .custom-scrollbar::-webkit-scrollbar-thumb:hover{background-color:#4b5563b3}.fade-enter-active[data-v-832d304e],.fade-leave-active[data-v-832d304e]{transition:opacity .3s ease,transform .3s ease}.fade-enter-from[data-v-832d304e],.fade-leave-to[data-v-832d304e]{opacity:0;transform:translateY(10px)}@media(min-width:640px){.sm\:w-120[data-v-832d304e]{width:30rem}}.fade-enter-to[data-v-832d304e],.fade-leave-from[data-v-832d304e]{opacity:1;transform:translateY(0)}.drawer-enter-active[data-v-832d304e],.drawer-leave-active[data-v-832d304e]{transition:transform .3s ease}.drawer-enter-from[data-v-832d304e],.drawer-leave-to[data-v-832d304e]{transform:translate(100%)}.list-enter-active[data-v-832d304e],.list-leave-active[data-v-832d304e]{transition:all .5s ease}.list-enter-from[data-v-832d304e],.list-leave-to[data-v-832d304e]{opacity:0;transform:translate(30px)}select option[data-v-832d304e]{padding:8px;margin:4px;border-radius:6px}select option[data-v-832d304e]:checked{background:linear-gradient(to right,#6366f180,#a855f780)!important;color:#fff!important}.dark select option[data-v-832d304e]:checked{background:linear-gradient(to right,#6366f1b3,#a855f7b3)!important}select option[data-v-832d304e]:hover{background-color:#6366f11a}.dark select option[data-v-832d304e]:hover{background-color:#6366f133}.custom-scrollbar[data-v-832d304e]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.4) rgba(243,244,246,.3)}.custom-scrollbar[data-v-832d304e]::-webkit-scrollbar{width:8px;height:8px}.custom-scrollbar[data-v-832d304e]::-webkit-scrollbar-track{background:#f3f4f64d;border-radius:6px;margin:4px}.custom-scrollbar[data-v-832d304e]::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#6366f199,#a855f799);border-radius:6px;border:1px solid rgba(255,255,255,.2);-webkit-transition:all .3s ease;transition:all .3s ease}.custom-scrollbar[data-v-832d304e]::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#6366f1cc,#a855f7cc);transform:scale(1.1)}.custom-scrollbar[data-v-832d304e]::-webkit-scrollbar-corner{background:transparent}.dark .custom-scrollbar[data-v-832d304e]{scrollbar-color:rgba(75,85,99,.6) rgba(31,41,55,.4)}.dark .custom-scrollbar[data-v-832d304e]::-webkit-scrollbar-track{background:#1f293766}.dark .custom-scrollbar[data-v-832d304e]::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#6366f1b3,#a855f7b3);border:1px solid rgba(255,255,255,.1)}.dark .custom-scrollbar[data-v-832d304e]::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#6366f1e6,#a855f7e6)}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.border-progress-container[data-v-2fbf5085]{position:relative;width:100%;height:100%}.border-progress-canvas[data-v-2fbf5085]{position:absolute;top:0;left:0;width:100%;height:100%;transition:all .3s ease}.custom-scrollbar[data-v-bebdfe92]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.3) rgba(243,244,246,.5)}.custom-scrollbar[data-v-bebdfe92]::-webkit-scrollbar{width:6px;height:6px}.custom-scrollbar[data-v-bebdfe92]::-webkit-scrollbar-track{background:#f3f4f680;border-radius:3px}.custom-scrollbar[data-v-bebdfe92]::-webkit-scrollbar-thumb{background-color:#9ca3af80;border-radius:3px;-webkit-transition:background-color .3s;transition:background-color .3s}.custom-scrollbar[data-v-bebdfe92]::-webkit-scrollbar-thumb:hover{background-color:#9ca3afb3}[data-v-bebdfe92] [class*=dark] .custom-scrollbar{scrollbar-color:rgba(75,85,99,.5) rgba(31,41,55,.5)}[data-v-bebdfe92] [class*=dark] .custom-scrollbar::-webkit-scrollbar-track{background:#1f293780}[data-v-bebdfe92] [class*=dark] .custom-scrollbar::-webkit-scrollbar-thumb{background-color:#4b556380}[data-v-bebdfe92] [class*=dark] .custom-scrollbar::-webkit-scrollbar-thumb:hover{background-color:#4b5563b3}.fade-enter-active[data-v-0a16769d],.fade-leave-active[data-v-0a16769d]{transition:opacity .3s ease,transform .3s ease}.fade-enter-from[data-v-0a16769d],.fade-leave-to[data-v-0a16769d]{opacity:0;transform:translateY(10px)}@media (min-width: 640px){.sm\:w-120[data-v-0a16769d]{width:30rem}}.fade-enter-to[data-v-0a16769d],.fade-leave-from[data-v-0a16769d]{opacity:1;transform:translateY(0)}.drawer-enter-active[data-v-0a16769d],.drawer-leave-active[data-v-0a16769d]{transition:transform .3s ease}.drawer-enter-from[data-v-0a16769d],.drawer-leave-to[data-v-0a16769d]{transform:translate(100%)}.list-enter-active[data-v-0a16769d],.list-leave-active[data-v-0a16769d]{transition:all .5s ease}.list-enter-from[data-v-0a16769d],.list-leave-to[data-v-0a16769d]{opacity:0;transform:translate(30px)}select option[data-v-0a16769d]{padding:8px;margin:4px;border-radius:6px}select option[data-v-0a16769d]:checked{background:linear-gradient(to right,#6366f180,#a855f780)!important;color:#fff!important}.dark select option[data-v-0a16769d]:checked{background:linear-gradient(to right,#6366f1b3,#a855f7b3)!important}select option[data-v-0a16769d]:hover{background-color:#6366f11a}.dark select option[data-v-0a16769d]:hover{background-color:#6366f133}.custom-scrollbar[data-v-0a16769d]{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.4) rgba(243,244,246,.3)}.custom-scrollbar[data-v-0a16769d]::-webkit-scrollbar{width:8px;height:8px}.custom-scrollbar[data-v-0a16769d]::-webkit-scrollbar-track{background:#f3f4f64d;border-radius:6px;margin:4px}.custom-scrollbar[data-v-0a16769d]::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#6366f199,#a855f799);border-radius:6px;border:1px solid rgba(255,255,255,.2);-webkit-transition:all .3s ease;transition:all .3s ease}.custom-scrollbar[data-v-0a16769d]::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#6366f1cc,#a855f7cc);transform:scale(1.1)}.custom-scrollbar[data-v-0a16769d]::-webkit-scrollbar-corner{background:transparent}.dark .custom-scrollbar[data-v-0a16769d]{scrollbar-color:rgba(75,85,99,.6) rgba(31,41,55,.4)}.dark .custom-scrollbar[data-v-0a16769d]::-webkit-scrollbar-track{background:#1f293766}.dark .custom-scrollbar[data-v-0a16769d]::-webkit-scrollbar-thumb{background:linear-gradient(135deg,#6366f1b3,#a855f7b3);border:1px solid rgba(255,255,255,.1)}.dark .custom-scrollbar[data-v-0a16769d]::-webkit-scrollbar-thumb:hover{background:linear-gradient(135deg,#6366f1e6,#a855f7e6)}
|
||||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
|
|||||||
|
import{c as a}from"./index-B9FIg8c4.js";/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const e=a("box",[["path",{d:"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z",key:"hh9hay"}],["path",{d:"m3.3 7 8.7 5 8.7-5",key:"g66t2b"}],["path",{d:"M12 22V12",key:"d0xqtd"}]]);export{e as B};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{c as a}from"./index-B3zfsvgW.js";const e=a("box",[["path",{d:"M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z",key:"hh9hay"}],["path",{d:"m3.3 7 8.7 5 8.7-5",key:"g66t2b"}],["path",{d:"M12 22V12",key:"d0xqtd"}]]);export{e as B};
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import{c}from"./index-B9FIg8c4.js";/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const t=c("copy",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]);export{t as C};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{c}from"./index-B3zfsvgW.js";const t=c("copy",[["rect",{width:"14",height:"14",x:"8",y:"8",rx:"2",ry:"2",key:"17jyea"}],["path",{d:"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2",key:"zix9uf"}]]);export{t as C};
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import{c as a}from"./index-B9FIg8c4.js";/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const t=a("file",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}]]);export{t as F};
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{c as a}from"./index-B3zfsvgW.js";const t=a("file",[["path",{d:"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z",key:"1rqfz7"}],["path",{d:"M14 2v4a2 2 0 0 0 2 2h4",key:"tnqrlb"}]]);export{t as F};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
import{c as e}from"./index-B3zfsvgW.js";const a=e("hard-drive",[["line",{x1:"22",x2:"2",y1:"12",y2:"12",key:"1y58io"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16",key:"sgf278"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16",key:"1l4acy"}]]);export{a as H};
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import{c as e}from"./index-B9FIg8c4.js";/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const a=e("hard-drive",[["line",{x1:"22",x2:"2",y1:"12",y2:"12",key:"1y58io"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}],["line",{x1:"6",x2:"6.01",y1:"16",y2:"16",key:"sgf278"}],["line",{x1:"10",x2:"10.01",y1:"16",y2:"16",key:"1l4acy"}]]);export{a as H};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
|||||||
import{c as a}from"./index-B3zfsvgW.js";const c=a("eye",[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);const t=a("trash",[["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]]);export{c as E,t as T};
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import{c as a}from"./index-B9FIg8c4.js";/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const c=a("eye",[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]);/**
|
||||||
|
* @license lucide-vue-next v0.535.0 - ISC
|
||||||
|
*
|
||||||
|
* This source code is licensed under the ISC license.
|
||||||
|
* See the LICENSE file in the root directory of this source tree.
|
||||||
|
*/const t=a("trash",[["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]]);export{c as E,t as T};
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
<link rel="manifest" href="/assets/manifest.json" />
|
<link rel="manifest" href="/assets/manifest.json" />
|
||||||
<meta name="github" content="https://github.com/vastsa/FileCodeBox" />
|
<meta name="github" content="https://github.com/vastsa/FileCodeBox" />
|
||||||
<title>{{title}}</title>
|
<title>{{title}}</title>
|
||||||
<script type="module" crossorigin src="/assets/index-B3zfsvgW.js"></script>
|
<script type="module" crossorigin src="/assets/index-B9FIg8c4.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BXCbdTRh.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Xt5VatQo.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user