lesscode-flask 0.2.115__tar.gz → 0.2.117__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/PKG-INFO +1 -1
  2. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/__init__.py +1 -1
  3. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/setting/__init__.py +1 -1
  4. lesscode_flask-0.2.117/lesscode_flask/utils/oss/__init__.py +352 -0
  5. lesscode_flask-0.2.117/lesscode_flask/utils/oss/rustfs_oss.py +170 -0
  6. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/sign/signature.py +13 -2
  7. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask.egg-info/PKG-INFO +1 -1
  8. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask.egg-info/SOURCES.txt +1 -0
  9. lesscode_flask-0.2.115/lesscode_flask/utils/oss/__init__.py +0 -165
  10. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/README.md +0 -0
  11. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/app.py +0 -0
  12. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/db/__init__.py +0 -0
  13. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/db/datasource.py +0 -0
  14. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/db/executor.py +0 -0
  15. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/export_data/__init__.py +0 -0
  16. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/export_data/data_download_handler.py +0 -0
  17. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/log/access_log_handler.py +0 -0
  18. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/model/access_log.py +0 -0
  19. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/model/base_model.py +0 -0
  20. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/model/parameterized_query.py +0 -0
  21. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/model/resource_param_template.py +0 -0
  22. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/model/response_result.py +0 -0
  23. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/model/user.py +0 -0
  24. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/model/user_limit_policy.py +0 -0
  25. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/service/access_log_service.py +0 -0
  26. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/service/base_service.py +0 -0
  27. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/service/resource_param_template_service.py +0 -0
  28. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/setup/__init__.py +0 -0
  29. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/signals.py +0 -0
  30. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/static/swagger.py +0 -0
  31. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/__init__.py +0 -0
  32. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/decorator/__init__.py +0 -0
  33. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/decorator/cache.py +0 -0
  34. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/decorator/sql_injection.py +0 -0
  35. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/decorator/swagger.py +0 -0
  36. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/dify_utils.py +0 -0
  37. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/file/file_exporter.py +0 -0
  38. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/file/file_utils.py +0 -0
  39. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/fs_util.py +0 -0
  40. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/helpers.py +0 -0
  41. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/json/NotSortJSONProvider.py +0 -0
  42. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/limit/__init__.py +0 -0
  43. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/limit/consecutive/consecutive_limiter_handler.py +0 -0
  44. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/limit/consecutive/redis_consecutive_limiter.py +0 -0
  45. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/limit/limit_util.py +0 -0
  46. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/limit/req/rate_limiter_handler.py +0 -0
  47. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/limit/req/redis_rate_limiter.py +0 -0
  48. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/limit/req_count/count_limiter_handler.py +0 -0
  49. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/limit/req_count/redis_count_limiter.py +0 -0
  50. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/oss/aliyun_oss.py +0 -0
  51. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/oss/ks3_oss.py +0 -0
  52. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/oss/minio_oss.py +0 -0
  53. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/redis/redis_helper.py +0 -0
  54. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/request/request.py +0 -0
  55. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/sign/__init__.py +0 -0
  56. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/sign/body_canonical.py +0 -0
  57. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/sign/sign_main.py +0 -0
  58. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/swagger/swagger_template.py +0 -0
  59. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/swagger/swagger_util.py +0 -0
  60. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/task/__init__.py +0 -0
  61. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/task/task_helper.py +0 -0
  62. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/utils/thread/thread_utils.py +0 -0
  63. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask/wsgi.py +0 -0
  64. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask.egg-info/dependency_links.txt +0 -0
  65. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask.egg-info/requires.txt +0 -0
  66. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/lesscode_flask.egg-info/top_level.txt +0 -0
  67. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/query_runner/__init__.py +0 -0
  68. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/query_runner/clickhouse.py +0 -0
  69. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/query_runner/dameng.py +0 -0
  70. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/query_runner/elasticsearch.py +0 -0
  71. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/query_runner/kingbase.py +0 -0
  72. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/query_runner/mysql.py +0 -0
  73. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/query_runner/pg.py +0 -0
  74. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/settings/__init__.py +0 -0
  75. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/settings/helpers.py +0 -0
  76. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/utils/__init__.py +0 -0
  77. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/redash/utils/requests_session.py +0 -0
  78. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/setup.cfg +0 -0
  79. {lesscode_flask-0.2.115 → lesscode_flask-0.2.117}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lesscode-flask
3
- Version: 0.2.115
3
+ Version: 0.2.117
4
4
  Summary: lesscode-flask 是基于flask的web开发脚手架项目,该项目初衷为简化开发过程,让研发人员更加关注业务。
5
5
  Home-page: https://lesscode-flask
6
6
  Author: Chao.yy
@@ -1,4 +1,4 @@
1
- __version__ = "0.2.115"
1
+ __version__ = "0.2.117"
2
2
 
3
3
  import functools
4
4
  import logging
@@ -100,7 +100,7 @@ class BaseConfig:
100
100
  SIGN_NONCE_ENABLE: bool = True
101
101
  # nonce缓存时长(秒)
102
102
  SIGN_NONCE_TTL_SEC: int = 300
