access-control-async 0.2.5__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.
- access_control_async-0.2.5/LICENSE +21 -0
- access_control_async-0.2.5/PKG-INFO +181 -0
- access_control_async-0.2.5/README.md +162 -0
- access_control_async-0.2.5/pyproject.toml +29 -0
- access_control_async-0.2.5/setup.cfg +4 -0
- access_control_async-0.2.5/src/access_control/__init__.py +12 -0
- access_control_async-0.2.5/src/access_control/exceptions.py +18 -0
- access_control_async-0.2.5/src/access_control/file_backed_resource_manager.py +195 -0
- access_control_async-0.2.5/src/access_control/resource_manager.py +540 -0
- access_control_async-0.2.5/src/access_control/utils.py +97 -0
- access_control_async-0.2.5/src/access_control_async.egg-info/PKG-INFO +181 -0
- access_control_async-0.2.5/src/access_control_async.egg-info/SOURCES.txt +15 -0
- access_control_async-0.2.5/src/access_control_async.egg-info/dependency_links.txt +1 -0
- access_control_async-0.2.5/src/access_control_async.egg-info/requires.txt +3 -0
- access_control_async-0.2.5/src/access_control_async.egg-info/top_level.txt +1 -0
- access_control_async-0.2.5/test/test_file_backed_resource_manager.py +62 -0
- access_control_async-0.2.5/test/test_resource_manager.py +54 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jerry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: access-control-async
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: A brief description of your access control library
|
|
5
|
+
Author-email: Jerry <wujr24@m.fudan.edu.cn>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Jerry-Wu-GitHub/access-control
|
|
8
|
+
Project-URL: Repository, https://github.com/Jerry-Wu-GitHub/access-control.git
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: treelib
|
|
16
|
+
Requires-Dist: aiofiles
|
|
17
|
+
Requires-Dist: pathvalidate
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Access Control
|
|
21
|
+
|
|
22
|
+
一个轻量级的 Python 资源访问控制库,支持基于控制码(Control Code)和访问码(Access Code)的细粒度权限管理,并提供可选的磁盘持久化能力。
|
|
23
|
+
|
|
24
|
+
## 特性
|
|
25
|
+
|
|
26
|
+
- **两级授权模型**:每个资源拥有唯一的控制码(所有者权限)和多个访问码(分享权限)
|
|
27
|
+
- **树形访问结构**:访问码可继续衍生子访问码,形成层级分享关系
|
|
28
|
+
- **同步与异步双接口**:所有方法均提供 `*_async` 异步版本和同步版本
|
|
29
|
+
- **可插拔的码生成器**:支持自定义控制码/访问码生成逻辑
|
|
30
|
+
- **磁盘持久化**:`FileBackedResourceManager` 可将资源序列化保存到文件系统
|
|
31
|
+
- **Pickle 友好**:管理器对象可序列化(需保证内部对象可 Pickle)
|
|
32
|
+
|
|
33
|
+
## 安装
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install access-control-async
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
或者直接从源码安装:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone https://github.com/Jerry-Wu-GitHub/access-control.git
|
|
43
|
+
cd access-control
|
|
44
|
+
pip install .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 快速开始
|
|
48
|
+
|
|
49
|
+
### 基本用法(内存存储)
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from access_control import ResourceManager
|
|
53
|
+
|
|
54
|
+
# 创建管理器(控制码和访问码默认使用 uuid4)
|
|
55
|
+
manager = ResourceManager()
|
|
56
|
+
|
|
57
|
+
# 创建一个资源
|
|
58
|
+
resource = {"data": "secret message"}
|
|
59
|
+
control_code = manager.create(resource) # 返回控制码,如 "3a6f2b1c..."
|
|
60
|
+
|
|
61
|
+
# 基于控制码分享访问码
|
|
62
|
+
access_code1 = manager.share(control_code)
|
|
63
|
+
access_code2 = manager.share(control_code)
|
|
64
|
+
|
|
65
|
+
# 从访问码获取资源
|
|
66
|
+
retrieved = manager.get(access_code1)
|
|
67
|
+
assert retrieved == resource
|
|
68
|
+
|
|
69
|
+
# 查看某个码的所有后代访问码
|
|
70
|
+
codes = manager.get_access_codes(control_code) # [access_code1, access_code2]
|
|
71
|
+
|
|
72
|
+
# 撤销一个访问码及其所有子访问码
|
|
73
|
+
manager.revoke(control_code, access_code1)
|
|
74
|
+
|
|
75
|
+
# 删除整个资源(包括所有访问码)
|
|
76
|
+
manager.delete(control_code)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 异步接口
|
|
80
|
+
|
|
81
|
+
所有操作都提供 `*_async` 异步版本,便于集成到异步应用中:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
from access_control import ResourceManager
|
|
86
|
+
|
|
87
|
+
async def main():
|
|
88
|
+
manager = ResourceManager()
|
|
89
|
+
control_code = await manager.create_async({"data": "async test"})
|
|
90
|
+
access_code = await manager.share_async(control_code)
|
|
91
|
+
resource = await manager.get_async(access_code)
|
|
92
|
+
print(resource)
|
|
93
|
+
|
|
94
|
+
asyncio.run(main())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 使用文件持久化
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import asyncio
|
|
101
|
+
from access_control import FileBackedResourceManager
|
|
102
|
+
|
|
103
|
+
async def main():
|
|
104
|
+
# 资源将保存在 ./data 目录下
|
|
105
|
+
manager = FileBackedResourceManager("./data")
|
|
106
|
+
|
|
107
|
+
# 创建资源(自动序列化到磁盘)
|
|
108
|
+
resource = {"user": "alice", "score": 100}
|
|
109
|
+
control_code = await manager.create_async(resource)
|
|
110
|
+
|
|
111
|
+
# 即使重启程序,只要 data 目录存在,即可恢复
|
|
112
|
+
new_manager = FileBackedResourceManager("./data")
|
|
113
|
+
same_resource = await new_manager.get_async(control_code)
|
|
114
|
+
print(same_resource)
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 自定义码生成器
|
|
120
|
+
|
|
121
|
+
你可以传入自己的生成函数(同步或异步均可):
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import hashlib
|
|
125
|
+
from access_control import ResourceManager
|
|
126
|
+
|
|
127
|
+
def my_control_code_gen(resource):
|
|
128
|
+
# 根据资源内容生成控制码
|
|
129
|
+
return hashlib.md5(str(resource).encode()).hexdigest()[:8]
|
|
130
|
+
|
|
131
|
+
def my_access_code_gen(resource, control_code, parent_code):
|
|
132
|
+
# 自定义访问码生成逻辑
|
|
133
|
+
return f"share_{control_code}_{parent_code}"
|
|
134
|
+
|
|
135
|
+
manager = ResourceManager(
|
|
136
|
+
control_code_gen=my_control_code_gen,
|
|
137
|
+
access_code_gen=my_access_code_gen
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
control = manager.create({"foo": "bar"})
|
|
141
|
+
print(control) # 输出类似 "d3b07384"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## API 概览
|
|
145
|
+
|
|
146
|
+
### `ResourceManager[Resource, ControlCode, AccessCode]`
|
|
147
|
+
|
|
148
|
+
主要方法(均提供同步版本和 `*_async` 异步版本):
|
|
149
|
+
|
|
150
|
+
| 方法 | 描述 |
|
|
151
|
+
|------|------|
|
|
152
|
+
| `create(resource, control_code=None)` | 创建资源,返回控制码 |
|
|
153
|
+
| `get(code)` | 通过控制码或访问码获取资源 |
|
|
154
|
+
| `share(parent_code, child_code=None)` | 生成新的访问码 |
|
|
155
|
+
| `revoke(ancestor_code, descendant_code)` | 撤销一个访问码及其所有子访问码 |
|
|
156
|
+
| `get_access_codes(code)` | 返回某个码的所有后代访问码 |
|
|
157
|
+
| `replace(control_code, new_resource)` | 替换控制码对应的资源 |
|
|
158
|
+
| `transfer(old_control, new_control)` | 将资源所有权转移给另一个控制码 |
|
|
159
|
+
| `delete(control_code)` | 删除资源及其所有关联码 |
|
|
160
|
+
| `is_control_code(code)` / `is_access_code(code)` | 判断码的类型 |
|
|
161
|
+
|
|
162
|
+
### `FileBackedResourceManager`
|
|
163
|
+
|
|
164
|
+
继承自 `ResourceManager`,额外接受以下构造参数:
|
|
165
|
+
|
|
166
|
+
- `data_dir_path`: 存储文件的目录路径
|
|
167
|
+
- `dumps_function` / `loads_function`: 自定义序列化函数(默认 `pickle.dumps` / `pickle.loads`)
|
|
168
|
+
- `file_name_gen`: 根据资源和控制码生成文件名的函数(默认使用 `uuid4`)
|
|
169
|
+
|
|
170
|
+
所有资源的存取均自动读写磁盘文件。
|
|
171
|
+
|
|
172
|
+
## 依赖
|
|
173
|
+
|
|
174
|
+
- Python >= 3.10
|
|
175
|
+
- `treelib` – 用于维护访问码树结构
|
|
176
|
+
- `aiofiles` – 异步文件操作
|
|
177
|
+
- `pathvalidate` – 校验文件名的合法性
|
|
178
|
+
|
|
179
|
+
## 许可证
|
|
180
|
+
|
|
181
|
+
本项目使用 [MIT 许可证](LICENSE)。
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Access Control
|
|
2
|
+
|
|
3
|
+
一个轻量级的 Python 资源访问控制库,支持基于控制码(Control Code)和访问码(Access Code)的细粒度权限管理,并提供可选的磁盘持久化能力。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **两级授权模型**:每个资源拥有唯一的控制码(所有者权限)和多个访问码(分享权限)
|
|
8
|
+
- **树形访问结构**:访问码可继续衍生子访问码,形成层级分享关系
|
|
9
|
+
- **同步与异步双接口**:所有方法均提供 `*_async` 异步版本和同步版本
|
|
10
|
+
- **可插拔的码生成器**:支持自定义控制码/访问码生成逻辑
|
|
11
|
+
- **磁盘持久化**:`FileBackedResourceManager` 可将资源序列化保存到文件系统
|
|
12
|
+
- **Pickle 友好**:管理器对象可序列化(需保证内部对象可 Pickle)
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install access-control-async
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
或者直接从源码安装:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/Jerry-Wu-GitHub/access-control.git
|
|
24
|
+
cd access-control
|
|
25
|
+
pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 快速开始
|
|
29
|
+
|
|
30
|
+
### 基本用法(内存存储)
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from access_control import ResourceManager
|
|
34
|
+
|
|
35
|
+
# 创建管理器(控制码和访问码默认使用 uuid4)
|
|
36
|
+
manager = ResourceManager()
|
|
37
|
+
|
|
38
|
+
# 创建一个资源
|
|
39
|
+
resource = {"data": "secret message"}
|
|
40
|
+
control_code = manager.create(resource) # 返回控制码,如 "3a6f2b1c..."
|
|
41
|
+
|
|
42
|
+
# 基于控制码分享访问码
|
|
43
|
+
access_code1 = manager.share(control_code)
|
|
44
|
+
access_code2 = manager.share(control_code)
|
|
45
|
+
|
|
46
|
+
# 从访问码获取资源
|
|
47
|
+
retrieved = manager.get(access_code1)
|
|
48
|
+
assert retrieved == resource
|
|
49
|
+
|
|
50
|
+
# 查看某个码的所有后代访问码
|
|
51
|
+
codes = manager.get_access_codes(control_code) # [access_code1, access_code2]
|
|
52
|
+
|
|
53
|
+
# 撤销一个访问码及其所有子访问码
|
|
54
|
+
manager.revoke(control_code, access_code1)
|
|
55
|
+
|
|
56
|
+
# 删除整个资源(包括所有访问码)
|
|
57
|
+
manager.delete(control_code)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 异步接口
|
|
61
|
+
|
|
62
|
+
所有操作都提供 `*_async` 异步版本,便于集成到异步应用中:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
from access_control import ResourceManager
|
|
67
|
+
|
|
68
|
+
async def main():
|
|
69
|
+
manager = ResourceManager()
|
|
70
|
+
control_code = await manager.create_async({"data": "async test"})
|
|
71
|
+
access_code = await manager.share_async(control_code)
|
|
72
|
+
resource = await manager.get_async(access_code)
|
|
73
|
+
print(resource)
|
|
74
|
+
|
|
75
|
+
asyncio.run(main())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 使用文件持久化
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import asyncio
|
|
82
|
+
from access_control import FileBackedResourceManager
|
|
83
|
+
|
|
84
|
+
async def main():
|
|
85
|
+
# 资源将保存在 ./data 目录下
|
|
86
|
+
manager = FileBackedResourceManager("./data")
|
|
87
|
+
|
|
88
|
+
# 创建资源(自动序列化到磁盘)
|
|
89
|
+
resource = {"user": "alice", "score": 100}
|
|
90
|
+
control_code = await manager.create_async(resource)
|
|
91
|
+
|
|
92
|
+
# 即使重启程序,只要 data 目录存在,即可恢复
|
|
93
|
+
new_manager = FileBackedResourceManager("./data")
|
|
94
|
+
same_resource = await new_manager.get_async(control_code)
|
|
95
|
+
print(same_resource)
|
|
96
|
+
|
|
97
|
+
asyncio.run(main())
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 自定义码生成器
|
|
101
|
+
|
|
102
|
+
你可以传入自己的生成函数(同步或异步均可):
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import hashlib
|
|
106
|
+
from access_control import ResourceManager
|
|
107
|
+
|
|
108
|
+
def my_control_code_gen(resource):
|
|
109
|
+
# 根据资源内容生成控制码
|
|
110
|
+
return hashlib.md5(str(resource).encode()).hexdigest()[:8]
|
|
111
|
+
|
|
112
|
+
def my_access_code_gen(resource, control_code, parent_code):
|
|
113
|
+
# 自定义访问码生成逻辑
|
|
114
|
+
return f"share_{control_code}_{parent_code}"
|
|
115
|
+
|
|
116
|
+
manager = ResourceManager(
|
|
117
|
+
control_code_gen=my_control_code_gen,
|
|
118
|
+
access_code_gen=my_access_code_gen
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
control = manager.create({"foo": "bar"})
|
|
122
|
+
print(control) # 输出类似 "d3b07384"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## API 概览
|
|
126
|
+
|
|
127
|
+
### `ResourceManager[Resource, ControlCode, AccessCode]`
|
|
128
|
+
|
|
129
|
+
主要方法(均提供同步版本和 `*_async` 异步版本):
|
|
130
|
+
|
|
131
|
+
| 方法 | 描述 |
|
|
132
|
+
|------|------|
|
|
133
|
+
| `create(resource, control_code=None)` | 创建资源,返回控制码 |
|
|
134
|
+
| `get(code)` | 通过控制码或访问码获取资源 |
|
|
135
|
+
| `share(parent_code, child_code=None)` | 生成新的访问码 |
|
|
136
|
+
| `revoke(ancestor_code, descendant_code)` | 撤销一个访问码及其所有子访问码 |
|
|
137
|
+
| `get_access_codes(code)` | 返回某个码的所有后代访问码 |
|
|
138
|
+
| `replace(control_code, new_resource)` | 替换控制码对应的资源 |
|
|
139
|
+
| `transfer(old_control, new_control)` | 将资源所有权转移给另一个控制码 |
|
|
140
|
+
| `delete(control_code)` | 删除资源及其所有关联码 |
|
|
141
|
+
| `is_control_code(code)` / `is_access_code(code)` | 判断码的类型 |
|
|
142
|
+
|
|
143
|
+
### `FileBackedResourceManager`
|
|
144
|
+
|
|
145
|
+
继承自 `ResourceManager`,额外接受以下构造参数:
|
|
146
|
+
|
|
147
|
+
- `data_dir_path`: 存储文件的目录路径
|
|
148
|
+
- `dumps_function` / `loads_function`: 自定义序列化函数(默认 `pickle.dumps` / `pickle.loads`)
|
|
149
|
+
- `file_name_gen`: 根据资源和控制码生成文件名的函数(默认使用 `uuid4`)
|
|
150
|
+
|
|
151
|
+
所有资源的存取均自动读写磁盘文件。
|
|
152
|
+
|
|
153
|
+
## 依赖
|
|
154
|
+
|
|
155
|
+
- Python >= 3.10
|
|
156
|
+
- `treelib` – 用于维护访问码树结构
|
|
157
|
+
- `aiofiles` – 异步文件操作
|
|
158
|
+
- `pathvalidate` – 校验文件名的合法性
|
|
159
|
+
|
|
160
|
+
## 许可证
|
|
161
|
+
|
|
162
|
+
本项目使用 [MIT 许可证](LICENSE)。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "access-control-async" # 你的包名
|
|
7
|
+
version = "0.2.5" # 当前版本
|
|
8
|
+
description = "A brief description of your access control library" # 简短描述
|
|
9
|
+
readme = "README.md" # 自述文件,PyPI 会将其渲染为项目首页
|
|
10
|
+
authors = [{name = "Jerry", email = "wujr24@m.fudan.edu.cn"}] # 替换为你的信息
|
|
11
|
+
license = {text = "MIT"} # 指定许可证,与你的 LICENSE 文件一致
|
|
12
|
+
classifiers = [ # 帮助用户在 PyPI 上找到你的项目
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
]
|
|
17
|
+
requires-python = ">=3.10"
|
|
18
|
+
dependencies = [ # 项目的运行时依赖,从 requirements.txt 移植
|
|
19
|
+
"treelib",
|
|
20
|
+
"aiofiles",
|
|
21
|
+
"pathvalidate",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls] # 可选,但推荐,指向你的项目主页或仓库
|
|
25
|
+
Homepage = "https://github.com/Jerry-Wu-GitHub/access-control"
|
|
26
|
+
Repository = "https://github.com/Jerry-Wu-GitHub/access-control.git"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"] # 在 src 目录下找包
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
异常
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class ResourceManagerError(Exception):
|
|
6
|
+
"""
|
|
7
|
+
与 ResourceManager 有关的错误。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
class PermissionInsufficient(ResourceManagerError):
|
|
11
|
+
"""
|
|
12
|
+
所使用的控制/访问码的权限不足(控制/访问码不存在)。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
class CodeExistError(ResourceManagerError):
|
|
16
|
+
"""
|
|
17
|
+
若控制码/访问码重复。
|
|
18
|
+
"""
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
class: FileBackedResourceManager
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable, Coroutine
|
|
7
|
+
import os
|
|
8
|
+
import pickle
|
|
9
|
+
from typing import Generic, Optional, override
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
import aiofiles
|
|
13
|
+
import aiofiles.os
|
|
14
|
+
from pathvalidate import ValidationError, is_valid_filename
|
|
15
|
+
|
|
16
|
+
from .utils import to_async
|
|
17
|
+
from .exceptions import PermissionInsufficient
|
|
18
|
+
from .resource_manager import (
|
|
19
|
+
ResourceManager,
|
|
20
|
+
ControlCode, AccessCode, Resource,
|
|
21
|
+
ControlCodeGen, ControlCodeGenAsync, AccessCodeGen, AccessCodeGenAsync,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def uuid4_str():
|
|
26
|
+
"""生成一个标准格式的随机UUID字符串"""
|
|
27
|
+
return str(uuid4())
|
|
28
|
+
|
|
29
|
+
# 资源与二进制数据转换函数
|
|
30
|
+
DumpsFunction = Callable[[Resource], bytes]
|
|
31
|
+
LoadsFunction = Callable[[bytes], Resource]
|
|
32
|
+
DumpsFunctionAsync = Callable[[Resource], Coroutine[None, None, bytes]]
|
|
33
|
+
LoadsFunctionAsync = Callable[[bytes], Coroutine[None, None, Resource]]
|
|
34
|
+
|
|
35
|
+
# 文件名生成函数
|
|
36
|
+
FileName = str
|
|
37
|
+
FileNameGen = Callable[[Optional[Resource], Optional[ControlCode]], FileName]
|
|
38
|
+
FileNameGenAsync = Callable[[Optional[Resource], Optional[ControlCode]], Coroutine[None, None, FileName]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FileBackedResourceManager(
|
|
42
|
+
Generic[Resource, ControlCode, AccessCode],
|
|
43
|
+
ResourceManager[FileName, ControlCode, AccessCode]
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Subclass of ResourceManager.
|
|
47
|
+
|
|
48
|
+
Store resources in disk files.
|
|
49
|
+
|
|
50
|
+
If control/access codes, codes generators and filename generator are all pickle serializable,
|
|
51
|
+
then the FileBackedResourceManager object can also be.
|
|
52
|
+
"""
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
data_dir_path: str,
|
|
56
|
+
dumps_function: Optional[DumpsFunction | DumpsFunctionAsync] = None,
|
|
57
|
+
loads_function: Optional[LoadsFunction | LoadsFunctionAsync] = None,
|
|
58
|
+
file_name_gen: Optional[FileNameGen | FileNameGenAsync] = None,
|
|
59
|
+
*,
|
|
60
|
+
control_code_gen: Optional[ControlCodeGen | ControlCodeGenAsync] = None,
|
|
61
|
+
access_code_gen: Optional[AccessCodeGen | AccessCodeGenAsync] = None,
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
初始化一个 FileBackedResourceManager 对象。
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
data_dir_path (str): 存储资源文件的目录。
|
|
68
|
+
dumps_function (Optional[DumpsFunction | DumpsFunctionAsync]): 将资源转换为字节串的函数,
|
|
69
|
+
默认为 pickle.dumps 。
|
|
70
|
+
loads_function (Optional[LoadsFunction | LoadsFunctionAsync]): 将字节串解码为资源的函数,
|
|
71
|
+
默认为 pickle.loads 。
|
|
72
|
+
file_name_gen (Optional[FileNameGen | FileNameGenAsync]): 根据资源内容(可选)和控制码(可选)生成文件名的函数。
|
|
73
|
+
control_code_gen (Optional[ControlCodeGen | ControlCodeGenAsync]): 根据资源内容(可选)生成控制码的函数。
|
|
74
|
+
access_code_gen (Optional[AccessCodeGen | AccessCodeGenAsync]): 根据资源内容(可选)、控制码(可选)、
|
|
75
|
+
父级访问码(可选)生成访问码的函数。
|
|
76
|
+
"""
|
|
77
|
+
super().__init__(control_code_gen, access_code_gen)
|
|
78
|
+
|
|
79
|
+
# 文件存放路径
|
|
80
|
+
self.data_dir_path = data_dir_path
|
|
81
|
+
|
|
82
|
+
# 数据转换函数
|
|
83
|
+
if not dumps_function:
|
|
84
|
+
dumps_function = pickle.dumps
|
|
85
|
+
|
|
86
|
+
if not loads_function:
|
|
87
|
+
loads_function = pickle.loads
|
|
88
|
+
|
|
89
|
+
self.dumps_async: DumpsFunctionAsync = to_async(dumps_function)
|
|
90
|
+
self.loads_async: LoadsFunctionAsync = to_async(loads_function)
|
|
91
|
+
|
|
92
|
+
# 文件名生成函数
|
|
93
|
+
if not file_name_gen:
|
|
94
|
+
file_name_gen = uuid4_str
|
|
95
|
+
self._file_name_gen_raw_async: FileNameGenAsync = to_async(file_name_gen)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get_path(self, file_name: str) -> str:
|
|
99
|
+
"""
|
|
100
|
+
获取文件存储的路径。
|
|
101
|
+
"""
|
|
102
|
+
return os.path.join(self.data_dir_path, file_name)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def file_name_gen_async(
|
|
106
|
+
self,
|
|
107
|
+
resource: Optional[Resource] = None,
|
|
108
|
+
control_code: Optional[ControlCode] = None
|
|
109
|
+
) -> FileName:
|
|
110
|
+
"""
|
|
111
|
+
包装了生成文件名的函数,使其能够接受接受 resource, control_code 参数。
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
return await self._file_name_gen_raw_async(resource, control_code)
|
|
115
|
+
except TypeError:
|
|
116
|
+
try:
|
|
117
|
+
return await self._file_name_gen_raw_async(resource)
|
|
118
|
+
except TypeError:
|
|
119
|
+
return await self._file_name_gen_raw_async()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ==== 重载父类方法 ====
|
|
123
|
+
|
|
124
|
+
@override
|
|
125
|
+
async def _get_resource_async(self, control_code: ControlCode) -> Resource:
|
|
126
|
+
"""
|
|
127
|
+
返回资源。
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
code (Code): 控制码或访问码。
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Resource: control_code 对应的资源。
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
FileNotFoundError: 如果文件没有找到。
|
|
137
|
+
PermissionInsufficient: 如果 control_code 不是控制码。
|
|
138
|
+
"""
|
|
139
|
+
# 获取文件路径
|
|
140
|
+
file_name = await super()._get_resource_async(control_code)
|
|
141
|
+
file_path = self._get_path(file_name)
|
|
142
|
+
|
|
143
|
+
# 读取文件
|
|
144
|
+
async with aiofiles.open(file_path, mode="rb") as file:
|
|
145
|
+
content = await file.read()
|
|
146
|
+
return await self.loads_async(content)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
async def _set_resource_async(self, control_code: ControlCode, resource: Resource) -> None:
|
|
151
|
+
"""
|
|
152
|
+
记录资源。
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
control_code (Code): 控制码。
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
PermissionInsufficient: 如果 file_name_gen 生成了重复的文件名。
|
|
159
|
+
ValidationError: 如果 file_name_gen 生成了非法的文件名。
|
|
160
|
+
"""
|
|
161
|
+
# 生成文件名
|
|
162
|
+
file_name = await self.file_name_gen_async(resource=resource, control_code=control_code)
|
|
163
|
+
if not is_valid_filename(file_name):
|
|
164
|
+
raise ValidationError(f"Invalid file name: {file_name}")
|
|
165
|
+
if file_name in self._control_resource_map.values():
|
|
166
|
+
raise PermissionInsufficient("Duplicate file name")
|
|
167
|
+
|
|
168
|
+
# 写入文件
|
|
169
|
+
await aiofiles.os.makedirs(self.data_dir_path, exist_ok=True)
|
|
170
|
+
file_path = self._get_path(file_name)
|
|
171
|
+
content = await self.dumps_async(resource)
|
|
172
|
+
async with aiofiles.open(file_path, mode="wb") as file:
|
|
173
|
+
await file.write(content)
|
|
174
|
+
|
|
175
|
+
# 记录
|
|
176
|
+
await super()._set_resource_async(control_code, file_name)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@override
|
|
180
|
+
async def _delete_resource_async(self, control_code: ControlCode) -> None:
|
|
181
|
+
"""
|
|
182
|
+
删除资源。
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
control_code (Code): 控制码。
|
|
186
|
+
"""
|
|
187
|
+
file_name = await super()._get_resource_async(control_code)
|
|
188
|
+
|
|
189
|
+
# 删除记录
|
|
190
|
+
await super()._delete_resource_async(control_code)
|
|
191
|
+
|
|
192
|
+
# 删除文件
|
|
193
|
+
file_path = self._get_path(file_name)
|
|
194
|
+
if await aiofiles.os.path.isfile(file_path):
|
|
195
|
+
await aiofiles.os.remove(file_path)
|