- 新增图像生成接口,支持试用、积分和自定义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): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
250 lines
8.9 KiB
Python
250 lines
8.9 KiB
Python
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
import warnings
|
|
from datetime import timezone
|
|
|
|
from tzlocal import utils
|
|
|
|
import zoneinfo
|
|
|
|
_cache_tz = None
|
|
_cache_tz_name = None
|
|
|
|
log = logging.getLogger("tzlocal")
|
|
|
|
|
|
def _get_localzone_name(_root="/"):
|
|
"""Tries to find the local timezone configuration.
|
|
|
|
This method finds the timezone name, if it can, or it returns None.
|
|
|
|
The parameter _root makes the function look for files like /etc/localtime
|
|
beneath the _root directory. This is primarily used by the tests.
|
|
In normal usage you call the function without parameters."""
|
|
|
|
# First try the ENV setting.
|
|
tzenv = utils._tz_name_from_env()
|
|
if tzenv:
|
|
return tzenv
|
|
|
|
# Are we under Termux on Android?
|
|
if os.path.exists(os.path.join(_root, "system/bin/getprop")):
|
|
log.debug("This looks like Termux")
|
|
|
|
import subprocess
|
|
|
|
try:
|
|
androidtz = (
|
|
subprocess.check_output(["getprop", "persist.sys.timezone"])
|
|
.strip()
|
|
.decode()
|
|
)
|
|
return androidtz
|
|
except (OSError, subprocess.CalledProcessError):
|
|
# proot environment or failed to getprop
|
|
log.debug("It's not termux?")
|
|
pass
|
|
|
|
# Now look for distribution specific configuration files
|
|
# that contain the timezone name.
|
|
|
|
# Stick all of them in a dict, to compare later.
|
|
found_configs = {}
|
|
|
|
for configfile in ("etc/timezone", "var/db/zoneinfo"):
|
|
tzpath = os.path.join(_root, configfile)
|
|
try:
|
|
with open(tzpath) as tzfile:
|
|
data = tzfile.read()
|
|
log.debug(f"{tzpath} found, contents:\n {data}")
|
|
|
|
etctz = data.strip("/ \t\r\n")
|
|
if not etctz:
|
|
# Empty file, skip
|
|
continue
|
|
for etctz in etctz.splitlines():
|
|
# Get rid of host definitions and comments:
|
|
if " " in etctz:
|
|
etctz, dummy = etctz.split(" ", 1)
|
|
if "#" in etctz:
|
|
etctz, dummy = etctz.split("#", 1)
|
|
if not etctz:
|
|
continue
|
|
|
|
found_configs[tzpath] = etctz.replace(" ", "_")
|
|
|
|
except (OSError, UnicodeDecodeError):
|
|
# File doesn't exist or is a directory, or it's a binary file.
|
|
continue
|
|
|
|
# CentOS has a ZONE setting in /etc/sysconfig/clock,
|
|
# OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
|
|
# Gentoo has a TIMEZONE setting in /etc/conf.d/clock
|
|
# We look through these files for a timezone:
|
|
|
|
zone_re = re.compile(r"\s*ZONE\s*=\s*\"")
|
|
timezone_re = re.compile(r"\s*TIMEZONE\s*=\s*\"")
|
|
end_re = re.compile('"')
|
|
|
|
for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
|
|
tzpath = os.path.join(_root, filename)
|
|
try:
|
|
with open(tzpath, "rt") as tzfile:
|
|
data = tzfile.readlines()
|
|
log.debug(f"{tzpath} found, contents:\n {data}")
|
|
|
|
for line in data:
|
|
# Look for the ZONE= setting.
|
|
match = zone_re.match(line)
|
|
if match is None:
|
|
# No ZONE= setting. Look for the TIMEZONE= setting.
|
|
match = timezone_re.match(line)
|
|
if match is not None:
|
|
# Some setting existed
|
|
line = line[match.end() :]
|
|
etctz = line[: end_re.search(line).start()]
|
|
|
|
# We found a timezone
|
|
found_configs[tzpath] = etctz.replace(" ", "_")
|
|
|
|
except (OSError, UnicodeDecodeError):
|
|
# UnicodeDecode handles when clock is symlink to /etc/localtime
|
|
continue
|
|
|
|
# systemd distributions use symlinks that include the zone name,
|
|
# see manpage of localtime(5) and timedatectl(1)
|
|
tzpath = os.path.join(_root, "etc/localtime")
|
|
if os.path.exists(tzpath) and os.path.islink(tzpath):
|
|
log.debug(f"{tzpath} found")
|
|
etctz = os.path.realpath(tzpath)
|
|
start = etctz.find("/") + 1
|
|
while start != 0:
|
|
etctz = etctz[start:]
|
|
try:
|
|
zoneinfo.ZoneInfo(etctz)
|
|
tzinfo = f"{tzpath} is a symlink to"
|
|
found_configs[tzinfo] = etctz.replace(" ", "_")
|
|
# Only need first valid relative path in simlink.
|
|
break
|
|
except zoneinfo.ZoneInfoNotFoundError:
|
|
pass
|
|
start = etctz.find("/") + 1
|
|
|
|
if len(found_configs) > 0:
|
|
log.debug(f"{len(found_configs)} found:\n {found_configs}")
|
|
|
|
# We found some explicit config of some sort!
|
|
if len(found_configs) > 1:
|
|
# Uh-oh, multiple configs. See if they match:
|
|
unique_tzs = _get_unique_tzs(found_configs, _root)
|
|
|
|
if len(unique_tzs) != 1 and "etc/timezone" in str(found_configs.keys()):
|
|
# For some reason some distros are removing support for /etc/timezone,
|
|
# which is bad, because that's the only place where the timezone is stated
|
|
# in plain text, and what's worse, they don't delete it. So we can't trust
|
|
# it now, so when we have conflicting configs, we just ignore it, with a warning.
|
|
log.warning("/etc/timezone is deprecated in some distros, and no longer reliable. "
|
|
"tzlocal is ignoring it, and you can likely delete it.")
|
|
found_configs = {k: v for k, v in found_configs.items() if "etc/timezone" not in k}
|
|
unique_tzs = _get_unique_tzs(found_configs, _root)
|
|
|
|
if len(unique_tzs) != 1:
|
|
message = "Multiple conflicting time zone configurations found:\n"
|
|
for key, value in found_configs.items():
|
|
message += f"{key}: {value}\n"
|
|
message += "Fix the configuration, or set the time zone in a TZ environment variable.\n"
|
|
raise zoneinfo.ZoneInfoNotFoundError(message)
|
|
|
|
# We found exactly one config! Use it.
|
|
return list(found_configs.values())[0]
|
|
|
|
|
|
def _get_unique_tzs(found_configs, _root):
|
|
unique_tzs = set()
|
|
zoneinfopath = os.path.join(_root, "usr", "share", "zoneinfo")
|
|
directory_depth = len(zoneinfopath.split(os.path.sep))
|
|
|
|
for tzname in found_configs.values():
|
|
# Look them up in /usr/share/zoneinfo, and find what they
|
|
# really point to:
|
|
path = os.path.realpath(os.path.join(zoneinfopath, *tzname.split("/")))
|
|
real_zone_name = "/".join(path.split(os.path.sep)[directory_depth:])
|
|
unique_tzs.add(real_zone_name)
|
|
|
|
return unique_tzs
|
|
|
|
|
|
def _get_localzone(_root="/"):
|
|
"""Creates a timezone object from the timezone name.
|
|
|
|
If there is no timezone config, it will try to create a file from the
|
|
localtime timezone, and if there isn't one, it will default to UTC.
|
|
|
|
The parameter _root makes the function look for files like /etc/localtime
|
|
beneath the _root directory. This is primarily used by the tests.
|
|
In normal usage you call the function without parameters."""
|
|
|
|
# First try the ENV setting.
|
|
tzenv = utils._tz_from_env()
|
|
if tzenv:
|
|
return tzenv
|
|
|
|
tzname = _get_localzone_name(_root)
|
|
if tzname is None:
|
|
# No explicit setting existed. Use localtime
|
|
log.debug("No explicit setting existed. Use localtime")
|
|
for filename in ("etc/localtime", "usr/local/etc/localtime"):
|
|
tzpath = os.path.join(_root, filename)
|
|
|
|
if not os.path.exists(tzpath):
|
|
continue
|
|
with open(tzpath, "rb") as tzfile:
|
|
tz = zoneinfo.ZoneInfo.from_file(tzfile, key="local")
|
|
break
|
|
else:
|
|
warnings.warn("Can not find any timezone configuration, defaulting to UTC.")
|
|
utcname = [x for x in zoneinfo.available_timezones() if "UTC" in x]
|
|
if utcname:
|
|
tz = zoneinfo.ZoneInfo(utcname[0])
|
|
else:
|
|
tz = timezone.utc
|
|
else:
|
|
tz = zoneinfo.ZoneInfo(tzname)
|
|
|
|
if _root == "/":
|
|
# We are using a file in etc to name the timezone.
|
|
# Verify that the timezone specified there is actually used:
|
|
utils.assert_tz_offset(tz, error=False)
|
|
return tz
|
|
|
|
|
|
def get_localzone_name() -> str:
|
|
"""Get the computers configured local timezone name, if any."""
|
|
global _cache_tz_name
|
|
if _cache_tz_name is None:
|
|
_cache_tz_name = _get_localzone_name()
|
|
|
|
return _cache_tz_name
|
|
|
|
|
|
def get_localzone() -> zoneinfo.ZoneInfo:
|
|
"""Get the computers configured local timezone, if any."""
|
|
|
|
global _cache_tz
|
|
if _cache_tz is None:
|
|
_cache_tz = _get_localzone()
|
|
|
|
return _cache_tz
|
|
|
|
|
|
def reload_localzone() -> zoneinfo.ZoneInfo:
|
|
"""Reload the cached localzone. You need to call this if the timezone has changed."""
|
|
global _cache_tz_name
|
|
global _cache_tz
|
|
_cache_tz_name = _get_localzone_name()
|
|
_cache_tz = _get_localzone()
|
|
|
|
return _cache_tz
|