103
- # 签名白名单路径前缀
103
+ # 签名白名单路径,普通配置按前缀匹配;带 * 时支持任意内容通配,例如 /a/*/b
104
104
  SIGN_WHITE_LIST: list = [SWAGGER_URL, SWAGGER_API_URL, f"{ROUTE_PREFIX}/oauth/token", f"{ROUTE_PREFIX}/oauth/captcha"]
105
105
  # 签名IP白名单(支持单IP或CIDR,命中后跳过签名校验)
106
106
  SIGN_IP_WHITE_LIST: list = []
@@ -0,0 +1,352 @@
1
+ import copy
2
+ import os
3
+ from io import BytesIO
4
+
5
+ from lesscode_flask.utils.helpers import app_config
6
+ from lesscode_flask.utils.oss.aliyun_oss import AliYunOss
7
+ from lesscode_flask.utils.oss.ks3_oss import Ks3Oss
8
+ from lesscode_flask.utils.oss.minio_oss import MinioOss
9
+ from lesscode_flask.utils.oss.rustfs_oss import RustfsOss
10
+
11
+
12
+ class CommonOss:
13
+ def __init__(self, storage_type=None, data_type="stream", config_key="default", **kwargs):
14
+ """
15
+ 初始化统一 OSS 入口。
16
+
17
+ Args:
18
+ storage_type (str): 存储类型,可选值:
19
+ - "ks3" : 金山云 KS3 对象存储,依赖 ks3sdk==1.5.0
20
+ - "aliyun" : 阿里云 OSS,依赖 oss2
21
+ - "minio" : MinIO / 自托管 S3,依赖 minio
22
+ - "rustfs" : RustFS(S3 兼容),依赖 boto3
23
+ - "file" : 本地文件系统存储,无额外依赖
24
+ data_type (str): 数据来源类型,可选值:
25
+ - "stream" : 从文件流(BytesIO / werkzeug FileStorage)上传(默认)
26
+ - "file_path" : 从本地文件路径上传
27
+ config_key (str): 读取 app.config["STORAGE_CONFIG"] 时使用的配置键名,默认 "default"。
28
+ STORAGE_CONFIG 示例结构:
29
+ STORAGE_CONFIG = {
30
+ "default": {
31
+ "storage_type": "<storage_type>",
32
+ "storage_config": { ...各存储类型连接参数... }
33
+ }
34
+ }
35
+ **kwargs:
36
+ storage_config (dict): 直接传入连接参数字典,会覆盖 STORAGE_CONFIG 中的 storage_config。
37
+ bucket_name (str) : 直接指定桶名,会覆盖 STORAGE_CONFIG 中的 bucket_name。
38
+
39
+ 各存储类型的 storage_config 参数说明
40
+ =====================================
41
+
42
+ ── ks3(金山云 KS3)──────────────────────────────────────────
43
+ 依赖:pip install ks3sdk==1.5.0
44
+ storage_config 字段:
45
+ access_key_id (str, 必填) : KS3 Access Key ID
46
+ access_key_secret (str, 必填) : KS3 Access Key Secret
47
+ host (str, 必填) : KS3 服务域名,例如 "ks3-cn-beijing.ksyun.com"
48
+ is_secure (bool, 可选): 是否启用 HTTPS,默认 False
49
+ bucket_name (str, 必填) : 默认操作的桶名
50
+ STORAGE_CONFIG 示例:
51
+ "storage_config": {
52
+ "access_key_id": "your-access-key-id",
53
+ "access_key_secret": "your-access-key-secret",
54
+ "host": "ks3-cn-beijing.ksyun.com",
55
+ "is_secure": False,
56
+ "bucket_name": "your-bucket"
57
+ }
58
+ save() 返回的 URL 格式:
59
+ https://<bucket_name>.<region>.ksyun.com/<key>
60
+ region 默认 "ks3-cn-beijing",domain 默认 "ksyun.com"
61
+
62
+ ── aliyun(阿里云 OSS)──────────────────────────────────────
63
+ 依赖:pip install oss2
64
+ storage_config 字段:
65
+ access_key_id (str, 必填) : 阿里云 Access Key ID
66
+ access_key_secret (str, 必填) : 阿里云 Access Key Secret
67
+ endpoint (str, 必填) : OSS Endpoint,例如 "https://oss-cn-hangzhou.aliyuncs.com"
68
+ bucket_name (str, 必填) : 默认操作的桶名
69
+ region (str, 可选) : 地域标识,默认 "oss-cn-hangzhou",用于拼接公网 URL
70
+ domain (str, 可选) : 域名后缀,默认 "aliyuncs.com"
71
+ protocol (str, 可选) : URL 协议,默认 "https"
72
+ session (obj, 可选) : oss2 Session 对象
73
+ connect_timeout (int, 可选) : 连接超时秒数
74
+ proxies (dict, 可选): 代理配置
75
+ cloudbox_id (str, 可选) : 云盒 ID(云盒场景使用)
76
+ is_path_style (bool, 可选): 是否使用 Path-Style 访问,默认 False
77
+ STORAGE_CONFIG 示例:
78
+ "storage_config": {
79
+ "access_key_id": "your-access-key-id",
80
+ "access_key_secret": "your-access-key-secret",
81
+ "endpoint": "https://oss-cn-hangzhou.aliyuncs.com",
82
+ "bucket_name": "your-bucket",
83
+ "region": "oss-cn-hangzhou"
84
+ }
85
+ save() 返回的 URL 格式:
86
+ https://<bucket_name>.<region>.aliyuncs.com/<key>
87
+
88
+ ── minio(MinIO / 自托管 S3 兼容)──────────────────────────
89
+ 依赖:pip install minio
90
+ storage_config 字段:
91
+ service (str, 必填) : MinIO 服务地址(不含协议),例如 "127.0.0.1:9000"
92
+ access_key (str, 必填) : MinIO Access Key(用户名)
93
+ secret_key (str, 必填) : MinIO Secret Key(密码)
94
+ secure (bool, 可选): 是否启用 HTTPS,默认 False
95
+ bucket_name (str, 必填) : 默认操作的桶名
96
+ STORAGE_CONFIG 示例:
97
+ "storage_config": {
98
+ "service": "127.0.0.1:9000",
99
+ "access_key": "minioadmin",
100
+ "secret_key": "minioadmin",
101
+ "secure": False,
102
+ "bucket_name": "your-bucket"
103
+ }
104
+ save() 返回的 URL 格式:
105
+ presigned_get_object 生成的预签名 URL(有效期 3600 秒)
106
+
107
+ ── rustfs(RustFS,S3 兼容协议)─────────────────────────────
108
+ 依赖:pip install boto3
109
+ storage_config 字段:
110
+ endpoint (str, 必填) : RustFS 服务地址,可含协议头,
111
+ 例如 "http://127.0.0.1:9000" 或 "127.0.0.1:9000"
112
+ access_key (str, 必填) : Access Key ID
113
+ secret_key (str, 必填) : Secret Access Key
114
+ secure (bool, 可选): 为 True 时自动补全 "https://",默认 False(即 "http://");
115
+ 若 endpoint 已含协议头则忽略此参数
116
+ region (str, 可选) : S3 Region,默认 "us-east-1"(RustFS 通常不校验)
117
+ bucket_name (str, 必填) : 默认操作的桶名
118
+ STORAGE_CONFIG 示例:
119
+ "storage_config": {
120
+ "endpoint": "http://127.0.0.1:9000",
121
+ "access_key": "rustfsadmin",
122
+ "secret_key": "rustfsadmin",
123
+ "secure": False,
124
+ "region": "us-east-1",
125
+ "bucket_name": "your-bucket"
126
+ }
127
+ save() 返回的 URL 格式:
128
+ <endpoint_url>/<bucket_name>/<key>
129
+ 例如:http://127.0.0.1:9000/your-bucket/path/to/file.jpg
130
+
131
+ ── file(本地文件系统)──────────────────────────────────────
132
+ 无额外依赖
133
+ storage_config 字段:
134
+ STORAGE_DIR (str, 必填) : 本地存储根目录的绝对路径,
135
+ 例如 "/data/uploads"
136
+ bucket_name 不适用,文件直接按 key 的路径层级写入 STORAGE_DIR
137
+ STORAGE_CONFIG 示例:
138
+ "storage_config": {
139
+ "STORAGE_DIR": "/data/uploads"
140
+ }
141
+ save() 返回的 URL 格式:
142
+ 文件在磁盘上的完整绝对路径,例如 "/data/uploads/2024/01/photo.jpg"
143
+ 注意:
144
+ - key 中的路径分隔符("/" 或 "\\")会被转换为系统路径,目录不存在时自动创建
145
+ - data_type="file_path" 时会先读取源文件再写入目标路径
146
+ """
147
+ storage_config_setting = app_config.get("STORAGE_CONFIG", {})
148
+ storage_config = copy.deepcopy(storage_config_setting) or dict()
149
+ _storage_config = storage_config.get(config_key, {}) or dict()
150
+ _storage_config_storage_type = _storage_config.get("storage_type", "")
151
+ _storage_type = storage_type if storage_type else _storage_config_storage_type
152
+ _config_storage_config = _storage_config.get("storage_config", {})
153
+ _config_bucket_name = _config_storage_config.pop("bucket_name", "")
154
+ self.storage_type = _storage_type
155
+ self.data_type = data_type
156
+ self.storage_config = kwargs.get("storage_config", {}) if kwargs.get("storage_config",
157
+ {}) else _config_storage_config
158
+ self.bucket_name = kwargs.get("bucket_name") or _config_bucket_name
159
+
160
+ def _save(self, key, io_stream: BytesIO = None, file_path: str = None, bucket_name: str = None):
161
+ file_url_obj = dict()
162
+ if self.storage_type == "ks3":
163
+ if self.data_type == "stream":
164
+ storage_config = self.storage_config or dict()
165
+ ks3 = Ks3Oss(bucket_name=bucket_name or self.bucket_name, **storage_config)
166
+ url = ks3.save(key=key, string_data=io_stream.getvalue(), content_type="string", policy="public-read",
167
+ bucket_name=bucket_name or self.bucket_name)
168
+ file_url_obj = {"key": key, "url": url}
169
+ elif self.data_type == "file_path":
170
+ storage_config = self.storage_config or dict()
171
+ ks3 = Ks3Oss(bucket_name=bucket_name or self.bucket_name, **storage_config)
172
+ url = ks3.save(key=key, filename=file_path, content_type="filename", policy="public-read")
173
+ file_url_obj = {"key": key, "url": url}
174
+ elif self.storage_type == "aliyun":
175
+ storage_config = self.storage_config or dict()
176
+ aliyun = AliYunOss(bucket_name=bucket_name or self.bucket_name, **storage_config)
177
+ url = aliyun.save(key=key, content_type="string", data=io_stream.getvalue(),
178
+ bucket_name=bucket_name or self.bucket_name)
179
+ file_url_obj = {"key": key, "url": url}
180
+ elif self.storage_type == "minio":
181
+ storage_config = self.storage_config or dict()
182
+ minio = MinioOss(**storage_config)
183
+ url = minio.save(key=key, content_type="string", data=io_stream.getvalue(),
184
+ bucket_name=bucket_name or self.bucket_name)
185
+ file_url_obj = {"key": key, "url": url}
186
+ elif self.storage_type == "rustfs":
187
+ storage_config = self.storage_config or dict()
188
+ rustfs = RustfsOss(**storage_config)
189
+ url = rustfs.save(key=key, content_type="string", data=io_stream.getvalue(),
190
+ bucket_name=bucket_name or self.bucket_name)
191
+ file_url_obj = {"key": key, "url": url}
192
+ elif self.storage_type == "file":
193
+ storage_path = ""
194
+ storage_dir = self.storage_config.get("STORAGE_DIR", "")
195
+ if not storage_dir:
196
+ raise Exception("storage_dir is empty")
197
+ if self.data_type == "stream":
198
+ if "\\" in key:
199
+ key_list = key.split("\\")
200
+ elif "/" in key:
201
+ key_list = key.split("/")
202
+ else:
203
+ key_list = [key]
204
+ storage_path = storage_dir
205
+ if key_list:
206
+ for k in key_list:
207
+ storage_path = os.path.join(storage_path, k)
208
+ dir_path = os.path.dirname(storage_path)
209
+ if not os.path.exists(dir_path):
210
+ os.makedirs(dir_path)
211
+ elif self.data_type == "file_path":
212
+ with open(file_path, 'rb') as infile:
213
+ io_stream = BytesIO(infile.read())
214
+ if storage_path:
215
+ with open(storage_path, 'wb') as outfile:
216
+ outfile.write(io_stream.getvalue())
217
+ file_url_obj = {"key": key, "url": storage_path}
218
+ return file_url_obj
219
+
220
+ def upload(self, **kwargs):
221
+ """上传文件到对象存储。
222
+
223
+ Args:
224
+ files (list, 必填): 待上传的文件列表。根据 data_type 不同,列表元素格式不同:
225
+
226
+ data_type="stream" 时,每个元素可以是:
227
+ - werkzeug FileStorage 对象(Flask request.files 中的文件)
228
+ 内部会读取 f.filename 作为 key,f.stream.read() 作为内容
229
+ - dict,格式:
230
+ {
231
+ "key": str, # 对象存储中的文件路径/名称,例如 "images/2024/photo.jpg"
232
+ "stream": werkzeug.FileStorage # 文件流对象
233
+ }
234
+
235
+ data_type="file_path" 时,每个元素可以是:
236
+ - str,本地文件的绝对路径,例如 "/tmp/photo.jpg"
237
+ key 自动取路径中的文件名部分
238
+ - dict,格式:
239
+ {
240
+ "key": str, # 对象存储中的文件路径/名称
241
+ "file_path": str # 本地文件的绝对路径
242
+ }
243
+
244
+ bucket_name (str, 可选): 上传到的目标桶名,默认使用初始化时指定的 bucket_name。
245
+
246
+ Returns:
247
+ list[dict]: 每个元素对应一个上传结果,格式为:
248
+ {
249
+ "key": str, # 上传时使用的对象 key
250
+ "url": str # 文件的访问地址:
251
+ # ks3 → https://<bucket>.<region>.ksyun.com/<key>
252
+ # aliyun → https://<bucket>.<region>.aliyuncs.com/<key>
253
+ # minio → presigned URL(有效期 3600 秒)
254
+ # rustfs → http(s)://<endpoint>/<bucket>/<key>
255
+ # file → 本地文件绝对路径
256
+ }
257
+
258
+ Raises:
259
+ Exception: files 为空时抛出 "files is empty"
260
+ """
261
+ file_url_list = []
262
+ files = kwargs.get("files", [])
263
+ bucket_name = kwargs.get("bucket_name", self.bucket_name)
264
+ if not files:
265
+ raise Exception("files is empty")
266
+ if self.data_type == "stream":
267
+ for f in files:
268
+ if not isinstance(f, dict):
269
+ key = f.filename
270
+ stream = f.stream.read()
271
+ file_stream = BytesIO(stream)
272
+ else:
273
+ key = f.get("key", "")
274
+ _steam = f.get("stream")
275
+ stream = _steam.stream.read()
276
+ file_stream = BytesIO(stream)
277
+ file_url_obj = self._save(key=key, io_stream=file_stream, bucket_name=bucket_name)
278
+ file_url_list.append(file_url_obj)
279
+
280
+ elif self.data_type == "file_path":
281
+ for f in files:
282
+ if not isinstance(f, dict):
283
+ if "\\" in f:
284
+ file_name = f.split("\\")[-1]
285
+ elif "/" in f:
286
+ file_name = f.split("/")[-1]
287
+ else:
288
+ file_name = f
289
+ key = file_name
290
+ _file_path = f
291
+ else:
292
+ key = f.get("key", "")
293
+ _file_path = f.get("file_path")
294
+ file_url_obj = self._save(key=key, file_path=_file_path, bucket_name=bucket_name)
295
+ file_url_list.append(file_url_obj)
296
+ return file_url_list
297
+
298
+ def download(self, key, bucket_name=None):
299
+ """从对象存储下载文件,返回文件内容。
300
+
301
+ Args:
302
+ key (str, 必填): 文件在对象存储中的 key(即上传时使用的路径/名称),
303
+ 例如 "images/2024/photo.jpg"。
304
+ 对于 storage_type="file",key 同样支持 "/" 或 "\\" 分隔的相对路径,
305
+ 会与 STORAGE_DIR 拼接成完整本地路径。
306
+ bucket_name (str, 可选): 目标桶名,默认使用初始化时指定的 bucket_name。
307
+
308
+ Returns:
309
+ 各存储类型返回值说明:
310
+ - ks3 : bytes,文件的原始字节内容
311
+ - aliyun : oss2 GetObjectResult 对象,支持 .read() 读取内容,
312
+ 也可通过 get_file(file_path=...) 直接保存到本地
313
+ - minio : urllib3 HTTPResponse 对象,支持 .read() 或迭代流式读取,
314
+ 使用后需调用 .close() 释放连接
315
+ - rustfs : botocore StreamingBody 对象,支持 .read() 或 .iter_chunks() 流式读取
316
+ - file : bytes,从本地文件读取的原始字节内容
317
+ 若 storage_type 未匹配任何已知类型,返回 None。
318
+
319
+ Raises:
320
+ FileNotFoundError: storage_type="file" 时,若本地文件不存在则抛出
321
+ """
322
+ if self.storage_type == "ks3":
323
+ storage_config = self.storage_config or dict()
324
+ ks3 = Ks3Oss(bucket_name=self.bucket_name, **storage_config)
325
+ return ks3.get_file(key=key, bucket_name=bucket_name or self.bucket_name)
326
+ elif self.storage_type == "aliyun":
327
+ storage_config = self.storage_config or dict()
328
+ aliyun = AliYunOss(bucket_name=self.bucket_name, **storage_config)
329
+ return aliyun.get_file(key=key, bucket_name=bucket_name or self.bucket_name)
330
+ elif self.storage_type == "minio":
331
+ storage_config = self.storage_config or dict()
332
+ minio = MinioOss(**storage_config)
333
+ return minio.get_file(bucket_name=bucket_name or self.bucket_name, key=key)
334
+ elif self.storage_type == "rustfs":
335
+ storage_config = self.storage_config or dict()
336
+ rustfs = RustfsOss(**storage_config)
337
+ return rustfs.get_file(bucket_name=bucket_name or self.bucket_name, key=key)
338
+ if self.storage_type == "file":
339
+ storage_dir = self.storage_config.get("STORAGE_DIR", "")
340
+ if "\\" in key:
341
+ key_list = key.split("\\")
342
+ elif "/" in key:
343
+ key_list = key.split("/")
344
+ else:
345
+ key_list = [key]
346
+ file_path = storage_dir
347
+ if key_list:
348
+ for k in key_list:
349
+ file_path = os.path.join(file_path, k)
350
+ with open(file_path, 'rb') as f:
351
+ return f.read()
352
+ return None
@@ -0,0 +1,170 @@
1
+ import importlib
2
+ import logging
3
+ from datetime import timedelta
4
+ from io import BytesIO
5
+
6
+
7
+ class RustfsOss:
8
+ """
9
+ RustFS 对象存储客户端(基于 S3 兼容协议,使用 boto3)
10
+ RustFS is an S3-compatible object storage written in Rust.
11
+ """
12
+
13
+ def __init__(self, endpoint, access_key, secret_key, secure=False, region="us-east-1"):
14
+ try:
15
+ boto3 = importlib.import_module("boto3")
16
+ except ImportError:
17
+ raise Exception("boto3 is not installed, run: pip install boto3")
18
+
19
+ protocol = "https" if secure else "http"
20
+ endpoint_url = endpoint if endpoint.startswith("http") else f"{protocol}://{endpoint}"
21
+
22
+ self.endpoint_url = endpoint_url
23
+ self.region = region
24
+ self.client = boto3.client(
25
+ "s3",
26
+ endpoint_url=endpoint_url,
27
+ aws_access_key_id=access_key,
28
+ aws_secret_access_key=secret_key,
29
+ region_name=region,
30
+ )
31
+
32
+ # ------------------------------------------------------------------
33
+ # Bucket operations
34
+ # ------------------------------------------------------------------
35
+
36
+ def exists_bucket(self, bucket_name: str) -> bool:
37
+ """判断桶是否存在"""
38
+ try:
39
+ self.client.head_bucket(Bucket=bucket_name)
40
+ return True
41
+ except Exception:
42
+ return False
43
+
44
+ def create_bucket(self, bucket_name: str, public_read: bool = True) -> bool:
45
+ """创建桶,可选设置公共读策略"""
46
+ if self.exists_bucket(bucket_name):
47
+ return False
48
+ self.client.create_bucket(Bucket=bucket_name)
49
+ if public_read:
50
+ import json
51
+ policy = json.dumps({
52
+ "Version": "2012-10-17",
53
+ "Statement": [
54
+ {
55
+ "Effect": "Allow",
56
+ "Principal": {"AWS": ["*"]},
57
+ "Action": ["s3:GetObject"],
58
+ "Resource": [f"arn:aws:s3:::{bucket_name}/*"],
59
+ }
60
+ ],
61
+ })
62
+ self.client.put_bucket_policy(Bucket=bucket_name, Policy=policy)
63
+ return True
64
+
65
+ def get_bucket_list(self) -> list:
66
+ """列出所有桶"""
67
+ resp = self.client.list_buckets()
68
+ return [
69
+ {"bucket_name": b["Name"], "create_time": b.get("CreationDate")}
70
+ for b in resp.get("Buckets", [])
71
+ ]
72
+
73
+ def remove_bucket(self, bucket_name: str) -> bool:
74
+ """删除桶(桶需为空)"""
75
+ try:
76
+ self.client.delete_bucket(Bucket=bucket_name)
77
+ return True
78
+ except Exception as e:
79
+ logging.error(f"[RustfsOss] remove_bucket error: {e}")
80
+ return False
81
+
82
+ # ------------------------------------------------------------------
83
+ # Object operations
84
+ # ------------------------------------------------------------------
85
+
86
+ def bucket_list_files(self, bucket_name: str, prefix: str = "") -> list:
87
+ """列出桶内所有对象"""
88
+ try:
89
+ paginator = self.client.get_paginator("list_objects_v2")
90
+ files = []
91
+ for page in paginator.paginate(Bucket=bucket_name, Prefix=prefix):
92
+ for obj in page.get("Contents", []):
93
+ files.append(obj)
94
+ return files
95
+ except Exception as e:
96
+ logging.error(f"[RustfsOss] bucket_list_files error: {e}")
97
+ return []
98
+
99
+ def save(self, key: str, bucket_name: str, data, content_type: str = "string", **kwargs) -> str:
100
+ """
101
+ 上传对象并返回访问 URL
102
+ :param key: 对象名(含路径)
103
+ :param bucket_name: 桶名
104
+ :param data: bytes(content_type="string")或本地文件路径(content_type="filename")
105
+ :param content_type: "string" | "filename"
106
+ :return: 公共访问 URL
107
+ """
108
+ if content_type == "filename":
109
+ with open(data, "rb") as f:
110
+ data = f.read()
111
+
112
+ if isinstance(data, str):
113
+ data = data.encode("utf-8")
114
+
115
+ self.client.put_object(
116
+ Bucket=bucket_name,
117
+ Key=key,
118
+ Body=BytesIO(data),
119
+ **kwargs,
120
+ )
121
+ return f"{self.endpoint_url}/{bucket_name}/{key}"
122
+
123
+ def get_file(self, bucket_name: str, key: str):
124
+ """下载对象,返回响应对象(含 .read() 方法)"""
125
+ resp = self.client.get_object(Bucket=bucket_name, Key=key)
126
+ return resp["Body"]
127
+
128
+ def download_file(self, bucket_name: str, key: str, file_path: str):
129
+ """下载对象并保存到本地文件"""
130
+ self.client.download_file(Bucket=bucket_name, Key=key, Filename=file_path)
131
+
132
+ def upload_file(self, bucket_name: str, key: str, file_path: str, content_type: str = "application/octet-stream"):
133
+ """从本地文件上传对象"""
134
+ with open(file_path, "rb") as f:
135
+ self.client.put_object(
136
+ Bucket=bucket_name,
137
+ Key=key,
138
+ Body=f,
139
+ ContentType=content_type,
140
+ )
141
+
142
+ def remove_file(self, bucket_name: str, key: str):
143
+ """删除单个对象"""
144
+ self.client.delete_object(Bucket=bucket_name, Key=key)
145
+
146
+ def remove_files(self, bucket_name: str, key_list: list):
147
+ """批量删除对象"""
148
+ objects = [{"Key": k} for k in key_list]
149
+ self.client.delete_objects(Bucket=bucket_name, Delete={"Objects": objects})
150
+
151
+ def stat_object(self, bucket_name: str, key: str):
152
+ """获取对象元数据"""
153
+ try:
154
+ return self.client.head_object(Bucket=bucket_name, Key=key)
155
+ except Exception as e:
156
+ logging.error(f"[RustfsOss] stat_object error: {e}")
157
+ return None
158
+
159
+ def copy_file(self, src_bucket: str, src_key: str, dst_bucket: str, dst_key: str):
160
+ """复制对象"""
161
+ copy_source = {"Bucket": src_bucket, "Key": src_key}
162
+ self.client.copy_object(CopySource=copy_source, Bucket=dst_bucket, Key=dst_key)
163
+
164
+ def presigned_get_file(self, bucket_name: str, key: str, days: int = 7) -> str:
165
+ """生成预签名 GET URL"""
166
+ return self.client.generate_presigned_url(
167
+ "get_object",
168
+ Params={"Bucket": bucket_name, "Key": key},
169
+ ExpiresIn=int(timedelta(days=days).total_seconds()),
170
+ )
@@ -2,6 +2,7 @@ import hashlib
2
2
  import hmac
