- 新增图像生成接口,支持试用、积分和自定义API Key模式 - 实现生成图片结果异步上传至MinIO存储,带重试机制 - 优化积分预扣除和异常退还逻辑,保障用户积分准确 - 添加获取生成历史记录接口,支持时间范围和分页 - 提供本地字典配置接口,支持模型、比例、提示模板和尺寸 - 实现图片批量上传接口,支持S3兼容对象存储 feat(admin): 增加管理员角色管理与权限分配接口 - 实现角色列表查询、角色创建、更新及删除功能 - 增加权限列表查询接口 - 实现用户角色分配接口,便于统一管理用户权限 - 增加系统字典增删查改接口,支持分类过滤和排序 - 权限控制全面覆盖管理接口,保证安全访问 feat(auth): 完善用户登录注册及权限相关接口与页面 - 实现手机号验证码发送及校验功能,保障注册安全 - 支持手机号注册、登录及退出接口,集成日志记录 - 增加修改密码功能,验证原密码后更新 - 提供动态导航菜单接口,基于权限展示不同菜单 - 实现管理界面路由及日志、角色、字典管理页面访问权限控制 - 添加系统日志查询接口,支持关键词和等级筛选 feat(app): 初始化Flask应用并配置蓝图与数据库 - 创建应用程序工厂,加载配置,初始化数据库和Redis客户端 - 注册认证、API及管理员蓝图,整合路由 - 根路由渲染主页模板 - 应用上下文中自动创建数据库表,保证运行环境准备完毕 feat(database): 提供数据库创建与迁移支持脚本 - 新增数据库创建脚本,支持自动检测是否已存在 - 添加数据库表初始化脚本,支持创建和删除所有表 - 实现RBAC权限初始化,包含基础权限和角色创建 - 新增字段手动修复脚本,添加用户API Key和积分字段 - 强制迁移脚本支持清理连接和修复表结构,初始化默认数据及角色分配 feat(config): 新增系统配置参数 - 配置数据库、Redis、Session和MinIO相关参数 - 添加AI接口地址及试用Key配置 - 集成阿里云短信服务配置及开发模式相关参数 feat(extensions): 初始化数据库、Redis和MinIO客户端 - 创建全局SQLAlchemy数据库实例和Redis客户端 - 配置基于boto3的MinIO兼容S3客户端 chore(logs): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
180 lines
6.2 KiB
Python
180 lines
6.2 KiB
Python
import io
|
|
import warnings
|
|
from typing import Any, Iterable, List, Optional
|
|
from urllib.parse import urlencode
|
|
|
|
from multidict import MultiDict, MultiDictProxy
|
|
|
|
from . import hdrs, multipart, payload
|
|
from .helpers import guess_filename
|
|
from .payload import Payload
|
|
|
|
__all__ = ("FormData",)
|
|
|
|
|
|
class FormData:
|
|
"""Helper class for form body generation.
|
|
|
|
Supports multipart/form-data and application/x-www-form-urlencoded.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
fields: Iterable[Any] = (),
|
|
quote_fields: bool = True,
|
|
charset: Optional[str] = None,
|
|
*,
|
|
default_to_multipart: bool = False,
|
|
) -> None:
|
|
self._writer = multipart.MultipartWriter("form-data")
|
|
self._fields: List[Any] = []
|
|
self._is_multipart = default_to_multipart
|
|
self._quote_fields = quote_fields
|
|
self._charset = charset
|
|
|
|
if isinstance(fields, dict):
|
|
fields = list(fields.items())
|
|
elif not isinstance(fields, (list, tuple)):
|
|
fields = (fields,)
|
|
self.add_fields(*fields)
|
|
|
|
@property
|
|
def is_multipart(self) -> bool:
|
|
return self._is_multipart
|
|
|
|
def add_field(
|
|
self,
|
|
name: str,
|
|
value: Any,
|
|
*,
|
|
content_type: Optional[str] = None,
|
|
filename: Optional[str] = None,
|
|
content_transfer_encoding: Optional[str] = None,
|
|
) -> None:
|
|
|
|
if isinstance(value, io.IOBase):
|
|
self._is_multipart = True
|
|
elif isinstance(value, (bytes, bytearray, memoryview)):
|
|
msg = (
|
|
"In v4, passing bytes will no longer create a file field. "
|
|
"Please explicitly use the filename parameter or pass a BytesIO object."
|
|
)
|
|
if filename is None and content_transfer_encoding is None:
|
|
warnings.warn(msg, DeprecationWarning)
|
|
filename = name
|
|
|
|
type_options: MultiDict[str] = MultiDict({"name": name})
|
|
if filename is not None and not isinstance(filename, str):
|
|
raise TypeError("filename must be an instance of str. Got: %s" % filename)
|
|
if filename is None and isinstance(value, io.IOBase):
|
|
filename = guess_filename(value, name)
|
|
if filename is not None:
|
|
type_options["filename"] = filename
|
|
self._is_multipart = True
|
|
|
|
headers = {}
|
|
if content_type is not None:
|
|
if not isinstance(content_type, str):
|
|
raise TypeError(
|
|
"content_type must be an instance of str. Got: %s" % content_type
|
|
)
|
|
headers[hdrs.CONTENT_TYPE] = content_type
|
|
self._is_multipart = True
|
|
if content_transfer_encoding is not None:
|
|
if not isinstance(content_transfer_encoding, str):
|
|
raise TypeError(
|
|
"content_transfer_encoding must be an instance"
|
|
" of str. Got: %s" % content_transfer_encoding
|
|
)
|
|
msg = (
|
|
"content_transfer_encoding is deprecated. "
|
|
"To maintain compatibility with v4 please pass a BytesPayload."
|
|
)
|
|
warnings.warn(msg, DeprecationWarning)
|
|
self._is_multipart = True
|
|
|
|
self._fields.append((type_options, headers, value))
|
|
|
|
def add_fields(self, *fields: Any) -> None:
|
|
to_add = list(fields)
|
|
|
|
while to_add:
|
|
rec = to_add.pop(0)
|
|
|
|
if isinstance(rec, io.IOBase):
|
|
k = guess_filename(rec, "unknown")
|
|
self.add_field(k, rec) # type: ignore[arg-type]
|
|
|
|
elif isinstance(rec, (MultiDictProxy, MultiDict)):
|
|
to_add.extend(rec.items())
|
|
|
|
elif isinstance(rec, (list, tuple)) and len(rec) == 2:
|
|
k, fp = rec
|
|
self.add_field(k, fp)
|
|
|
|
else:
|
|
raise TypeError(
|
|
"Only io.IOBase, multidict and (name, file) "
|
|
"pairs allowed, use .add_field() for passing "
|
|
"more complex parameters, got {!r}".format(rec)
|
|
)
|
|
|
|
def _gen_form_urlencoded(self) -> payload.BytesPayload:
|
|
# form data (x-www-form-urlencoded)
|
|
data = []
|
|
for type_options, _, value in self._fields:
|
|
data.append((type_options["name"], value))
|
|
|
|
charset = self._charset if self._charset is not None else "utf-8"
|
|
|
|
if charset == "utf-8":
|
|
content_type = "application/x-www-form-urlencoded"
|
|
else:
|
|
content_type = "application/x-www-form-urlencoded; charset=%s" % charset
|
|
|
|
return payload.BytesPayload(
|
|
urlencode(data, doseq=True, encoding=charset).encode(),
|
|
content_type=content_type,
|
|
)
|
|
|
|
def _gen_form_data(self) -> multipart.MultipartWriter:
|
|
"""Encode a list of fields using the multipart/form-data MIME format"""
|
|
for dispparams, headers, value in self._fields:
|
|
try:
|
|
if hdrs.CONTENT_TYPE in headers:
|
|
part = payload.get_payload(
|
|
value,
|
|
content_type=headers[hdrs.CONTENT_TYPE],
|
|
headers=headers,
|
|
encoding=self._charset,
|
|
)
|
|
else:
|
|
part = payload.get_payload(
|
|
value, headers=headers, encoding=self._charset
|
|
)
|
|
except Exception as exc:
|
|
raise TypeError(
|
|
"Can not serialize value type: %r\n "
|
|
"headers: %r\n value: %r" % (type(value), headers, value)
|
|
) from exc
|
|
|
|
if dispparams:
|
|
part.set_content_disposition(
|
|
"form-data", quote_fields=self._quote_fields, **dispparams
|
|
)
|
|
# FIXME cgi.FieldStorage doesn't likes body parts with
|
|
# Content-Length which were sent via chunked transfer encoding
|
|
assert part.headers is not None
|
|
part.headers.popall(hdrs.CONTENT_LENGTH, None)
|
|
|
|
self._writer.append_payload(part)
|
|
|
|
self._fields.clear()
|
|
return self._writer
|
|
|
|
def __call__(self) -> Payload:
|
|
if self._is_multipart:
|
|
return self._gen_form_data()
|
|
else:
|
|
return self._gen_form_urlencoded()
|