pixelarraylib 1.0.0__py3-none-any.whl
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.
- arraylib/__init__.py +36 -0
- arraylib/__main__.py +126 -0
- arraylib/aliyun/__init__.py +0 -0
- arraylib/aliyun/aliyun_email.py +130 -0
- arraylib/aliyun/billing.py +477 -0
- arraylib/aliyun/content_scanner.py +253 -0
- arraylib/aliyun/domain.py +434 -0
- arraylib/aliyun/eci.py +47 -0
- arraylib/aliyun/ecs.py +68 -0
- arraylib/aliyun/fc.py +142 -0
- arraylib/aliyun/oss.py +649 -0
- arraylib/aliyun/sms.py +59 -0
- arraylib/aliyun/sts.py +124 -0
- arraylib/db_utils/mysql.py +544 -0
- arraylib/db_utils/redis.py +373 -0
- arraylib/decorators/__init__.py +13 -0
- arraylib/decorators/decorators.py +194 -0
- arraylib/gitlab/__init__.py +0 -0
- arraylib/gitlab/code_analyzer.py +344 -0
- arraylib/gitlab/pypi_package_manager.py +61 -0
- arraylib/monitor/__init__.py +0 -0
- arraylib/monitor/feishu.py +132 -0
- arraylib/net/request.py +143 -0
- arraylib/scripts/__init__.py +22 -0
- arraylib/scripts/collect_code_to_txt.py +327 -0
- arraylib/scripts/create_test_case_files.py +100 -0
- arraylib/scripts/nginx_proxy_to_ecs.py +119 -0
- arraylib/scripts/remove_empty_lines.py +120 -0
- arraylib/scripts/summary_code_count.py +430 -0
- arraylib/system/__init__.py +0 -0
- arraylib/system/common.py +390 -0
- pixelarraylib-1.0.0.dist-info/METADATA +141 -0
- pixelarraylib-1.0.0.dist-info/RECORD +37 -0
- pixelarraylib-1.0.0.dist-info/WHEEL +5 -0
- pixelarraylib-1.0.0.dist-info/entry_points.txt +2 -0
- pixelarraylib-1.0.0.dist-info/licenses/LICENSE +21 -0
- pixelarraylib-1.0.0.dist-info/top_level.txt +1 -0
arraylib/aliyun/oss.py
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import traceback
|
|
4
|
+
import oss2
|
|
5
|
+
import hmac
|
|
6
|
+
import base64
|
|
7
|
+
import aiohttp
|
|
8
|
+
import asyncio
|
|
9
|
+
import xml.etree.ElementTree as ET
|
|
10
|
+
from hashlib import sha1
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from urllib.parse import quote, urlencode
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from arraylib.system.common import size_unit_convert, percentage
|
|
15
|
+
from arraylib.monitor.feishu import Feishu
|
|
16
|
+
import aiofiles
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
feishu_alert = Feishu("devtoolkit服务报警")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OSSUtils:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
access_key_id,
|
|
27
|
+
access_key_secret,
|
|
28
|
+
region_id,
|
|
29
|
+
bucket_name,
|
|
30
|
+
use_vpc=False,
|
|
31
|
+
):
|
|
32
|
+
self.bucket_name = bucket_name
|
|
33
|
+
if use_vpc:
|
|
34
|
+
self.endpoint = f"https://oss-{region_id}-internal.aliyuncs.com"
|
|
35
|
+
else:
|
|
36
|
+
self.endpoint = f"https://oss-{region_id}.aliyuncs.com"
|
|
37
|
+
self.client = self.get_oss_client(
|
|
38
|
+
access_key_id, access_key_secret, self.endpoint, bucket_name
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def get_oss_client(
|
|
42
|
+
self, access_key_id, access_key_secret, endpoint, bucket_name, retry=3
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
description:
|
|
46
|
+
获取OSS客户端
|
|
47
|
+
parameters:
|
|
48
|
+
access_key_id(str): 阿里云access_key_id
|
|
49
|
+
access_key_secret(str): 阿里云access_key_secret
|
|
50
|
+
endpoint(str): 阿里云endpoint
|
|
51
|
+
bucket_name(str): 阿里云bucket_name
|
|
52
|
+
return:
|
|
53
|
+
bucket(oss2.Bucket): OSS客户端
|
|
54
|
+
"""
|
|
55
|
+
for _ in range(retry):
|
|
56
|
+
try:
|
|
57
|
+
return oss2.Bucket(
|
|
58
|
+
oss2.Auth(access_key_id, access_key_secret), endpoint, bucket_name
|
|
59
|
+
)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(e)
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def list_objects(self, prefix, batch_size=100):
|
|
65
|
+
"""
|
|
66
|
+
description:
|
|
67
|
+
列出OSS中的对象,返回生成器
|
|
68
|
+
parameters:
|
|
69
|
+
prefix(str): 前缀
|
|
70
|
+
batch_size(int): 每批返回的对象数量
|
|
71
|
+
return:
|
|
72
|
+
generator: 对象列表生成器
|
|
73
|
+
"""
|
|
74
|
+
next_marker = ""
|
|
75
|
+
while True:
|
|
76
|
+
object_list = self.client.list_objects(
|
|
77
|
+
prefix=prefix, marker=next_marker, max_keys=batch_size
|
|
78
|
+
)
|
|
79
|
+
for obj in object_list.object_list:
|
|
80
|
+
if obj.size > 0:
|
|
81
|
+
yield obj
|
|
82
|
+
if not object_list.is_truncated:
|
|
83
|
+
break
|
|
84
|
+
next_marker = object_list.next_marker
|
|
85
|
+
|
|
86
|
+
def is_object(self, key):
|
|
87
|
+
"""
|
|
88
|
+
description:
|
|
89
|
+
判断某个对象是否在OSS中存在
|
|
90
|
+
parameters:
|
|
91
|
+
key(str): 对象的key
|
|
92
|
+
return:
|
|
93
|
+
flag(bool): 是否存在
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
return self.client.object_exists(key)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(e)
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def get_size(self, prefix, unit="B"):
|
|
102
|
+
"""
|
|
103
|
+
description:
|
|
104
|
+
获取OSS中对象或前缀下所有对象的大小
|
|
105
|
+
parameters:
|
|
106
|
+
prefix(str): 前缀
|
|
107
|
+
unit(str): 单位,默认B
|
|
108
|
+
return:
|
|
109
|
+
size(int): 对象或前缀下所有对象的大小
|
|
110
|
+
count(int): 对象或前缀下所有对象的个数
|
|
111
|
+
"""
|
|
112
|
+
object_list = self.list_objects(prefix)
|
|
113
|
+
total_size = 0
|
|
114
|
+
object_count = 0
|
|
115
|
+
for obj in object_list:
|
|
116
|
+
total_size += self.client.get_object_meta(obj.key).content_length
|
|
117
|
+
object_count += 1
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
size_unit_convert(total_size, input_unit="B", output_unit=unit),
|
|
121
|
+
object_count,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def delete(self, prefix):
|
|
125
|
+
"""
|
|
126
|
+
description:
|
|
127
|
+
删除OSS中的对象或前缀
|
|
128
|
+
parameters:
|
|
129
|
+
prefix(str): 对象或前缀的key
|
|
130
|
+
return:
|
|
131
|
+
flag(bool): 是否删除成功
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
for obj in self.list_objects(prefix):
|
|
135
|
+
self.client.delete_object(obj.key)
|
|
136
|
+
return True
|
|
137
|
+
except Exception as e:
|
|
138
|
+
feishu_alert.send(traceback.format_exc())
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def ls(self, prefix):
|
|
142
|
+
"""
|
|
143
|
+
description:
|
|
144
|
+
列出当前目录下的所有文件夹和文件的key
|
|
145
|
+
parameters:
|
|
146
|
+
prefix(str): 前缀
|
|
147
|
+
return:
|
|
148
|
+
key_list(list): 文件夹和文件的key列表
|
|
149
|
+
"""
|
|
150
|
+
if not prefix.endswith("/"):
|
|
151
|
+
prefix += "/"
|
|
152
|
+
|
|
153
|
+
key_list = []
|
|
154
|
+
next_marker = ""
|
|
155
|
+
|
|
156
|
+
while True:
|
|
157
|
+
result = self.client.list_objects(
|
|
158
|
+
prefix=prefix, marker=next_marker, max_keys=1000, delimiter="/"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
key_list.extend(
|
|
162
|
+
[{"type": "directory", "key": p} for p in result.prefix_list]
|
|
163
|
+
)
|
|
164
|
+
key_list.extend(
|
|
165
|
+
[{"type": "file", "key": obj.key} for obj in result.object_list]
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if not result.is_truncated:
|
|
169
|
+
break
|
|
170
|
+
next_marker = result.next_marker
|
|
171
|
+
|
|
172
|
+
return [key for key in key_list if key["key"] != prefix]
|
|
173
|
+
|
|
174
|
+
def get_last_modified(self, prefix):
|
|
175
|
+
"""
|
|
176
|
+
description:
|
|
177
|
+
获取OSS中对象或前缀下所有对象的最后修改时间
|
|
178
|
+
parameters:
|
|
179
|
+
prefix(str): 前缀
|
|
180
|
+
return:
|
|
181
|
+
last_modified(str): 最后修改时间
|
|
182
|
+
"""
|
|
183
|
+
if self.is_object(prefix):
|
|
184
|
+
obj_key = prefix
|
|
185
|
+
else:
|
|
186
|
+
obj_key = max(
|
|
187
|
+
self.list_objects(prefix),
|
|
188
|
+
key=lambda x: self.client.get_object_meta(x.key).last_modified,
|
|
189
|
+
).key
|
|
190
|
+
|
|
191
|
+
return datetime.fromtimestamp(
|
|
192
|
+
self.client.get_object_meta(obj_key).last_modified
|
|
193
|
+
).strftime("%Y-%m-%d %H:%M:%S")
|
|
194
|
+
|
|
195
|
+
def path_exists(self, prefix):
|
|
196
|
+
"""
|
|
197
|
+
description:
|
|
198
|
+
检查OSS中对象或前缀是否存在
|
|
199
|
+
parameters:
|
|
200
|
+
prefix(str): 前缀
|
|
201
|
+
return:
|
|
202
|
+
flag(bool): 是否存在
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
for obj in self.list_objects(prefix):
|
|
206
|
+
return True
|
|
207
|
+
return False
|
|
208
|
+
except Exception as e:
|
|
209
|
+
feishu_alert.send(traceback.format_exc())
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
def download_object(self, prefix, dir_path):
|
|
213
|
+
"""
|
|
214
|
+
description:
|
|
215
|
+
下载OSS中的对象
|
|
216
|
+
parameters:
|
|
217
|
+
prefix(str): 前缀
|
|
218
|
+
local_path(str): 本地文件路径
|
|
219
|
+
return:
|
|
220
|
+
flag(bool): 是否下载成功
|
|
221
|
+
"""
|
|
222
|
+
if not dir_path:
|
|
223
|
+
dir_path = "."
|
|
224
|
+
if not self.is_object(prefix):
|
|
225
|
+
return False
|
|
226
|
+
file_name = os.path.basename(prefix)
|
|
227
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
228
|
+
download_path = os.path.join(dir_path, file_name)
|
|
229
|
+
print("download_path", download_path)
|
|
230
|
+
try:
|
|
231
|
+
self.client.get_object_to_file(prefix, download_path)
|
|
232
|
+
return True
|
|
233
|
+
except Exception as e:
|
|
234
|
+
feishu_alert.send(f"oss download_object error: {traceback.format_exc()}")
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
def upload_object(self, prefix, local_path):
|
|
238
|
+
"""
|
|
239
|
+
description:
|
|
240
|
+
上传对象到OSS
|
|
241
|
+
parameters:
|
|
242
|
+
prefix(str): 前缀
|
|
243
|
+
local_path(str): 本地文件路径
|
|
244
|
+
return:
|
|
245
|
+
flag(bool): 是否上传成功
|
|
246
|
+
"""
|
|
247
|
+
if not os.path.exists(local_path):
|
|
248
|
+
print(f"文件 {local_path} 不存在,请检查!")
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
file_name = os.path.basename(local_path)
|
|
252
|
+
try:
|
|
253
|
+
self.client.put_object_from_file(
|
|
254
|
+
os.path.join(prefix, file_name), local_path
|
|
255
|
+
)
|
|
256
|
+
return True
|
|
257
|
+
except Exception as e:
|
|
258
|
+
feishu_alert.send(traceback.format_exc())
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
def generate_presigned_url(self, prefix, expires_in=60 * 60 * 24):
|
|
262
|
+
"""
|
|
263
|
+
description:
|
|
264
|
+
使用OSS生成预签名URL
|
|
265
|
+
parameters:
|
|
266
|
+
prefix(str): 前缀
|
|
267
|
+
expires_in(int): 过期时间,默认24小时
|
|
268
|
+
return:
|
|
269
|
+
url(str): 预签名URL
|
|
270
|
+
"""
|
|
271
|
+
return self.client.sign_url("GET", prefix, expires_in)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class OSSObject:
|
|
275
|
+
"""OSS 对象表示类"""
|
|
276
|
+
|
|
277
|
+
def __init__(self, key, size, last_modified, etag=None):
|
|
278
|
+
self.key = key
|
|
279
|
+
self.size = size
|
|
280
|
+
self.last_modified = last_modified
|
|
281
|
+
self.etag = etag
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class OSSUtilsAsync:
|
|
285
|
+
def __init__(
|
|
286
|
+
self, access_key_id, access_key_secret, region_id, bucket_name, use_vpc=False
|
|
287
|
+
):
|
|
288
|
+
self.access_key_id = access_key_id
|
|
289
|
+
self.access_key_secret = access_key_secret
|
|
290
|
+
self.bucket_name = bucket_name
|
|
291
|
+
if use_vpc:
|
|
292
|
+
self.endpoint = (
|
|
293
|
+
f"https://{self.bucket_name}.oss-{region_id}-internal.aliyuncs.com"
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
self.endpoint = f"https://{self.bucket_name}.oss-{region_id}.aliyuncs.com"
|
|
297
|
+
self.base_url = self.endpoint
|
|
298
|
+
self.session = None
|
|
299
|
+
self.thread_pool = ThreadPoolExecutor(max_workers=4)
|
|
300
|
+
|
|
301
|
+
# 创建oss2的Auth对象用于签名
|
|
302
|
+
self.auth = oss2.Auth(access_key_id, access_key_secret)
|
|
303
|
+
|
|
304
|
+
# 创建oss2的Bucket对象用于同步操作
|
|
305
|
+
correct_endpoint = self.endpoint.replace(f"{self.bucket_name}.", "")
|
|
306
|
+
self.oss_bucket = oss2.Bucket(self.auth, correct_endpoint, self.bucket_name)
|
|
307
|
+
|
|
308
|
+
async def _ensure_session(self):
|
|
309
|
+
"""确保 aiohttp 会话已创建"""
|
|
310
|
+
if self.session is None:
|
|
311
|
+
self.session = aiohttp.ClientSession()
|
|
312
|
+
return self.session
|
|
313
|
+
|
|
314
|
+
async def close(self):
|
|
315
|
+
"""关闭 aiohttp 会话和线程池"""
|
|
316
|
+
if self.session is not None:
|
|
317
|
+
await self.session.close()
|
|
318
|
+
self.session = None
|
|
319
|
+
if self.thread_pool is not None:
|
|
320
|
+
self.thread_pool.shutdown(wait=True)
|
|
321
|
+
self.thread_pool = None
|
|
322
|
+
|
|
323
|
+
async def _run_in_thread(self, func, *args, **kwargs):
|
|
324
|
+
"""在线程池中运行同步函数"""
|
|
325
|
+
loop = asyncio.get_event_loop()
|
|
326
|
+
return await loop.run_in_executor(self.thread_pool, func, *args, **kwargs)
|
|
327
|
+
|
|
328
|
+
def _sign_request(self, method, url, headers=None, params=None):
|
|
329
|
+
"""使用oss2库生成OSS签名"""
|
|
330
|
+
if headers is None:
|
|
331
|
+
headers = {}
|
|
332
|
+
if params is None:
|
|
333
|
+
params = {}
|
|
334
|
+
|
|
335
|
+
# 创建oss2的Request对象
|
|
336
|
+
req = oss2.http.Request(method, url, params=params, headers=headers)
|
|
337
|
+
|
|
338
|
+
# 使用oss2的签名功能
|
|
339
|
+
self.auth._sign_request(req, self.bucket_name, "")
|
|
340
|
+
|
|
341
|
+
# 过滤掉None键和None值
|
|
342
|
+
result = {}
|
|
343
|
+
for k, v in req.headers.items():
|
|
344
|
+
if k is not None and v is not None:
|
|
345
|
+
result[str(k)] = str(v)
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
def _parse_list_objects_xml(self, xml_text):
|
|
349
|
+
"""解析 OSS ListObjects 响应的 XML"""
|
|
350
|
+
try:
|
|
351
|
+
root = ET.fromstring(xml_text)
|
|
352
|
+
objects = []
|
|
353
|
+
|
|
354
|
+
# 查找所有 Contents 元素(不使用命名空间)
|
|
355
|
+
for contents in root.findall(".//Contents"):
|
|
356
|
+
key_elem = contents.find("Key")
|
|
357
|
+
size_elem = contents.find("Size")
|
|
358
|
+
last_modified_elem = contents.find("LastModified")
|
|
359
|
+
etag_elem = contents.find("ETag")
|
|
360
|
+
|
|
361
|
+
if key_elem is not None and size_elem is not None:
|
|
362
|
+
# URL解码key
|
|
363
|
+
from urllib.parse import unquote
|
|
364
|
+
|
|
365
|
+
key = unquote(key_elem.text) if key_elem.text else ""
|
|
366
|
+
size = int(size_elem.text) if size_elem.text else 0
|
|
367
|
+
last_modified = (
|
|
368
|
+
last_modified_elem.text
|
|
369
|
+
if last_modified_elem is not None
|
|
370
|
+
else None
|
|
371
|
+
)
|
|
372
|
+
etag = etag_elem.text if etag_elem is not None else None
|
|
373
|
+
|
|
374
|
+
# 只返回大小大于0的对象(排除目录)
|
|
375
|
+
if size > 0:
|
|
376
|
+
objects.append(OSSObject(key, size, last_modified, etag))
|
|
377
|
+
|
|
378
|
+
return objects
|
|
379
|
+
except ET.ParseError as e:
|
|
380
|
+
print(f"XML 解析错误: {e}")
|
|
381
|
+
return []
|
|
382
|
+
|
|
383
|
+
def _parse_ls_xml(self, xml_text, prefix):
|
|
384
|
+
"""解析 OSS ListObjects 响应的 XML (用于 ls 方法)"""
|
|
385
|
+
try:
|
|
386
|
+
root = ET.fromstring(xml_text)
|
|
387
|
+
key_list = []
|
|
388
|
+
|
|
389
|
+
# 查找所有 CommonPrefixes 元素 (目录) - 不使用命名空间
|
|
390
|
+
for prefix_elem in root.findall(".//CommonPrefixes"):
|
|
391
|
+
prefix_text = prefix_elem.find("Prefix")
|
|
392
|
+
if prefix_text is not None and prefix_text.text != prefix:
|
|
393
|
+
# URL解码key,与同步版本保持一致
|
|
394
|
+
from urllib.parse import unquote
|
|
395
|
+
|
|
396
|
+
decoded_key = unquote(prefix_text.text)
|
|
397
|
+
key_list.append({"type": "directory", "key": decoded_key})
|
|
398
|
+
|
|
399
|
+
# 查找所有 Contents 元素 (文件) - 不使用命名空间
|
|
400
|
+
for contents in root.findall(".//Contents"):
|
|
401
|
+
key_elem = contents.find("Key")
|
|
402
|
+
if key_elem is not None and key_elem.text != prefix:
|
|
403
|
+
# URL解码key,与同步版本保持一致
|
|
404
|
+
from urllib.parse import unquote
|
|
405
|
+
|
|
406
|
+
decoded_key = unquote(key_elem.text)
|
|
407
|
+
key_list.append({"type": "file", "key": decoded_key})
|
|
408
|
+
|
|
409
|
+
return key_list
|
|
410
|
+
except ET.ParseError as e:
|
|
411
|
+
print(f"XML 解析错误: {e}")
|
|
412
|
+
return []
|
|
413
|
+
|
|
414
|
+
async def list_objects(self, prefix="", max_keys=100):
|
|
415
|
+
"""列出对象,返回所有匹配的对象"""
|
|
416
|
+
session = await self._ensure_session()
|
|
417
|
+
all_objects = []
|
|
418
|
+
next_marker = ""
|
|
419
|
+
|
|
420
|
+
while True:
|
|
421
|
+
params = {
|
|
422
|
+
"prefix": prefix,
|
|
423
|
+
"max-keys": str(max_keys),
|
|
424
|
+
"delimiter": "",
|
|
425
|
+
"marker": next_marker,
|
|
426
|
+
"encoding-type": "url",
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
# 构建URL参数
|
|
430
|
+
param_str = "&".join(f"{k}={v}" for k, v in params.items())
|
|
431
|
+
url = f"{self.base_url}/?{param_str}"
|
|
432
|
+
headers = self._sign_request("GET", url, params=params)
|
|
433
|
+
async with session.get(url, headers=headers) as resp:
|
|
434
|
+
if resp.status != 200:
|
|
435
|
+
break
|
|
436
|
+
|
|
437
|
+
text = await resp.text()
|
|
438
|
+
objects = self._parse_list_objects_xml(text)
|
|
439
|
+
all_objects.extend(objects)
|
|
440
|
+
|
|
441
|
+
# 检查是否还有更多对象
|
|
442
|
+
root = ET.fromstring(text)
|
|
443
|
+
is_truncated = root.find(".//IsTruncated")
|
|
444
|
+
if is_truncated is None or is_truncated.text.lower() != "true":
|
|
445
|
+
break
|
|
446
|
+
|
|
447
|
+
# 获取下一页的marker
|
|
448
|
+
next_marker_elem = root.find(".//NextMarker")
|
|
449
|
+
if next_marker_elem is None:
|
|
450
|
+
break
|
|
451
|
+
next_marker = next_marker_elem.text
|
|
452
|
+
|
|
453
|
+
return all_objects
|
|
454
|
+
|
|
455
|
+
async def is_object(self, key):
|
|
456
|
+
"""判断对象是否存在"""
|
|
457
|
+
try:
|
|
458
|
+
# 使用list_objects来检查对象是否存在,避免签名问题
|
|
459
|
+
object_list = await self.list_objects(key)
|
|
460
|
+
# 检查是否有完全匹配的对象
|
|
461
|
+
for obj in object_list:
|
|
462
|
+
if obj.key == key:
|
|
463
|
+
return True
|
|
464
|
+
return False
|
|
465
|
+
except Exception as e:
|
|
466
|
+
print(f"is_object error: {e}")
|
|
467
|
+
return False
|
|
468
|
+
|
|
469
|
+
async def upload_object(self, prefix, local_path):
|
|
470
|
+
"""上传对象"""
|
|
471
|
+
if not os.path.exists(local_path):
|
|
472
|
+
return False
|
|
473
|
+
|
|
474
|
+
file_name = os.path.basename(local_path)
|
|
475
|
+
key = f"{prefix}/{file_name}" if prefix else file_name
|
|
476
|
+
|
|
477
|
+
# 使用线程池运行oss2的同步操作
|
|
478
|
+
try:
|
|
479
|
+
await self._run_in_thread(self.oss_bucket.put_object_from_file, key, local_path)
|
|
480
|
+
return True
|
|
481
|
+
except Exception as e:
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
async def download_object(self, key, dir_path="."):
|
|
485
|
+
"""下载对象"""
|
|
486
|
+
if dir_path and dir_path != "":
|
|
487
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
488
|
+
file_name = os.path.basename(key)
|
|
489
|
+
local_path = os.path.join(dir_path, file_name)
|
|
490
|
+
|
|
491
|
+
# 使用线程池运行oss2的同步操作
|
|
492
|
+
try:
|
|
493
|
+
await self._run_in_thread(self.oss_bucket.get_object_to_file, key, local_path)
|
|
494
|
+
return True
|
|
495
|
+
except Exception as e:
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
async def delete(self, prefix):
|
|
499
|
+
"""删除OSS中的对象或前缀"""
|
|
500
|
+
try:
|
|
501
|
+
object_list = await self.list_objects(prefix)
|
|
502
|
+
|
|
503
|
+
# 使用线程池批量删除对象
|
|
504
|
+
delete_tasks = []
|
|
505
|
+
for obj in object_list:
|
|
506
|
+
task = self._run_in_thread(self.oss_bucket.delete_object, obj.key)
|
|
507
|
+
delete_tasks.append(task)
|
|
508
|
+
|
|
509
|
+
# 等待所有删除操作完成
|
|
510
|
+
results = await asyncio.gather(*delete_tasks, return_exceptions=True)
|
|
511
|
+
|
|
512
|
+
# 检查是否有任何删除失败
|
|
513
|
+
for result in results:
|
|
514
|
+
if isinstance(result, Exception):
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
return True
|
|
518
|
+
except Exception as e:
|
|
519
|
+
print(f"删除对象时出错: {e}")
|
|
520
|
+
return False
|
|
521
|
+
|
|
522
|
+
async def get_size(self, prefix, unit="B"):
|
|
523
|
+
"""获取OSS中对象或前缀下所有对象的大小"""
|
|
524
|
+
object_list = await self.list_objects(prefix)
|
|
525
|
+
total_size = 0
|
|
526
|
+
object_count = 0
|
|
527
|
+
|
|
528
|
+
# 使用线程池获取每个对象的准确大小
|
|
529
|
+
size_tasks = []
|
|
530
|
+
for obj in object_list:
|
|
531
|
+
task = self._run_in_thread(self.oss_bucket.get_object_meta, obj.key)
|
|
532
|
+
size_tasks.append(task)
|
|
533
|
+
|
|
534
|
+
# 等待所有获取元数据操作完成
|
|
535
|
+
results = await asyncio.gather(*size_tasks, return_exceptions=True)
|
|
536
|
+
|
|
537
|
+
for result in results:
|
|
538
|
+
if not isinstance(result, Exception):
|
|
539
|
+
total_size += result.content_length
|
|
540
|
+
object_count += 1
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
size_unit_convert(total_size, input_unit="B", output_unit=unit),
|
|
544
|
+
object_count,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
async def ls(self, prefix):
|
|
548
|
+
"""列出当前目录下的所有文件夹和文件的key"""
|
|
549
|
+
if not prefix.endswith("/"):
|
|
550
|
+
prefix += "/"
|
|
551
|
+
|
|
552
|
+
session = await self._ensure_session()
|
|
553
|
+
key_list = []
|
|
554
|
+
next_marker = ""
|
|
555
|
+
|
|
556
|
+
while True:
|
|
557
|
+
params = {
|
|
558
|
+
"prefix": prefix,
|
|
559
|
+
"delimiter": "/",
|
|
560
|
+
"max-keys": "1000",
|
|
561
|
+
"marker": next_marker,
|
|
562
|
+
"encoding-type": "url",
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
url = f"{self.base_url}"
|
|
566
|
+
headers = self._sign_request("GET", "", params=params)
|
|
567
|
+
|
|
568
|
+
async with session.get(url, headers=headers, params=params) as resp:
|
|
569
|
+
if resp.status != 200:
|
|
570
|
+
break
|
|
571
|
+
|
|
572
|
+
text = await resp.text()
|
|
573
|
+
batch_key_list = self._parse_ls_xml(text, prefix)
|
|
574
|
+
key_list.extend(batch_key_list)
|
|
575
|
+
|
|
576
|
+
# 检查是否还有更多结果
|
|
577
|
+
root = ET.fromstring(text)
|
|
578
|
+
is_truncated = root.find(".//IsTruncated")
|
|
579
|
+
if is_truncated is None or is_truncated.text.lower() != "true":
|
|
580
|
+
break
|
|
581
|
+
|
|
582
|
+
# 获取下一页的marker
|
|
583
|
+
next_marker_elem = root.find(".//NextMarker")
|
|
584
|
+
if next_marker_elem is None:
|
|
585
|
+
break
|
|
586
|
+
next_marker = next_marker_elem.text
|
|
587
|
+
|
|
588
|
+
return key_list
|
|
589
|
+
|
|
590
|
+
async def get_last_modified(self, prefix):
|
|
591
|
+
"""获取OSS中对象或前缀下所有对象的最后修改时间,格式化为%Y-%m-%d %H:%M:%S"""
|
|
592
|
+
from datetime import datetime
|
|
593
|
+
|
|
594
|
+
def format_time(timestamp):
|
|
595
|
+
if not timestamp:
|
|
596
|
+
return ""
|
|
597
|
+
try:
|
|
598
|
+
# 将时间戳转换为datetime对象
|
|
599
|
+
dt = datetime.fromtimestamp(timestamp)
|
|
600
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
601
|
+
except Exception:
|
|
602
|
+
return ""
|
|
603
|
+
|
|
604
|
+
if await self.is_object(prefix):
|
|
605
|
+
# 如果是单个对象,直接获取其最后修改时间
|
|
606
|
+
try:
|
|
607
|
+
meta = await self._run_in_thread(self.oss_bucket.get_object_meta, prefix)
|
|
608
|
+
return format_time(meta.last_modified)
|
|
609
|
+
except Exception:
|
|
610
|
+
return ""
|
|
611
|
+
else:
|
|
612
|
+
# 如果是前缀,找到最后修改的对象 - 与同步版本保持一致
|
|
613
|
+
object_list = await self.list_objects(prefix)
|
|
614
|
+
if object_list:
|
|
615
|
+
# 使用线程池获取每个对象的最后修改时间
|
|
616
|
+
meta_tasks = []
|
|
617
|
+
for obj in object_list:
|
|
618
|
+
task = self._run_in_thread(self.oss_bucket.get_object_meta, obj.key)
|
|
619
|
+
meta_tasks.append(task)
|
|
620
|
+
|
|
621
|
+
# 等待所有获取元数据操作完成
|
|
622
|
+
results = await asyncio.gather(*meta_tasks, return_exceptions=True)
|
|
623
|
+
|
|
624
|
+
# 找到最后修改的对象
|
|
625
|
+
latest_time = 0
|
|
626
|
+
latest_meta = None
|
|
627
|
+
for result in results:
|
|
628
|
+
if not isinstance(result, Exception) and result.last_modified > latest_time:
|
|
629
|
+
latest_time = result.last_modified
|
|
630
|
+
latest_meta = result
|
|
631
|
+
|
|
632
|
+
if latest_meta:
|
|
633
|
+
return format_time(latest_meta.last_modified)
|
|
634
|
+
|
|
635
|
+
return ""
|
|
636
|
+
|
|
637
|
+
async def path_exists(self, prefix):
|
|
638
|
+
"""检查OSS中对象或前缀是否存在"""
|
|
639
|
+
try:
|
|
640
|
+
object_list = await self.list_objects(prefix)
|
|
641
|
+
return len(object_list) > 0
|
|
642
|
+
except Exception as e:
|
|
643
|
+
print(f"检查路径存在性时出错: {e}")
|
|
644
|
+
return False
|
|
645
|
+
|
|
646
|
+
async def generate_presigned_url(self, key, expires_in=3600):
|
|
647
|
+
"""生成预签名URL"""
|
|
648
|
+
# 使用线程池运行oss2的同步操作
|
|
649
|
+
return await self._run_in_thread(self.oss_bucket.sign_url, "GET", key, expires_in)
|
arraylib/aliyun/sms.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import traceback
|
|
3
|
+
from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
|
|
4
|
+
from alibabacloud_tea_openapi import models as open_api_models
|
|
5
|
+
from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
|
|
6
|
+
from alibabacloud_tea_util import models as util_models
|
|
7
|
+
from arraylib.monitor.feishu import Feishu
|
|
8
|
+
feishu_alert = Feishu("devtoolkit服务报警")
|
|
9
|
+
|
|
10
|
+
class SMSUtils:
|
|
11
|
+
def __init__(self, access_key_id, access_key_secret):
|
|
12
|
+
self.sms_cilent = Dysmsapi20170525Client(
|
|
13
|
+
open_api_models.Config(
|
|
14
|
+
type="access_key",
|
|
15
|
+
access_key_id=access_key_id,
|
|
16
|
+
access_key_secret=access_key_secret,
|
|
17
|
+
endpoint="dysmsapi.aliyuncs.com",
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def generate_verification_code(self, length=6):
|
|
22
|
+
"""
|
|
23
|
+
description:
|
|
24
|
+
生成数字验证码
|
|
25
|
+
parameters:
|
|
26
|
+
length(int): 验证码长度
|
|
27
|
+
return:
|
|
28
|
+
str: 验证码
|
|
29
|
+
"""
|
|
30
|
+
return "".join(str(random.randint(0, 9)) for _ in range(length))
|
|
31
|
+
|
|
32
|
+
def send_verification_code(self, phone_numbers, verification_code):
|
|
33
|
+
"""
|
|
34
|
+
description:
|
|
35
|
+
发送验证码给指定手机号
|
|
36
|
+
param:
|
|
37
|
+
phone_numbers: 手机号
|
|
38
|
+
verification_code: 验证码(6位数字)
|
|
39
|
+
return:
|
|
40
|
+
flag(bool): 是否发送成功
|
|
41
|
+
"""
|
|
42
|
+
send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
|
|
43
|
+
sign_name="北京矩阵像素科技有限公司",
|
|
44
|
+
template_code="SMS_318235798",
|
|
45
|
+
phone_numbers=phone_numbers,
|
|
46
|
+
template_param=f'{{"code":"{verification_code}"}}',
|
|
47
|
+
)
|
|
48
|
+
runtime = util_models.RuntimeOptions()
|
|
49
|
+
try:
|
|
50
|
+
response = self.sms_cilent.send_sms_with_options(send_sms_request, runtime)
|
|
51
|
+
flag = bool(
|
|
52
|
+
response and response.status_code == 200 and response.body.code == "OK"
|
|
53
|
+
)
|
|
54
|
+
if not flag:
|
|
55
|
+
feishu_alert.send(f"短信发送失败: {response}")
|
|
56
|
+
return flag
|
|
57
|
+
except Exception as e:
|
|
58
|
+
feishu_alert.send("短信发送失败: " + traceback.format_exc())
|
|
59
|
+
return False
|