3
3
  import ipaddress
4
4
  import logging
5
+ import re
5
6
  import time
6
7
 
7
8
  from flask import request
@@ -70,13 +71,23 @@ def _canonical_sign_content(timestamp: str, nonce: str) -> str:
70
71
  return f"{method}\n{path}\n{query_string}\n{body_hash}\n{timestamp}\n{nonce}"
71
72
 
72
73
 
74
+ def _match_sign_white_path(request_path: str, white_item: str) -> bool:
75
+ white_path = (white_item or "").strip()
76
+ if not white_path:
77
+ return False
78
+ if "*" not in white_path:
79
+ return request_path.startswith(white_path)
80
+ pattern = re.escape(white_path).replace(r"\*", ".*")
81
+ return re.match(f"^{pattern}", request_path) is not None
82
+
83
+
73
84
  def _in_sign_white_list(white_list: list) -> bool:
74
- # 白名单匹配规则:只要请求路径以前缀命中就跳过签名校验。
85
+ # 白名单匹配规则:普通配置按前缀匹配;带 * 时支持任意内容通配。
75
86
  if not white_list:
76
87
  return False
77
88
  request_path = request.path or ""
78
89
  for white_item in white_list:
79
- if white_item and request_path.startswith(white_item):
90
+ if _match_sign_white_path(request_path, white_item):
80
91
  return True
