fiuai-s3 0.1.1__tar.gz → 0.2.1__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.
Potentially problematic release.
This version of fiuai-s3 might be problematic. Click here for more details.
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/PKG-INFO +55 -1
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/README.md +54 -0
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/pyproject.toml +1 -1
- fiuai_s3-0.2.1/src/fiuai_s3/alicloud/alicloud_storage.py +152 -0
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/src/fiuai_s3/minio/minio_storage.py +65 -2
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/src/fiuai_s3/object_storage.py +68 -11
- fiuai_s3-0.1.1/src/fiuai_s3/alicloud/alicloud_storage.py +0 -101
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/.gitignore +0 -0
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/LICENSE +0 -0
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/src/fiuai_s3/__init__.py +0 -0
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/src/fiuai_s3/alicloud/__init__.py +0 -0
- {fiuai_s3-0.1.1 → fiuai_s3-0.2.1}/src/fiuai_s3/minio/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fiuai-s3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: 一个支持阿里云OSS和MinIO的对象存储抽象包
|
|
5
5
|
Project-URL: Homepage, https://github.com/fiuai-sz/fiuai-s3
|
|
6
6
|
Project-URL: Repository, https://github.com/fiuai-sz/fiuai-s3.git
|
|
@@ -286,6 +286,60 @@ S3_Client.delete_file("test.txt")
|
|
|
286
286
|
files = S3_Client.list_files(prefix="test/")
|
|
287
287
|
```
|
|
288
288
|
|
|
289
|
+
### 单据文件管理(推荐业务用法)
|
|
290
|
+
|
|
291
|
+
支持业务身份(auth_tenant_id, auth_company_id, doc_id)在实例初始化时注入(可为空),各操作方法参数可选,若不传则使用实例属性,若传则覆盖。
|
|
292
|
+
|
|
293
|
+
#### 初始化带业务身份
|
|
294
|
+
```python
|
|
295
|
+
from fiuai_s3 import ObjectStorageFactory
|
|
296
|
+
from fiuai_s3.object_storage import StorageConfig
|
|
297
|
+
|
|
298
|
+
config = StorageConfig(
|
|
299
|
+
provider="minio",
|
|
300
|
+
bucket_name="dev",
|
|
301
|
+
endpoint="http://127.0.0.1:19000",
|
|
302
|
+
access_key="devdevdev",
|
|
303
|
+
secret_key="devdevdev"
|
|
304
|
+
)
|
|
305
|
+
# 业务身份可选
|
|
306
|
+
S3_Client = ObjectStorageFactory.create_storage(
|
|
307
|
+
config,
|
|
308
|
+
auth_tenant_id="t1",
|
|
309
|
+
auth_company_id="c1",
|
|
310
|
+
doc_id="d1"
|
|
311
|
+
)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
#### 上传单据文件
|
|
315
|
+
```python
|
|
316
|
+
# 方法参数可选,优先级高于实例属性
|
|
317
|
+
S3_Client.upload_doc_file(
|
|
318
|
+
filename="发票.pdf",
|
|
319
|
+
data=b"...文件内容...",
|
|
320
|
+
tags={"业务类型": "发票", "年份": "2024"}
|
|
321
|
+
# auth_tenant_id、auth_company_id、doc_id 可不传,使用实例属性
|
|
322
|
+
)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### 下载单据文件
|
|
326
|
+
```python
|
|
327
|
+
data = S3_Client.download_doc_file(
|
|
328
|
+
filename="发票.pdf"
|
|
329
|
+
# auth_tenant_id、auth_company_id、doc_id 可不传,使用实例属性
|
|
330
|
+
)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### 列出单据下所有文件
|
|
334
|
+
```python
|
|
335
|
+
files = S3_Client.list_doc_files()
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
- 存储路径自动为:`bucketname/auth_tenant_id/auth_company_id/doc_id/filename`
|
|
339
|
+
- tags会自动打到对象存储(Alicloud用OSS Tagging,Minio用metadata)
|
|
340
|
+
- 支持实例初始化时设置默认auth_tenant_id、auth_company_id、doc_id,调用时可覆盖
|
|
341
|
+
- 缺失必要参数会抛出异常
|
|
342
|
+
|
|
289
343
|
## 配置参数
|
|
290
344
|
|
|
291
345
|
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
@@ -61,6 +61,60 @@ S3_Client.delete_file("test.txt")
|
|
|
61
61
|
files = S3_Client.list_files(prefix="test/")
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### 单据文件管理(推荐业务用法)
|
|
65
|
+
|
|
66
|
+
支持业务身份(auth_tenant_id, auth_company_id, doc_id)在实例初始化时注入(可为空),各操作方法参数可选,若不传则使用实例属性,若传则覆盖。
|
|
67
|
+
|
|
68
|
+
#### 初始化带业务身份
|
|
69
|
+
```python
|
|
70
|
+
from fiuai_s3 import ObjectStorageFactory
|
|
71
|
+
from fiuai_s3.object_storage import StorageConfig
|
|
72
|
+
|
|
73
|
+
config = StorageConfig(
|
|
74
|
+
provider="minio",
|
|
75
|
+
bucket_name="dev",
|
|
76
|
+
endpoint="http://127.0.0.1:19000",
|
|
77
|
+
access_key="devdevdev",
|
|
78
|
+
secret_key="devdevdev"
|
|
79
|
+
)
|
|
80
|
+
# 业务身份可选
|
|
81
|
+
S3_Client = ObjectStorageFactory.create_storage(
|
|
82
|
+
config,
|
|
83
|
+
auth_tenant_id="t1",
|
|
84
|
+
auth_company_id="c1",
|
|
85
|
+
doc_id="d1"
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### 上传单据文件
|
|
90
|
+
```python
|
|
91
|
+
# 方法参数可选,优先级高于实例属性
|
|
92
|
+
S3_Client.upload_doc_file(
|
|
93
|
+
filename="发票.pdf",
|
|
94
|
+
data=b"...文件内容...",
|
|
95
|
+
tags={"业务类型": "发票", "年份": "2024"}
|
|
96
|
+
# auth_tenant_id、auth_company_id、doc_id 可不传,使用实例属性
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### 下载单据文件
|
|
101
|
+
```python
|
|
102
|
+
data = S3_Client.download_doc_file(
|
|
103
|
+
filename="发票.pdf"
|
|
104
|
+
# auth_tenant_id、auth_company_id、doc_id 可不传,使用实例属性
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### 列出单据下所有文件
|
|
109
|
+
```python
|
|
110
|
+
files = S3_Client.list_doc_files()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- 存储路径自动为:`bucketname/auth_tenant_id/auth_company_id/doc_id/filename`
|
|
114
|
+
- tags会自动打到对象存储(Alicloud用OSS Tagging,Minio用metadata)
|
|
115
|
+
- 支持实例初始化时设置默认auth_tenant_id、auth_company_id、doc_id,调用时可覆盖
|
|
116
|
+
- 缺失必要参数会抛出异常
|
|
117
|
+
|
|
64
118
|
## 配置参数
|
|
65
119
|
|
|
66
120
|
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# -- coding: utf-8 --
|
|
2
|
+
# Project: object_storage
|
|
3
|
+
# Created Date: 2025-05-01
|
|
4
|
+
# Author: liming
|
|
5
|
+
# Email: lmlala@aliyun.com
|
|
6
|
+
# Copyright (c) 2025 FiuAI
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import logging
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
import oss2
|
|
12
|
+
from ..object_storage import ObjectStorage, StorageConfig
|
|
13
|
+
from oss2.headers import OSS_OBJECT_TAGGING
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# 设置oss2的日志级别为WARNING,关闭INFO级别的日志
|
|
18
|
+
oss2.set_stream_logger(level=logging.WARNING)
|
|
19
|
+
|
|
20
|
+
class AliCloudStorage(ObjectStorage):
|
|
21
|
+
"""阿里云OSS存储实现"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, config: StorageConfig, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None):
|
|
24
|
+
"""初始化阿里云OSS客户端
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
config: 存储配置对象
|
|
28
|
+
auth_tenant_id: 业务租户ID(可为空,后续操作可覆盖)
|
|
29
|
+
auth_company_id: 业务公司ID(可为空,后续操作可覆盖)
|
|
30
|
+
doc_id: 单据ID(可为空,后续操作可覆盖)
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(config, auth_tenant_id, auth_company_id, doc_id)
|
|
33
|
+
self.auth = oss2.Auth(config.access_key, config.secret_key)
|
|
34
|
+
self.bucket = oss2.Bucket(
|
|
35
|
+
auth=self.auth,
|
|
36
|
+
endpoint=config.endpoint,
|
|
37
|
+
bucket_name=config.bucket_name
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def upload_file(self, object_key: str, data: bytes) -> bool:
|
|
41
|
+
"""上传文件到阿里云OSS
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
object_key: 对象存储中的key
|
|
45
|
+
data: 文件数据
|
|
46
|
+
Returns:
|
|
47
|
+
bool: 是否上传成功
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
self.bucket.put_object(object_key, data)
|
|
51
|
+
logger.info(f"文件上传成功: {object_key}")
|
|
52
|
+
return True
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"文件上传失败: {str(e)}")
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
def download_file(self, object_key: str) -> bytes:
|
|
58
|
+
"""从阿里云OSS下载文件
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
object_key: 对象存储中的key
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
bytes: 文件内容
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
return self.bucket.get_object(object_key).read()
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"文件下载失败: {str(e)}")
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def delete_file(self, object_key: str) -> bool:
|
|
73
|
+
"""删除阿里云OSS中的文件
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
object_key: 对象存储中的key
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
bool: 是否删除成功
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
self.bucket.delete_object(object_key)
|
|
83
|
+
logger.info(f"文件删除成功: {object_key}")
|
|
84
|
+
return True
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"文件删除失败: {str(e)}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def list_files(self, prefix: Optional[str] = None) -> List[str]:
|
|
90
|
+
"""列出阿里云OSS中的文件
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
prefix: 文件前缀过滤
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List[str]: 文件key列表
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
files = []
|
|
100
|
+
for obj in oss2.ObjectIterator(self.bucket, prefix=prefix):
|
|
101
|
+
files.append(obj.key)
|
|
102
|
+
return files
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"列出文件失败: {str(e)}")
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
def _build_doc_path(self, filename: str, auth_tenant_id: Optional[str], auth_company_id: Optional[str], doc_id: Optional[str]) -> str:
|
|
108
|
+
tenant_id = auth_tenant_id or self.auth_tenant_id
|
|
109
|
+
company_id = auth_company_id or self.auth_company_id
|
|
110
|
+
docid = doc_id or self.doc_id
|
|
111
|
+
if not (tenant_id and company_id and docid):
|
|
112
|
+
raise ValueError("auth_tenant_id、auth_company_id、doc_id 不能为空")
|
|
113
|
+
return f"{tenant_id}/{company_id}/{docid}/{filename}"
|
|
114
|
+
|
|
115
|
+
def upload_doc_file(self, filename: str, data: bytes, tags: Optional[dict] = None, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> bool:
|
|
116
|
+
try:
|
|
117
|
+
object_key = self._build_doc_path(filename, auth_tenant_id, auth_company_id, doc_id)
|
|
118
|
+
headers = None
|
|
119
|
+
if tags:
|
|
120
|
+
# 构造tagging字符串
|
|
121
|
+
tagging = "&".join([f"{oss2.urlquote(str(k))}={oss2.urlquote(str(v))}" for k, v in tags.items()])
|
|
122
|
+
headers = {OSS_OBJECT_TAGGING: tagging}
|
|
123
|
+
self.bucket.put_object(object_key, data, headers=headers)
|
|
124
|
+
logger.info(f"单据文件上传成功: {object_key}")
|
|
125
|
+
return True
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"单据文件上传失败: {str(e)}")
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
def download_doc_file(self, filename: str, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> bytes:
|
|
131
|
+
try:
|
|
132
|
+
object_key = self._build_doc_path(filename, auth_tenant_id, auth_company_id, doc_id)
|
|
133
|
+
return self.bucket.get_object(object_key).read()
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.error(f"单据文件下载失败: {str(e)}")
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def list_doc_files(self, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> list:
|
|
139
|
+
try:
|
|
140
|
+
tenant_id = auth_tenant_id or self.auth_tenant_id
|
|
141
|
+
company_id = auth_company_id or self.auth_company_id
|
|
142
|
+
docid = doc_id or self.doc_id
|
|
143
|
+
if not (tenant_id and company_id and docid):
|
|
144
|
+
raise ValueError("auth_tenant_id、auth_company_id、doc_id 不能为空")
|
|
145
|
+
prefix = f"{tenant_id}/{company_id}/{docid}/"
|
|
146
|
+
files = []
|
|
147
|
+
for obj in oss2.ObjectIterator(self.bucket, prefix=prefix):
|
|
148
|
+
files.append(obj.key.split(prefix, 1)[-1])
|
|
149
|
+
return files
|
|
150
|
+
except Exception as e:
|
|
151
|
+
logger.error(f"列出单据文件失败: {str(e)}")
|
|
152
|
+
return []
|
|
@@ -17,13 +17,16 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
class MinioStorage(ObjectStorage):
|
|
18
18
|
"""MinIO存储实现"""
|
|
19
19
|
|
|
20
|
-
def __init__(self, config: StorageConfig):
|
|
20
|
+
def __init__(self, config: StorageConfig, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None):
|
|
21
21
|
"""初始化MinIO客户端
|
|
22
22
|
|
|
23
23
|
Args:
|
|
24
24
|
config: 存储配置对象
|
|
25
|
+
auth_tenant_id: 业务租户ID(可为空,后续操作可覆盖)
|
|
26
|
+
auth_company_id: 业务公司ID(可为空,后续操作可覆盖)
|
|
27
|
+
doc_id: 单据ID(可为空,后续操作可覆盖)
|
|
25
28
|
"""
|
|
26
|
-
super().__init__(config)
|
|
29
|
+
super().__init__(config, auth_tenant_id, auth_company_id, doc_id)
|
|
27
30
|
|
|
28
31
|
# 处理endpoint格式
|
|
29
32
|
endpoint = self._format_endpoint(config.endpoint)
|
|
@@ -148,4 +151,64 @@ class MinioStorage(ObjectStorage):
|
|
|
148
151
|
return files
|
|
149
152
|
except S3Error as e:
|
|
150
153
|
logger.error(f"列出文件失败: {str(e)}")
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
def _build_doc_path(self, filename: str, auth_tenant_id: Optional[str], auth_company_id: Optional[str], doc_id: Optional[str]) -> str:
|
|
157
|
+
tenant_id = auth_tenant_id or self.auth_tenant_id
|
|
158
|
+
company_id = auth_company_id or self.auth_company_id
|
|
159
|
+
docid = doc_id or self.doc_id
|
|
160
|
+
if not (tenant_id and company_id and docid):
|
|
161
|
+
raise ValueError("auth_tenant_id、auth_company_id、doc_id 不能为空")
|
|
162
|
+
return f"{tenant_id}/{company_id}/{docid}/{filename}"
|
|
163
|
+
|
|
164
|
+
def upload_doc_file(self, filename: str, data: bytes, tags: Optional[dict] = None, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> bool:
|
|
165
|
+
try:
|
|
166
|
+
object_key = self._build_doc_path(filename, auth_tenant_id, auth_company_id, doc_id)
|
|
167
|
+
extra_headers = None
|
|
168
|
+
extra_tags = None
|
|
169
|
+
if tags:
|
|
170
|
+
extra_tags = "&".join([f"{k}={v}" for k, v in tags.items()])
|
|
171
|
+
self.client.put_object(
|
|
172
|
+
bucket_name=self.bucket_name,
|
|
173
|
+
object_name=object_key,
|
|
174
|
+
data=BytesIO(data),
|
|
175
|
+
length=len(data),
|
|
176
|
+
metadata=tags if tags else None
|
|
177
|
+
)
|
|
178
|
+
logger.info(f"单据文件上传成功: {object_key}")
|
|
179
|
+
return True
|
|
180
|
+
except S3Error as e:
|
|
181
|
+
logger.error(f"单据文件上传失败: {str(e)}")
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def download_doc_file(self, filename: str, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> bytes:
|
|
185
|
+
try:
|
|
186
|
+
object_key = self._build_doc_path(filename, auth_tenant_id, auth_company_id, doc_id)
|
|
187
|
+
response = self.client.get_object(
|
|
188
|
+
bucket_name=self.bucket_name,
|
|
189
|
+
object_name=object_key
|
|
190
|
+
)
|
|
191
|
+
return response.read()
|
|
192
|
+
except S3Error as e:
|
|
193
|
+
logger.error(f"单据文件下载失败: {str(e)}")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def list_doc_files(self, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> list:
|
|
197
|
+
try:
|
|
198
|
+
tenant_id = auth_tenant_id or self.auth_tenant_id
|
|
199
|
+
company_id = auth_company_id or self.auth_company_id
|
|
200
|
+
docid = doc_id or self.doc_id
|
|
201
|
+
if not (tenant_id and company_id and docid):
|
|
202
|
+
raise ValueError("auth_tenant_id、auth_company_id、doc_id 不能为空")
|
|
203
|
+
prefix = f"{tenant_id}/{company_id}/{docid}/"
|
|
204
|
+
files = []
|
|
205
|
+
objects = self.client.list_objects(
|
|
206
|
+
bucket_name=self.bucket_name,
|
|
207
|
+
prefix=prefix
|
|
208
|
+
)
|
|
209
|
+
for obj in objects:
|
|
210
|
+
files.append(obj.object_name.split(prefix, 1)[-1])
|
|
211
|
+
return files
|
|
212
|
+
except S3Error as e:
|
|
213
|
+
logger.error(f"列出单据文件失败: {str(e)}")
|
|
151
214
|
return []
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from typing import Optional, List, Dict, Any
|
|
10
|
-
from pydantic import BaseModel, Field
|
|
10
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
11
11
|
import logging
|
|
12
12
|
from uuid import uuid4
|
|
13
13
|
|
|
@@ -23,15 +23,28 @@ class StorageConfig(BaseModel):
|
|
|
23
23
|
temp_dir: str = Field("temp/", description="临时目录")
|
|
24
24
|
use_https: bool = Field(False, description="是否使用HTTPS")
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
arbitrary_types_allowed = True
|
|
26
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
28
27
|
|
|
29
28
|
class ObjectStorage(ABC):
|
|
30
|
-
"""对象存储抽象基类
|
|
29
|
+
"""对象存储抽象基类
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
支持业务身份(auth_tenant_id, auth_company_id, doc_id)在实例初始化时注入(可为空),
|
|
32
|
+
各操作方法参数可选,若不传则使用实例属性,若传则覆盖。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, config: StorageConfig, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None):
|
|
36
|
+
"""
|
|
37
|
+
Args:
|
|
38
|
+
config: 存储配置对象
|
|
39
|
+
auth_tenant_id: 业务租户ID(可为空,后续操作可覆盖)
|
|
40
|
+
auth_company_id: 业务公司ID(可为空,后续操作可覆盖)
|
|
41
|
+
doc_id: 单据ID(可为空,后续操作可覆盖)
|
|
42
|
+
"""
|
|
33
43
|
self.config = config
|
|
34
44
|
self._id = str(uuid4())
|
|
45
|
+
self.auth_tenant_id = auth_tenant_id
|
|
46
|
+
self.auth_company_id = auth_company_id
|
|
47
|
+
self.doc_id = doc_id
|
|
35
48
|
|
|
36
49
|
@abstractmethod
|
|
37
50
|
def upload_file(self, object_key: str, data: bytes) -> bool:
|
|
@@ -82,6 +95,49 @@ class ObjectStorage(ABC):
|
|
|
82
95
|
"""
|
|
83
96
|
pass
|
|
84
97
|
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def upload_doc_file(self, filename: str, data: bytes, tags: Optional[Dict[str, str]] = None, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
上传单据文件,自动拼接存储路径并打tag
|
|
102
|
+
Args:
|
|
103
|
+
filename: 文件名
|
|
104
|
+
data: 文件内容
|
|
105
|
+
tags: 标签字典
|
|
106
|
+
auth_tenant_id: 租户ID(可选,若不传则用实例属性)
|
|
107
|
+
auth_company_id: 公司ID(可选,若不传则用实例属性)
|
|
108
|
+
doc_id: 单据ID(可选,若不传则用实例属性)
|
|
109
|
+
Returns:
|
|
110
|
+
bool: 是否上传成功
|
|
111
|
+
"""
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def download_doc_file(self, filename: str, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> bytes:
|
|
116
|
+
"""
|
|
117
|
+
下载单据文件,自动拼接存储路径
|
|
118
|
+
Args:
|
|
119
|
+
filename: 文件名
|
|
120
|
+
auth_tenant_id: 租户ID(可选,若不传则用实例属性)
|
|
121
|
+
auth_company_id: 公司ID(可选,若不传则用实例属性)
|
|
122
|
+
doc_id: 单据ID(可选,若不传则用实例属性)
|
|
123
|
+
Returns:
|
|
124
|
+
bytes: 文件内容
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def list_doc_files(self, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> List[str]:
|
|
130
|
+
"""
|
|
131
|
+
列出单据下所有文件
|
|
132
|
+
Args:
|
|
133
|
+
auth_tenant_id: 租户ID(可选,若不传则用实例属性)
|
|
134
|
+
auth_company_id: 公司ID(可选,若不传则用实例属性)
|
|
135
|
+
doc_id: 单据ID(可选,若不传则用实例属性)
|
|
136
|
+
Returns:
|
|
137
|
+
List[str]: 文件名列表
|
|
138
|
+
"""
|
|
139
|
+
pass
|
|
140
|
+
|
|
85
141
|
class ObjectStorageFactory:
|
|
86
142
|
"""对象存储工厂类"""
|
|
87
143
|
|
|
@@ -149,26 +205,27 @@ class ObjectStorageFactory:
|
|
|
149
205
|
return cls._instance
|
|
150
206
|
|
|
151
207
|
@classmethod
|
|
152
|
-
def create_storage(cls, config: StorageConfig) -> ObjectStorage:
|
|
208
|
+
def create_storage(cls, config: StorageConfig, auth_tenant_id: Optional[str] = None, auth_company_id: Optional[str] = None, doc_id: Optional[str] = None) -> ObjectStorage:
|
|
153
209
|
"""创建新的对象存储实例
|
|
154
210
|
|
|
155
211
|
Args:
|
|
156
212
|
config: 存储配置对象
|
|
157
|
-
|
|
213
|
+
auth_tenant_id: 业务租户ID(可为空,后续操作可覆盖)
|
|
214
|
+
auth_company_id: 业务公司ID(可为空,后续操作可覆盖)
|
|
215
|
+
doc_id: 单据ID(可为空,后续操作可覆盖)
|
|
158
216
|
Returns:
|
|
159
217
|
ObjectStorage: 对象存储实例
|
|
160
|
-
|
|
161
218
|
Raises:
|
|
162
219
|
ValueError: 不支持的存储提供商
|
|
163
220
|
"""
|
|
164
221
|
provider = config.provider.lower()
|
|
165
|
-
|
|
222
|
+
|
|
166
223
|
if provider == "alicloud":
|
|
167
224
|
from .alicloud.alicloud_storage import AliCloudStorage
|
|
168
|
-
return AliCloudStorage(config)
|
|
225
|
+
return AliCloudStorage(config, auth_tenant_id, auth_company_id, doc_id)
|
|
169
226
|
elif provider == "minio":
|
|
170
227
|
from .minio.minio_storage import MinioStorage
|
|
171
|
-
return MinioStorage(config)
|
|
228
|
+
return MinioStorage(config, auth_tenant_id, auth_company_id, doc_id)
|
|
172
229
|
else:
|
|
173
230
|
raise ValueError(f"不支持的存储提供商: {provider}")
|
|
174
231
|
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# -- coding: utf-8 --
|
|
2
|
-
# Project: object_storage
|
|
3
|
-
# Created Date: 2025-05-01
|
|
4
|
-
# Author: liming
|
|
5
|
-
# Email: lmlala@aliyun.com
|
|
6
|
-
# Copyright (c) 2025 FiuAI
|
|
7
|
-
|
|
8
|
-
import os
|
|
9
|
-
import logging
|
|
10
|
-
from typing import List, Optional
|
|
11
|
-
import oss2
|
|
12
|
-
from ..object_storage import ObjectStorage, StorageConfig
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
# 设置oss2的日志级别为WARNING,关闭INFO级别的日志
|
|
17
|
-
oss2.set_stream_logger(level=logging.WARNING)
|
|
18
|
-
|
|
19
|
-
class AliCloudStorage(ObjectStorage):
|
|
20
|
-
"""阿里云OSS存储实现"""
|
|
21
|
-
|
|
22
|
-
def __init__(self, config: StorageConfig):
|
|
23
|
-
"""初始化阿里云OSS客户端
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
config: 存储配置对象
|
|
27
|
-
"""
|
|
28
|
-
super().__init__(config)
|
|
29
|
-
self.auth = oss2.Auth(config.access_key, config.secret_key)
|
|
30
|
-
self.bucket = oss2.Bucket(
|
|
31
|
-
auth=self.auth,
|
|
32
|
-
endpoint=config.endpoint,
|
|
33
|
-
bucket_name=config.bucket_name
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
def upload_file(self, object_key: str, data: bytes) -> bool:
|
|
37
|
-
"""上传文件到阿里云OSS
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
object_key: 对象存储中的key
|
|
41
|
-
data: 文件数据
|
|
42
|
-
Returns:
|
|
43
|
-
bool: 是否上传成功
|
|
44
|
-
"""
|
|
45
|
-
try:
|
|
46
|
-
self.bucket.put_object(object_key, data)
|
|
47
|
-
logger.info(f"文件上传成功: {object_key}")
|
|
48
|
-
return True
|
|
49
|
-
except Exception as e:
|
|
50
|
-
logger.error(f"文件上传失败: {str(e)}")
|
|
51
|
-
return False
|
|
52
|
-
|
|
53
|
-
def download_file(self, object_key: str) -> bytes:
|
|
54
|
-
"""从阿里云OSS下载文件
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
object_key: 对象存储中的key
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
bytes: 文件内容
|
|
61
|
-
"""
|
|
62
|
-
try:
|
|
63
|
-
return self.bucket.get_object(object_key).read()
|
|
64
|
-
except Exception as e:
|
|
65
|
-
logger.error(f"文件下载失败: {str(e)}")
|
|
66
|
-
return None
|
|
67
|
-
|
|
68
|
-
def delete_file(self, object_key: str) -> bool:
|
|
69
|
-
"""删除阿里云OSS中的文件
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
object_key: 对象存储中的key
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
bool: 是否删除成功
|
|
76
|
-
"""
|
|
77
|
-
try:
|
|
78
|
-
self.bucket.delete_object(object_key)
|
|
79
|
-
logger.info(f"文件删除成功: {object_key}")
|
|
80
|
-
return True
|
|
81
|
-
except Exception as e:
|
|
82
|
-
logger.error(f"文件删除失败: {str(e)}")
|
|
83
|
-
return False
|
|
84
|
-
|
|
85
|
-
def list_files(self, prefix: Optional[str] = None) -> List[str]:
|
|
86
|
-
"""列出阿里云OSS中的文件
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
prefix: 文件前缀过滤
|
|
90
|
-
|
|
91
|
-
Returns:
|
|
92
|
-
List[str]: 文件key列表
|
|
93
|
-
"""
|
|
94
|
-
try:
|
|
95
|
-
files = []
|
|
96
|
-
for obj in oss2.ObjectIterator(self.bucket, prefix=prefix):
|
|
97
|
-
files.append(obj.key)
|
|
98
|
-
return files
|
|
99
|
-
except Exception as e:
|
|
100
|
-
logger.error(f"列出文件失败: {str(e)}")
|
|
101
|
-
return []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|