Compare commits
4 Commits
bcb0a9b322
...
f4052d229a
| Author | SHA1 | Date | |
|---|---|---|---|
| f4052d229a | |||
| bc2182720d | |||
| eddf38d316 | |||
| 03e768d232 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,8 +1,6 @@
|
|||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
*.so
|
*.so
|
||||||
*.egg
|
*.egg
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
@@ -79,3 +77,6 @@ statics/
|
|||||||
# 许可证密钥(保密)
|
# 许可证密钥(保密)
|
||||||
license_private.pem
|
license_private.pem
|
||||||
license.key
|
license.key
|
||||||
|
|
||||||
|
avatar/
|
||||||
|
.dev/
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
-- 迁移:文件元数据系统重构
|
|
||||||
-- 将固定列 FileMetadata 表替换为灵活的 ObjectMetadata KV 表
|
|
||||||
-- 日期:2026-02-23
|
|
||||||
|
|
||||||
BEGIN;
|
|
||||||
|
|
||||||
-- ==================== 1. object 表新增 mime_type 列 ====================
|
|
||||||
ALTER TABLE object ADD COLUMN IF NOT EXISTS mime_type VARCHAR(127);
|
|
||||||
|
|
||||||
-- ==================== 2. physicalfile 表新增 checksum_sha256 列 ====================
|
|
||||||
ALTER TABLE physicalfile ADD COLUMN IF NOT EXISTS checksum_sha256 VARCHAR(64);
|
|
||||||
|
|
||||||
-- ==================== 3. 创建 objectmetadata KV 表 ====================
|
|
||||||
CREATE TABLE IF NOT EXISTS objectmetadata (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
object_id UUID NOT NULL REFERENCES object(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
value TEXT NOT NULL,
|
|
||||||
is_public BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
CONSTRAINT uq_object_metadata_object_name UNIQUE (object_id, name)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS ix_object_metadata_object_id
|
|
||||||
ON objectmetadata (object_id);
|
|
||||||
|
|
||||||
-- ==================== 4. 创建 custompropertydefinition 表 ====================
|
|
||||||
CREATE TABLE IF NOT EXISTS custompropertydefinition (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
owner_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
type VARCHAR NOT NULL,
|
|
||||||
icon VARCHAR(100),
|
|
||||||
options JSON,
|
|
||||||
default_value VARCHAR(500),
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS ix_custompropertydefinition_owner_id
|
|
||||||
ON custompropertydefinition (owner_id);
|
|
||||||
|
|
||||||
-- ==================== 5. 迁移旧数据(从 filemetadata 到 objectmetadata)====================
|
|
||||||
-- 将 filemetadata 中的数据迁移到 objectmetadata KV 格式
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'filemetadata') THEN
|
|
||||||
-- 同时将 mime_type 上提到 object 表
|
|
||||||
UPDATE object o
|
|
||||||
SET mime_type = fm.mime_type
|
|
||||||
FROM filemetadata fm
|
|
||||||
WHERE fm.object_id = o.id AND fm.mime_type IS NOT NULL;
|
|
||||||
|
|
||||||
-- 将 checksum_sha256 迁移到 physicalfile 表
|
|
||||||
UPDATE physicalfile pf
|
|
||||||
SET checksum_sha256 = fm.checksum_sha256
|
|
||||||
FROM filemetadata fm
|
|
||||||
JOIN object o ON fm.object_id = o.id
|
|
||||||
WHERE o.physical_file_id = pf.id AND fm.checksum_sha256 IS NOT NULL;
|
|
||||||
|
|
||||||
-- 迁移 width → exif:width
|
|
||||||
INSERT INTO objectmetadata (id, object_id, name, value, is_public)
|
|
||||||
SELECT gen_random_uuid(), object_id, 'exif:width', CAST(width AS TEXT), true
|
|
||||||
FROM filemetadata WHERE width IS NOT NULL;
|
|
||||||
|
|
||||||
-- 迁移 height → exif:height
|
|
||||||
INSERT INTO objectmetadata (id, object_id, name, value, is_public)
|
|
||||||
SELECT gen_random_uuid(), object_id, 'exif:height', CAST(height AS TEXT), true
|
|
||||||
FROM filemetadata WHERE height IS NOT NULL;
|
|
||||||
|
|
||||||
-- 迁移 duration → stream:duration
|
|
||||||
INSERT INTO objectmetadata (id, object_id, name, value, is_public)
|
|
||||||
SELECT gen_random_uuid(), object_id, 'stream:duration', CAST(duration AS TEXT), true
|
|
||||||
FROM filemetadata WHERE duration IS NOT NULL;
|
|
||||||
|
|
||||||
-- 迁移 bitrate → stream:bitrate
|
|
||||||
INSERT INTO objectmetadata (id, object_id, name, value, is_public)
|
|
||||||
SELECT gen_random_uuid(), object_id, 'stream:bitrate', CAST(bitrate AS TEXT), true
|
|
||||||
FROM filemetadata WHERE bitrate IS NOT NULL;
|
|
||||||
|
|
||||||
-- 删除旧表
|
|
||||||
DROP TABLE filemetadata;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@@ -82,7 +82,8 @@ async def router_site_config(session: SessionDep) -> SiteConfigResponse:
|
|||||||
(Setting.type == SettingsType.REGISTER) |
|
(Setting.type == SettingsType.REGISTER) |
|
||||||
(Setting.type == SettingsType.CAPTCHA) |
|
(Setting.type == SettingsType.CAPTCHA) |
|
||||||
(Setting.type == SettingsType.AUTH) |
|
(Setting.type == SettingsType.AUTH) |
|
||||||
(Setting.type == SettingsType.OAUTH),
|
(Setting.type == SettingsType.OAUTH) |
|
||||||
|
(Setting.type == SettingsType.AVATAR),
|
||||||
fetch_mode="all",
|
fetch_mode="all",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -122,6 +123,7 @@ async def router_site_config(session: SessionDep) -> SiteConfigResponse:
|
|||||||
password_required=s.get("auth_password_required") == "1",
|
password_required=s.get("auth_password_required") == "1",
|
||||||
phone_binding_required=s.get("auth_phone_binding_required") == "1",
|
phone_binding_required=s.get("auth_phone_binding_required") == "1",
|
||||||
email_binding_required=s.get("auth_email_binding_required") == "1",
|
email_binding_required=s.get("auth_email_binding_required") == "1",
|
||||||
|
avatar_max_size=int(s["avatar_size"]),
|
||||||
footer_code=s.get("footer_code"),
|
footer_code=s.get("footer_code"),
|
||||||
tos_url=s.get("tos_url"),
|
tos_url=s.get("tos_url"),
|
||||||
privacy_url=s.get("privacy_url"),
|
privacy_url=s.get("privacy_url"),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import json
|
|||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.responses import FileResponse, RedirectResponse
|
||||||
from itsdangerous import URLSafeTimedSerializer
|
from itsdangerous import URLSafeTimedSerializer
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from webauthn import (
|
from webauthn import (
|
||||||
@@ -358,20 +359,78 @@ def router_user_profile(id: str) -> sqlmodels.ResponseBase:
|
|||||||
@user_router.get(
|
@user_router.get(
|
||||||
path='/avatar/{id}/{size}',
|
path='/avatar/{id}/{size}',
|
||||||
summary='获取用户头像',
|
summary='获取用户头像',
|
||||||
description='Get user avatar by ID and size.',
|
response_model=None,
|
||||||
)
|
)
|
||||||
def router_user_avatar(id: str, size: int = 128) -> sqlmodels.ResponseBase:
|
async def router_user_avatar(
|
||||||
|
session: SessionDep,
|
||||||
|
id: UUID,
|
||||||
|
size: int = 128,
|
||||||
|
) -> FileResponse | RedirectResponse:
|
||||||
"""
|
"""
|
||||||
Get user avatar by ID and size.
|
获取指定用户指定尺寸的头像(公开端点,无需认证)
|
||||||
|
|
||||||
Args:
|
路径参数:
|
||||||
id (str): The user ID.
|
- id: 用户 UUID
|
||||||
size (int): The size of the avatar image.
|
- size: 请求的头像尺寸(px),默认 128
|
||||||
|
|
||||||
Returns:
|
行为:
|
||||||
str: A Base64 encoded string of the user avatar image.
|
- default: 302 重定向到 Gravatar identicon
|
||||||
|
- gravatar: 302 重定向到 Gravatar(使用用户邮箱 MD5)
|
||||||
|
- file: 返回本地 WebP 文件
|
||||||
|
|
||||||
|
响应:
|
||||||
|
- 200: image/webp(file 模式)
|
||||||
|
- 302: 重定向到外部 URL(default/gravatar 模式)
|
||||||
|
- 404: 用户不存在
|
||||||
|
|
||||||
|
缓存:Cache-Control: public, max-age=3600
|
||||||
"""
|
"""
|
||||||
http_exceptions.raise_not_implemented()
|
import aiofiles.os
|
||||||
|
|
||||||
|
from service.avatar import (
|
||||||
|
get_avatar_file_path,
|
||||||
|
get_avatar_settings,
|
||||||
|
gravatar_url,
|
||||||
|
resolve_avatar_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = await sqlmodels.User.get(session, sqlmodels.User.id == id)
|
||||||
|
if not user:
|
||||||
|
http_exceptions.raise_not_found("用户不存在")
|
||||||
|
|
||||||
|
avatar_path, _, size_l, size_m, size_s = await get_avatar_settings(session)
|
||||||
|
|
||||||
|
if user.avatar == "file":
|
||||||
|
size_label = resolve_avatar_size(size, size_l, size_m, size_s)
|
||||||
|
file_path = get_avatar_file_path(avatar_path, user.id, size_label)
|
||||||
|
|
||||||
|
if not await aiofiles.os.path.exists(file_path):
|
||||||
|
# 文件丢失,降级为 identicon
|
||||||
|
fallback_url = gravatar_url(str(user.id), size, "https://www.gravatar.com/")
|
||||||
|
return RedirectResponse(url=fallback_url, status_code=302)
|
||||||
|
|
||||||
|
return FileResponse(
|
||||||
|
path=file_path,
|
||||||
|
media_type="image/webp",
|
||||||
|
headers={"Cache-Control": "public, max-age=3600"},
|
||||||
|
)
|
||||||
|
|
||||||
|
elif user.avatar == "gravatar":
|
||||||
|
gravatar_setting = await sqlmodels.Setting.get(
|
||||||
|
session,
|
||||||
|
(sqlmodels.Setting.type == sqlmodels.SettingsType.AVATAR)
|
||||||
|
& (sqlmodels.Setting.name == "gravatar_server"),
|
||||||
|
)
|
||||||
|
server = gravatar_setting.value if gravatar_setting else "https://www.gravatar.com/"
|
||||||
|
email = user.email or str(user.id)
|
||||||
|
url = gravatar_url(email, size, server)
|
||||||
|
return RedirectResponse(url=url, status_code=302)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# default: identicon
|
||||||
|
email_or_id = user.email or str(user.id)
|
||||||
|
url = gravatar_url(email_or_id, size, "https://www.gravatar.com/")
|
||||||
|
return RedirectResponse(url=url, status_code=302)
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
# 需要登录的接口
|
# 需要登录的接口
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
|
||||||
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
|
from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
|
||||||
|
|
||||||
import sqlmodels
|
import sqlmodels
|
||||||
@@ -165,34 +165,121 @@ async def router_user_settings(
|
|||||||
@user_settings_router.post(
|
@user_settings_router.post(
|
||||||
path='/avatar',
|
path='/avatar',
|
||||||
summary='从文件上传头像',
|
summary='从文件上传头像',
|
||||||
description='Upload user avatar from file.',
|
status_code=204,
|
||||||
dependencies=[Depends(auth_required)],
|
|
||||||
)
|
)
|
||||||
def router_user_settings_avatar() -> sqlmodels.ResponseBase:
|
async def router_user_settings_avatar(
|
||||||
|
session: SessionDep,
|
||||||
|
user: Annotated[sqlmodels.user.User, Depends(auth_required)],
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Upload user avatar from file.
|
上传头像文件
|
||||||
|
|
||||||
Returns:
|
认证:JWT token
|
||||||
dict: A dictionary containing the result of the avatar upload.
|
请求体:multipart/form-data,file 字段
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. 验证文件 MIME 类型(JPEG/PNG/GIF/WebP)
|
||||||
|
2. 验证文件大小 <= avatar_size 设置(默认 2MB)
|
||||||
|
3. 调用 Pillow 验证图片有效性并处理(居中裁剪、缩放 L/M/S)
|
||||||
|
4. 保存三种尺寸的 WebP 文件
|
||||||
|
5. 更新 User.avatar = "file"
|
||||||
|
|
||||||
|
错误处理:
|
||||||
|
- 400: 文件类型不支持 / 图片无法解析
|
||||||
|
- 413: 文件过大
|
||||||
"""
|
"""
|
||||||
http_exceptions.raise_not_implemented()
|
from service.avatar import (
|
||||||
|
ALLOWED_CONTENT_TYPES,
|
||||||
|
get_avatar_settings,
|
||||||
|
process_and_save_avatar,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 验证 MIME 类型
|
||||||
|
if file.content_type not in ALLOWED_CONTENT_TYPES:
|
||||||
|
http_exceptions.raise_bad_request(
|
||||||
|
f"不支持的图片格式,允许: {', '.join(ALLOWED_CONTENT_TYPES)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 读取并验证大小
|
||||||
|
_, max_upload_size, _, _, _ = await get_avatar_settings(session)
|
||||||
|
raw_bytes = await file.read()
|
||||||
|
if len(raw_bytes) > max_upload_size:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=413,
|
||||||
|
detail=f"文件过大,最大允许 {max_upload_size} 字节",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 处理并保存(内部会验证图片有效性,无效抛出 ValueError)
|
||||||
|
try:
|
||||||
|
await process_and_save_avatar(session, user.id, raw_bytes)
|
||||||
|
except ValueError as e:
|
||||||
|
http_exceptions.raise_bad_request(str(e))
|
||||||
|
|
||||||
|
# 更新用户头像字段
|
||||||
|
user.avatar = "file"
|
||||||
|
await user.save(session)
|
||||||
|
|
||||||
|
|
||||||
@user_settings_router.put(
|
@user_settings_router.put(
|
||||||
path='/avatar',
|
path='/avatar',
|
||||||
summary='设定为 Gravatar 头像',
|
summary='设定为 Gravatar 头像',
|
||||||
description='Set user avatar to Gravatar.',
|
|
||||||
dependencies=[Depends(auth_required)],
|
|
||||||
status_code=204,
|
status_code=204,
|
||||||
)
|
)
|
||||||
def router_user_settings_avatar_gravatar() -> None:
|
async def router_user_settings_avatar_gravatar(
|
||||||
|
session: SessionDep,
|
||||||
|
user: Annotated[sqlmodels.user.User, Depends(auth_required)],
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Set user avatar to Gravatar.
|
将头像切换为 Gravatar
|
||||||
|
|
||||||
Returns:
|
认证:JWT token
|
||||||
dict: A dictionary containing the result of setting the Gravatar avatar.
|
|
||||||
|
流程:
|
||||||
|
1. 验证用户有邮箱(Gravatar 基于邮箱 MD5)
|
||||||
|
2. 如果当前是 FILE 头像,删除本地文件
|
||||||
|
3. 更新 User.avatar = "gravatar"
|
||||||
|
|
||||||
|
错误处理:
|
||||||
|
- 400: 用户没有邮箱
|
||||||
"""
|
"""
|
||||||
http_exceptions.raise_not_implemented()
|
from service.avatar import delete_avatar_files
|
||||||
|
|
||||||
|
if not user.email:
|
||||||
|
http_exceptions.raise_bad_request("Gravatar 需要邮箱,请先绑定邮箱")
|
||||||
|
|
||||||
|
if user.avatar == "file":
|
||||||
|
await delete_avatar_files(session, user.id)
|
||||||
|
|
||||||
|
user.avatar = "gravatar"
|
||||||
|
await user.save(session)
|
||||||
|
|
||||||
|
|
||||||
|
@user_settings_router.delete(
|
||||||
|
path='/avatar',
|
||||||
|
summary='重置头像为默认',
|
||||||
|
status_code=204,
|
||||||
|
)
|
||||||
|
async def router_user_settings_avatar_delete(
|
||||||
|
session: SessionDep,
|
||||||
|
user: Annotated[sqlmodels.user.User, Depends(auth_required)],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
重置头像为默认
|
||||||
|
|
||||||
|
认证:JWT token
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. 如果当前是 FILE 头像,删除本地文件
|
||||||
|
2. 更新 User.avatar = "default"
|
||||||
|
"""
|
||||||
|
from service.avatar import delete_avatar_files
|
||||||
|
|
||||||
|
if user.avatar == "file":
|
||||||
|
await delete_avatar_files(session, user.id)
|
||||||
|
|
||||||
|
user.avatar = "default"
|
||||||
|
await user.save(session)
|
||||||
|
|
||||||
|
|
||||||
@user_settings_router.patch(
|
@user_settings_router.patch(
|
||||||
|
|||||||
@@ -263,15 +263,49 @@ class LocalStorageService:
|
|||||||
"""
|
"""
|
||||||
删除文件(物理删除)
|
删除文件(物理删除)
|
||||||
|
|
||||||
|
删除文件后会尝试清理因此变空的父目录。
|
||||||
|
|
||||||
:param path: 完整文件路径
|
:param path: 完整文件路径
|
||||||
"""
|
"""
|
||||||
if await self.file_exists(path):
|
if await self.file_exists(path):
|
||||||
try:
|
try:
|
||||||
await aiofiles.os.remove(path)
|
await aiofiles.os.remove(path)
|
||||||
l.debug(f"已删除文件: {path}")
|
l.debug(f"已删除文件: {path}")
|
||||||
|
await self._cleanup_empty_parents(path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
l.warning(f"删除文件失败 {path}: {e}")
|
l.warning(f"删除文件失败 {path}: {e}")
|
||||||
|
|
||||||
|
async def _cleanup_empty_parents(self, file_path: str) -> None:
|
||||||
|
"""
|
||||||
|
从被删文件的父目录开始,向上逐级删除空目录
|
||||||
|
|
||||||
|
在以下情况停止:
|
||||||
|
|
||||||
|
- 到达存储根目录(_base_path)
|
||||||
|
- 遇到非空目录
|
||||||
|
- 遇到 .trash 目录
|
||||||
|
- 删除失败(权限、并发等)
|
||||||
|
|
||||||
|
:param file_path: 被删文件的完整路径
|
||||||
|
"""
|
||||||
|
current = Path(file_path).parent
|
||||||
|
|
||||||
|
while current != self._base_path and str(current).startswith(str(self._base_path)):
|
||||||
|
if current.name == '.trash':
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
entries = await aiofiles.os.listdir(str(current))
|
||||||
|
if entries:
|
||||||
|
break
|
||||||
|
|
||||||
|
await aiofiles.os.rmdir(str(current))
|
||||||
|
l.debug(f"已清理空目录: {current}")
|
||||||
|
current = current.parent
|
||||||
|
except OSError as e:
|
||||||
|
l.debug(f"清理空目录失败(忽略): {current}: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
async def move_to_trash(
|
async def move_to_trash(
|
||||||
self,
|
self,
|
||||||
source_path: str,
|
source_path: str,
|
||||||
@@ -304,6 +338,7 @@ class LocalStorageService:
|
|||||||
try:
|
try:
|
||||||
await aiofiles.os.rename(source_path, str(trash_path))
|
await aiofiles.os.rename(source_path, str(trash_path))
|
||||||
l.info(f"文件已移动到回收站: {source_path} -> {trash_path}")
|
l.info(f"文件已移动到回收站: {source_path} -> {trash_path}")
|
||||||
|
await self._cleanup_empty_parents(source_path)
|
||||||
return str(trash_path)
|
return str(trash_path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise StorageException(f"移动文件到回收站失败: {e}")
|
raise StorageException(f"移动文件到回收站失败: {e}")
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ class SiteConfigResponse(SQLModelBase):
|
|||||||
email_binding_required: bool = True
|
email_binding_required: bool = True
|
||||||
"""是否强制绑定邮箱"""
|
"""是否强制绑定邮箱"""
|
||||||
|
|
||||||
|
avatar_max_size: int = 2097152
|
||||||
|
"""头像文件最大字节数(默认 2MB)"""
|
||||||
|
|
||||||
footer_code: str | None = None
|
footer_code: str | None = None
|
||||||
"""自定义页脚代码"""
|
"""自定义页脚代码"""
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../.
|
|||||||
|
|
||||||
from main import app
|
from main import app
|
||||||
from sqlmodels import Group, GroupClaims, GroupOptions, Object, ObjectType, Policy, PolicyType, Setting, SettingsType, User
|
from sqlmodels import Group, GroupClaims, GroupOptions, Object, ObjectType, Policy, PolicyType, Setting, SettingsType, User
|
||||||
|
from sqlmodels.policy import GroupPolicyLink
|
||||||
from sqlmodels.auth_identity import AuthIdentity, AuthProviderType
|
from sqlmodels.auth_identity import AuthIdentity, AuthProviderType
|
||||||
from sqlmodels.user import UserStatus
|
from sqlmodels.user import UserStatus
|
||||||
from utils import Password
|
from utils import Password
|
||||||
@@ -108,6 +109,12 @@ async def initialized_db(test_session: AsyncSession) -> AsyncSession:
|
|||||||
Setting(type=SettingsType.AUTH, name="auth_email_binding_required", value="1"),
|
Setting(type=SettingsType.AUTH, name="auth_email_binding_required", value="1"),
|
||||||
Setting(type=SettingsType.OAUTH, name="github_enabled", value="0"),
|
Setting(type=SettingsType.OAUTH, name="github_enabled", value="0"),
|
||||||
Setting(type=SettingsType.OAUTH, name="qq_enabled", value="0"),
|
Setting(type=SettingsType.OAUTH, name="qq_enabled", value="0"),
|
||||||
|
Setting(type=SettingsType.AVATAR, name="gravatar_server", value="https://www.gravatar.com/"),
|
||||||
|
Setting(type=SettingsType.AVATAR, name="avatar_size", value="2097152"),
|
||||||
|
Setting(type=SettingsType.AVATAR, name="avatar_size_l", value="200"),
|
||||||
|
Setting(type=SettingsType.AVATAR, name="avatar_size_m", value="130"),
|
||||||
|
Setting(type=SettingsType.AVATAR, name="avatar_size_s", value="50"),
|
||||||
|
Setting(type=SettingsType.PATH, name="avatar_path", value="avatar"),
|
||||||
]
|
]
|
||||||
for setting in settings:
|
for setting in settings:
|
||||||
test_session.add(setting)
|
test_session.add(setting)
|
||||||
@@ -156,7 +163,11 @@ async def initialized_db(test_session: AsyncSession) -> AsyncSession:
|
|||||||
await test_session.refresh(admin_group)
|
await test_session.refresh(admin_group)
|
||||||
await test_session.refresh(default_policy)
|
await test_session.refresh(default_policy)
|
||||||
|
|
||||||
# 4. 创建用户组选项
|
# 4. 关联用户组与存储策略
|
||||||
|
test_session.add(GroupPolicyLink(group_id=default_group.id, policy_id=default_policy.id))
|
||||||
|
test_session.add(GroupPolicyLink(group_id=admin_group.id, policy_id=default_policy.id))
|
||||||
|
|
||||||
|
# 5. 创建用户组选项
|
||||||
default_group_options = GroupOptions(
|
default_group_options = GroupOptions(
|
||||||
group_id=default_group.id,
|
group_id=default_group.id,
|
||||||
share_download=True,
|
share_download=True,
|
||||||
|
|||||||
Reference in New Issue
Block a user