81
92
  return False
82
93
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lesscode-flask
3
- Version: 0.2.115
3
+ Version: 0.2.117
4
4
  Summary: lesscode-flask 是基于flask的web开发脚手架项目,该项目初衷为简化开发过程,让研发人员更加关注业务。
5
5
  Home-page: https://lesscode-flask
6
6
  Author: Chao.yy
@@ -51,6 +51,7 @@ lesscode_flask/utils/oss/__init__.py
51
51
  lesscode_flask/utils/oss/aliyun_oss.py
52
52
  lesscode_flask/utils/oss/ks3_oss.py
53
53
  lesscode_flask/utils/oss/minio_oss.py
54
+ lesscode_flask/utils/oss/rustfs_oss.py
54
55
  lesscode_flask/utils/redis/redis_helper.py
55
56
  lesscode_flask/utils/request/request.py
56
57
  lesscode_flask/utils/sign/__init__.py
@@ -1,165 +0,0 @@
1
- import copy
2
- import os
3
- from io import BytesIO
4
-
5
- from lesscode_flask.utils.helpers import app_config
6
- from lesscode_flask.utils.oss.aliyun_oss import AliYunOss
7
- from lesscode_flask.utils.oss.ks3_oss import Ks3Oss
8
- from lesscode_flask.utils.oss.minio_oss import MinioOss
9
-
10
-
11
- class CommonOss:
12
- def __init__(self, storage_type=None, data_type="stream", config_key="default", **kwargs):
13
- """
14
- 初始化OSS
15
- Args:
16
- storage_type (str): 存储类型,目前支持ks3和file
17
- data_type (str): 数据类型,目前支持stream和file_path
18
- storage_config (dict): 存储配置,目前支持ks3和file,file_name,aliyun,minio
19
- """
20
- storage_config_setting = app_config.get("STORAGE_CONFIG", {})
21
- storage_config = copy.deepcopy(storage_config_setting) or dict()
22
- _storage_config = storage_config.get(config_key, {}) or dict()
23
- _storage_config_storage_type = _storage_config.get("storage_type", "")
24
- _storage_type = storage_type if storage_type else _storage_config_storage_type
25
- _config_storage_config = _storage_config.get("storage_config", {})
26
- _config_bucket_name = _config_storage_config.pop("bucket_name", "")
27
- self.storage_type = _storage_type
28
- self.data_type = data_type
29
- self.storage_config = kwargs.get("storage_config", {}) if kwargs.get("storage_config",
30
- {}) else _config_storage_config
31
- self.bucket_name = kwargs.get("bucket_name") or _config_bucket_name
32
-
33
- def _save(self, key, io_stream: BytesIO = None, file_path: str = None, bucket_name: str = None):
34
- file_url_obj = dict()
35
- if self.storage_type == "ks3":
36
- if self.data_type == "stream":
37
- storage_config = self.storage_config or dict()
38
- ks3 = Ks3Oss(bucket_name=bucket_name or self.bucket_name, **storage_config)
39
- url = ks3.save(key=key, string_data=io_stream.getvalue(), content_type="string", policy="public-read",
40
- bucket_name=bucket_name or self.bucket_name)
41
- file_url_obj = {"key": key, "url": url}
42
- elif self.data_type == "file_path":
43
- storage_config = self.storage_config or dict()
44
- ks3 = Ks3Oss(bucket_name=bucket_name or self.bucket_name, **storage_config)
45
- url = ks3.save(key=key, filename=file_path, content_type="filename", policy="public-read")
46
- file_url_obj = {"key": key, "url": url}
47
- elif self.storage_type == "aliyun":
48
- storage_config = self.storage_config or dict()
49
- aliyun = AliYunOss(bucket_name=bucket_name or self.bucket_name, **storage_config)
50
- url = aliyun.save(key=key, content_type="string", data=io_stream.getvalue(),
51
- bucket_name=bucket_name or self.bucket_name)
52
- file_url_obj = {"key": key, "url": url}
53
- elif self.storage_type == "minio":
54
- storage_config = self.storage_config or dict()
55
- minio = MinioOss(**storage_config)
56
- url = minio.save(key=key, content_type="string", data=io_stream.getvalue(),
57
- bucket_name=bucket_name or self.bucket_name)
58
- file_url_obj = {"key": key, "url": url}
59
- elif self.storage_type == "file":
60
- storage_path = ""
61
- storage_dir = self.storage_config.get("STORAGE_DIR", "")
62
- if not storage_dir:
63
- raise Exception("storage_dir is empty")
64
- if self.data_type == "stream":
65
- if "\\" in key:
66
- key_list = key.split("\\")
67
- elif "/" in key:
68
- key_list = key.split("/")
69
- else:
70
- key_list = [key]
71
- storage_path = storage_dir
72
- if key_list:
73
- for k in key_list:
74
- storage_path = os.path.join(storage_path, k)
75
- dir_path = os.path.dirname(storage_path)
76
- if not os.path.exists(dir_path):
77
- os.makedirs(dir_path)
78
- elif self.data_type == "file_path":
79
- with open(file_path, 'rb') as infile:
80
- io_stream = BytesIO(infile.read())
81
- if storage_path:
82
- with open(storage_path, 'wb') as outfile:
83
- outfile.write(io_stream.getvalue())
84
- file_url_obj = {"key": key, "url": storage_path}
85
- return file_url_obj
86
-
87
- def upload(self, **kwargs):
88
- """上传文件
89
- Args:
90
- files (list): 文件列表 可以是文件流列表,也可以是字典列表,字典格式为{"key":"文件key,可以是带路径的文件","stream":"文件流"}
91
- Returns:
92
- file_url_list: 文件url列表[{"key":"文件key,可以是带路径的文件","url":"本地文件存储的文件的全路径,对象存储,存放的是文件的下载地址"}]
93
- """
94
- file_url_list = []
95
- files = kwargs.get("files", [])
96
- bucket_name = kwargs.get("bucket_name", self.bucket_name)
97
- if not files:
98
- raise Exception("files is empty")
99
- if self.data_type == "stream":
100
- for f in files:
101
- if not isinstance(f, dict):
102
- key = f.filename
103
- stream = f.stream.read()
104
- file_stream = BytesIO(stream)
105
- else:
106
- key = f.get("key", "")
107
- _steam = f.get("stream")
108
- stream = _steam.stream.read()
109
- file_stream = BytesIO(stream)
110
- file_url_obj = self._save(key=key, io_stream=file_stream, bucket_name=bucket_name)
111
- file_url_list.append(file_url_obj)
112
-
113
- elif self.data_type == "file_path":
114
- for f in files:
115
- if not isinstance(f, dict):
116
- if "\\" in f:
117
- file_name = f.split("\\")[-1]
118
- elif "/" in f:
119
- file_name = f.split("/")[-1]
120
- else:
121
- file_name = f
122
- key = file_name
123
- _file_path = f
124
- else:
125
- key = f.get("key", "")
126
- _file_path = f.get("file_path")
127
- file_url_obj = self._save(key=key, file_path=_file_path, bucket_name=bucket_name)
128
- file_url_list.append(file_url_obj)
129
- return file_url_list
130
-
131
- def download(self, key, bucket_name=None):
132
- """下载文件,返回文件流
133
- Args:
134
- key (str): 上面接口返回的文件key
135
- Returns:
136
- file_stream: 文件流
137
- :param bucket_name:
138
- """
139
- if self.storage_type == "ks3":
140
- storage_config = self.storage_config or dict()
141
- ks3 = Ks3Oss(bucket_name=self.bucket_name, **storage_config)
142
- return ks3.get_file(key=key, bucket_name=bucket_name or self.bucket_name)
143
- elif self.storage_type == "aliyun":
144
- storage_config = self.storage_config or dict()
145
- aliyun = AliYunOss(bucket_name=self.bucket_name, **storage_config)
146
- return aliyun.get_file(key=key, bucket_name=bucket_name or self.bucket_name)
147
- elif self.storage_type == "minio":
148
- storage_config = self.storage_config or dict()
149
- minio = MinioOss(**storage_config)
150
- return minio.get_file(bucket_name=bucket_name or self.bucket_name, key=key)
151
- if self.storage_type == "file":
152
- storage_dir = self.storage_config.get("STORAGE_DIR", "")
153
- if "\\" in key:
154
- key_list = key.split("\\")
155
- elif "/" in key:
156
- key_list = key.split("/")
157
- else:
158
- key_list = [key]
159
- file_path = storage_dir
160
- if key_list:
161
- for k in key_list:
162
- file_path = os.path.join(file_path, k)
163
- with open(file_path, 'rb') as f:
164
- return f.read()
165
- return None