- 新增图像生成接口,支持试用、积分和自定义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): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
163 lines
5.3 KiB
Python
163 lines
5.3 KiB
Python
import errno
|
|
import io
|
|
import socket
|
|
from io import SEEK_END
|
|
from typing import Optional, Union
|
|
|
|
from ..exceptions import ConnectionError, TimeoutError
|
|
from ..utils import SSL_AVAILABLE
|
|
|
|
NONBLOCKING_EXCEPTION_ERROR_NUMBERS = {BlockingIOError: errno.EWOULDBLOCK}
|
|
|
|
if SSL_AVAILABLE:
|
|
import ssl
|
|
|
|
if hasattr(ssl, "SSLWantReadError"):
|
|
NONBLOCKING_EXCEPTION_ERROR_NUMBERS[ssl.SSLWantReadError] = 2
|
|
NONBLOCKING_EXCEPTION_ERROR_NUMBERS[ssl.SSLWantWriteError] = 2
|
|
else:
|
|
NONBLOCKING_EXCEPTION_ERROR_NUMBERS[ssl.SSLError] = 2
|
|
|
|
NONBLOCKING_EXCEPTIONS = tuple(NONBLOCKING_EXCEPTION_ERROR_NUMBERS.keys())
|
|
|
|
SERVER_CLOSED_CONNECTION_ERROR = "Connection closed by server."
|
|
SENTINEL = object()
|
|
|
|
SYM_CRLF = b"\r\n"
|
|
|
|
|
|
class SocketBuffer:
|
|
def __init__(
|
|
self, socket: socket.socket, socket_read_size: int, socket_timeout: float
|
|
):
|
|
self._sock = socket
|
|
self.socket_read_size = socket_read_size
|
|
self.socket_timeout = socket_timeout
|
|
self._buffer = io.BytesIO()
|
|
|
|
def unread_bytes(self) -> int:
|
|
"""
|
|
Remaining unread length of buffer
|
|
"""
|
|
pos = self._buffer.tell()
|
|
end = self._buffer.seek(0, SEEK_END)
|
|
self._buffer.seek(pos)
|
|
return end - pos
|
|
|
|
def _read_from_socket(
|
|
self,
|
|
length: Optional[int] = None,
|
|
timeout: Union[float, object] = SENTINEL,
|
|
raise_on_timeout: Optional[bool] = True,
|
|
) -> bool:
|
|
sock = self._sock
|
|
socket_read_size = self.socket_read_size
|
|
marker = 0
|
|
custom_timeout = timeout is not SENTINEL
|
|
|
|
buf = self._buffer
|
|
current_pos = buf.tell()
|
|
buf.seek(0, SEEK_END)
|
|
if custom_timeout:
|
|
sock.settimeout(timeout)
|
|
try:
|
|
while True:
|
|
data = self._sock.recv(socket_read_size)
|
|
# an empty string indicates the server shutdown the socket
|
|
if isinstance(data, bytes) and len(data) == 0:
|
|
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
|
|
buf.write(data)
|
|
data_length = len(data)
|
|
marker += data_length
|
|
|
|
if length is not None and length > marker:
|
|
continue
|
|
return True
|
|
except socket.timeout:
|
|
if raise_on_timeout:
|
|
raise TimeoutError("Timeout reading from socket")
|
|
return False
|
|
except NONBLOCKING_EXCEPTIONS as ex:
|
|
# if we're in nonblocking mode and the recv raises a
|
|
# blocking error, simply return False indicating that
|
|
# there's no data to be read. otherwise raise the
|
|
# original exception.
|
|
allowed = NONBLOCKING_EXCEPTION_ERROR_NUMBERS.get(ex.__class__, -1)
|
|
if not raise_on_timeout and ex.errno == allowed:
|
|
return False
|
|
raise ConnectionError(f"Error while reading from socket: {ex.args}")
|
|
finally:
|
|
buf.seek(current_pos)
|
|
if custom_timeout:
|
|
sock.settimeout(self.socket_timeout)
|
|
|
|
def can_read(self, timeout: float) -> bool:
|
|
return bool(self.unread_bytes()) or self._read_from_socket(
|
|
timeout=timeout, raise_on_timeout=False
|
|
)
|
|
|
|
def read(self, length: int) -> bytes:
|
|
length = length + 2 # make sure to read the \r\n terminator
|
|
# BufferIO will return less than requested if buffer is short
|
|
data = self._buffer.read(length)
|
|
missing = length - len(data)
|
|
if missing:
|
|
# fill up the buffer and read the remainder
|
|
self._read_from_socket(missing)
|
|
data += self._buffer.read(missing)
|
|
return data[:-2]
|
|
|
|
def readline(self) -> bytes:
|
|
buf = self._buffer
|
|
data = buf.readline()
|
|
while not data.endswith(SYM_CRLF):
|
|
# there's more data in the socket that we need
|
|
self._read_from_socket()
|
|
data += buf.readline()
|
|
|
|
return data[:-2]
|
|
|
|
def get_pos(self) -> int:
|
|
"""
|
|
Get current read position
|
|
"""
|
|
return self._buffer.tell()
|
|
|
|
def rewind(self, pos: int) -> None:
|
|
"""
|
|
Rewind the buffer to a specific position, to re-start reading
|
|
"""
|
|
self._buffer.seek(pos)
|
|
|
|
def purge(self) -> None:
|
|
"""
|
|
After a successful read, purge the read part of buffer
|
|
"""
|
|
unread = self.unread_bytes()
|
|
|
|
# Only if we have read all of the buffer do we truncate, to
|
|
# reduce the amount of memory thrashing. This heuristic
|
|
# can be changed or removed later.
|
|
if unread > 0:
|
|
return
|
|
|
|
if unread > 0:
|
|
# move unread data to the front
|
|
view = self._buffer.getbuffer()
|
|
view[:unread] = view[-unread:]
|
|
self._buffer.truncate(unread)
|
|
self._buffer.seek(0)
|
|
|
|
def close(self) -> None:
|
|
try:
|
|
self._buffer.close()
|
|
except Exception:
|
|
# issue #633 suggests the purge/close somehow raised a
|
|
# BadFileDescriptor error. Perhaps the client ran out of
|
|
# memory or something else? It's probably OK to ignore
|
|
# any error being raised from purge/close since we're
|
|
# removing the reference to the instance below.
|
|
pass
|
|
self._buffer = None
|
|
self._sock = None
|