lfss 0.9.1__py3-none-any.whl → 0.9.4__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.
- Readme.md +4 -2
- docs/Changelog.md +27 -0
- docs/Enviroment_variables.md +12 -0
- docs/Known_issues.md +3 -1
- docs/Webdav.md +3 -3
- frontend/api.js +21 -0
- frontend/scripts.js +49 -2
- lfss/api/connector.py +6 -0
- lfss/eng/connection_pool.py +1 -1
- lfss/eng/database.py +13 -16
- lfss/eng/error.py +6 -2
- lfss/eng/thumb.py +5 -1
- lfss/eng/utils.py +46 -24
- lfss/svc/app.py +2 -2
- lfss/svc/app_base.py +10 -8
- lfss/svc/app_dav.py +114 -96
- lfss/svc/app_native.py +15 -9
- lfss/svc/common_impl.py +98 -31
- {lfss-0.9.1.dist-info → lfss-0.9.4.dist-info}/METADATA +5 -3
- {lfss-0.9.1.dist-info → lfss-0.9.4.dist-info}/RECORD +22 -20
- {lfss-0.9.1.dist-info → lfss-0.9.4.dist-info}/WHEEL +0 -0
- {lfss-0.9.1.dist-info → lfss-0.9.4.dist-info}/entry_points.txt +0 -0
lfss/svc/common_impl.py
CHANGED
@@ -7,7 +7,7 @@ from ..eng.datatype import UserRecord, FileRecord, PathContents, AccessLevel, Fi
|
|
7
7
|
from ..eng.database import FileConn, UserConn, delayed_log_access, check_file_read_permission, check_path_permission
|
8
8
|
from ..eng.thumb import get_thumb
|
9
9
|
from ..eng.utils import format_last_modified, ensure_uri_compnents
|
10
|
-
from ..eng.config import CHUNK_SIZE
|
10
|
+
from ..eng.config import CHUNK_SIZE, DEBUG_MODE
|
11
11
|
|
12
12
|
from .app_base import skip_request_log, db, logger
|
13
13
|
|
@@ -60,10 +60,15 @@ async def emit_file(
|
|
60
60
|
else:
|
61
61
|
arng_e = range_end
|
62
62
|
|
63
|
-
if
|
64
|
-
|
65
|
-
|
66
|
-
|
63
|
+
if file_record.file_size > 0:
|
64
|
+
if arng_s >= file_record.file_size or arng_e >= file_record.file_size:
|
65
|
+
if DEBUG_MODE: print(f"[Invalid range] Actual range: {arng_s}-{arng_e} (size: {file_record.file_size})")
|
66
|
+
raise HTTPException(status_code=416, detail="Range not satisfiable")
|
67
|
+
if arng_s > arng_e:
|
68
|
+
raise HTTPException(status_code=416, detail="Invalid range")
|
69
|
+
else:
|
70
|
+
if not (arng_s == 0 and arng_e == -1):
|
71
|
+
raise HTTPException(status_code=416, detail="Invalid range (file size is 0)")
|
67
72
|
|
68
73
|
headers = {
|
69
74
|
"Content-Disposition": f"{disposition}; filename={fname}",
|
@@ -87,7 +92,7 @@ async def emit_file(
|
|
87
92
|
status_code=206 if range_start != -1 or range_end != -1 else 200
|
88
93
|
)
|
89
94
|
|
90
|
-
async def
|
95
|
+
async def get_impl(
|
91
96
|
request: Request,
|
92
97
|
user: UserRecord,
|
93
98
|
path: str,
|
@@ -96,30 +101,12 @@ async def get_file_impl(
|
|
96
101
|
is_head = False,
|
97
102
|
):
|
98
103
|
path = ensure_uri_compnents(path)
|
104
|
+
if path.startswith("/"): path = path[1:]
|
99
105
|
|
100
106
|
# handle directory query
|
101
107
|
if path == "": path = "/"
|
102
108
|
if path.endswith("/"):
|
103
|
-
|
104
|
-
async with unique_cursor() as cur:
|
105
|
-
fconn = FileConn(cur)
|
106
|
-
if user.id == 0:
|
107
|
-
raise HTTPException(status_code=401, detail="Permission denied, credential required")
|
108
|
-
if thumb:
|
109
|
-
return await emit_thumbnail(path, download, create_time=None)
|
110
|
-
|
111
|
-
if path == "/":
|
112
|
-
peer_users = await UserConn(cur).list_peer_users(user.id, AccessLevel.READ)
|
113
|
-
return PathContents(
|
114
|
-
dirs = await fconn.list_root_dirs(user.username, *[x.username for x in peer_users], skim=True) \
|
115
|
-
if not user.is_admin else await fconn.list_root_dirs(skim=True),
|
116
|
-
files = []
|
117
|
-
)
|
118
|
-
|
119
|
-
if not await check_path_permission(path, user, cursor=cur) >= AccessLevel.READ:
|
120
|
-
raise HTTPException(status_code=403, detail="Permission denied")
|
121
|
-
|
122
|
-
return await fconn.list_path(path)
|
109
|
+
return await _get_dir_impl(user=user, path=path, download=download, thumb=thumb, is_head=is_head)
|
123
110
|
|
124
111
|
# handle file query
|
125
112
|
async with unique_cursor() as cur:
|
@@ -147,6 +134,9 @@ async def get_file_impl(
|
|
147
134
|
else:
|
148
135
|
range_start, range_end = -1, -1
|
149
136
|
|
137
|
+
if DEBUG_MODE:
|
138
|
+
print(f"Get range: {range_start}-{range_end}")
|
139
|
+
|
150
140
|
if thumb:
|
151
141
|
if (range_start != -1 or range_end != -1): logger.warning("Range request for thumbnail")
|
152
142
|
return await emit_thumbnail(path, download, create_time=file_record.create_time, is_head=is_head)
|
@@ -156,11 +146,55 @@ async def get_file_impl(
|
|
156
146
|
else:
|
157
147
|
return await emit_file(file_record, None, "inline", is_head = is_head, range_start=range_start, range_end=range_end)
|
158
148
|
|
149
|
+
async def _get_dir_impl(
|
150
|
+
user: UserRecord,
|
151
|
+
path: str,
|
152
|
+
download: bool = False,
|
153
|
+
thumb: bool = False,
|
154
|
+
is_head = False,
|
155
|
+
):
|
156
|
+
""" handle directory query, return file under the path as json """
|
157
|
+
assert path.endswith("/")
|
158
|
+
async with unique_cursor() as cur:
|
159
|
+
fconn = FileConn(cur)
|
160
|
+
if user.id == 0:
|
161
|
+
raise HTTPException(status_code=401, detail="Permission denied, credential required")
|
162
|
+
if thumb:
|
163
|
+
return await emit_thumbnail(path, download, create_time=None)
|
164
|
+
|
165
|
+
if path == "/":
|
166
|
+
if is_head: return Response(status_code=200)
|
167
|
+
peer_users = await UserConn(cur).list_peer_users(user.id, AccessLevel.READ)
|
168
|
+
return PathContents(
|
169
|
+
dirs = await fconn.list_root_dirs(user.username, *[x.username for x in peer_users], skim=True) \
|
170
|
+
if not user.is_admin else await fconn.list_root_dirs(skim=True),
|
171
|
+
files = []
|
172
|
+
)
|
173
|
+
|
174
|
+
if not await check_path_permission(path, user, cursor=cur) >= AccessLevel.READ:
|
175
|
+
raise HTTPException(status_code=403, detail="Permission denied")
|
176
|
+
|
177
|
+
path_sp = path.split("/")
|
178
|
+
if is_head:
|
179
|
+
if len(path_sp) == 2:
|
180
|
+
assert path_sp[1] == ""
|
181
|
+
if await UserConn(cur).get_user(path_sp[0]):
|
182
|
+
return Response(status_code=200)
|
183
|
+
else:
|
184
|
+
raise HTTPException(status_code=404, detail="User not found")
|
185
|
+
else:
|
186
|
+
if await FileConn(cur).count_path_files(path, flat=True) > 0:
|
187
|
+
return Response(status_code=200)
|
188
|
+
else:
|
189
|
+
raise HTTPException(status_code=404, detail="Path not found")
|
190
|
+
|
191
|
+
return await fconn.list_path(path)
|
192
|
+
|
159
193
|
async def put_file_impl(
|
160
194
|
request: Request,
|
161
195
|
user: UserRecord,
|
162
196
|
path: str,
|
163
|
-
conflict: Literal["overwrite", "skip", "abort"] = "
|
197
|
+
conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
|
164
198
|
permission: int = 0,
|
165
199
|
):
|
166
200
|
path = ensure_uri_compnents(path)
|
@@ -187,7 +221,9 @@ async def put_file_impl(
|
|
187
221
|
exists_flag = True
|
188
222
|
if await check_path_permission(path, user) < AccessLevel.WRITE:
|
189
223
|
raise HTTPException(status_code=403, detail="Permission denied, cannot overwrite other's file")
|
190
|
-
await db.delete_file(path)
|
224
|
+
old_record = await db.delete_file(path)
|
225
|
+
if old_record and permission == FileReadPermission.UNSET.value:
|
226
|
+
permission = old_record.permission.value # inherit permission
|
191
227
|
|
192
228
|
# check content-type
|
193
229
|
content_type = request.headers.get("Content-Type", "application/octet-stream")
|
@@ -213,7 +249,7 @@ async def post_file_impl(
|
|
213
249
|
path: str,
|
214
250
|
user: UserRecord,
|
215
251
|
file: UploadFile,
|
216
|
-
conflict: Literal["overwrite", "skip", "abort"] = "
|
252
|
+
conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
|
217
253
|
permission: int = 0,
|
218
254
|
):
|
219
255
|
path = ensure_uri_compnents(path)
|
@@ -240,7 +276,9 @@ async def post_file_impl(
|
|
240
276
|
exists_flag = True
|
241
277
|
if await check_path_permission(path, user) < AccessLevel.WRITE:
|
242
278
|
raise HTTPException(status_code=403, detail="Permission denied, cannot overwrite other's file")
|
243
|
-
await db.delete_file(path)
|
279
|
+
old_record = await db.delete_file(path)
|
280
|
+
if old_record and permission == FileReadPermission.UNSET.value:
|
281
|
+
permission = old_record.permission.value # inherit permission
|
244
282
|
|
245
283
|
async def blob_reader():
|
246
284
|
nonlocal file
|
@@ -252,7 +290,7 @@ async def post_file_impl(
|
|
252
290
|
"Content-Type": "application/json",
|
253
291
|
}, content=json.dumps({"url": path}))
|
254
292
|
|
255
|
-
async def
|
293
|
+
async def delete_impl(path: str, user: UserRecord):
|
256
294
|
path = ensure_uri_compnents(path)
|
257
295
|
if await check_path_permission(path, user) < AccessLevel.WRITE:
|
258
296
|
raise HTTPException(status_code=403, detail="Permission denied")
|
@@ -268,3 +306,32 @@ async def delete_file_impl(path: str, user: UserRecord):
|
|
268
306
|
return Response(status_code=200, content="Deleted")
|
269
307
|
else:
|
270
308
|
return Response(status_code=404, content="Not found")
|
309
|
+
|
310
|
+
async def copy_impl(
|
311
|
+
op_user: UserRecord, src_path: str, dst_path: str,
|
312
|
+
):
|
313
|
+
src_path = ensure_uri_compnents(src_path)
|
314
|
+
dst_path = ensure_uri_compnents(dst_path)
|
315
|
+
copy_type = "file" if not src_path[-1] == "/" else "directory"
|
316
|
+
if (src_path[-1] == "/") != (dst_path[-1] == "/"):
|
317
|
+
raise HTTPException(status_code=400, detail="Source and destination must be same type")
|
318
|
+
|
319
|
+
if src_path == dst_path:
|
320
|
+
raise HTTPException(status_code=400, detail="Source and destination are the same")
|
321
|
+
|
322
|
+
logger.info(f"Copy {src_path} to {dst_path}, user: {op_user.username}")
|
323
|
+
if copy_type == "file":
|
324
|
+
async with unique_cursor() as cur:
|
325
|
+
fconn = FileConn(cur)
|
326
|
+
dst_record = await fconn.get_file_record(dst_path)
|
327
|
+
if dst_record:
|
328
|
+
raise HTTPException(status_code=409, detail="Destination exists")
|
329
|
+
await db.copy_file(src_path, dst_path, op_user)
|
330
|
+
else:
|
331
|
+
async with unique_cursor() as cur:
|
332
|
+
fconn = FileConn(cur)
|
333
|
+
dst_fcount = await fconn.count_path_files(dst_path, flat=True)
|
334
|
+
if dst_fcount > 0:
|
335
|
+
raise HTTPException(status_code=409, detail="Destination exists")
|
336
|
+
await db.copy_path(src_path, dst_path, op_user)
|
337
|
+
return Response(status_code=201, content="OK")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lfss
|
3
|
-
Version: 0.9.
|
3
|
+
Version: 0.9.4
|
4
4
|
Summary: Lightweight file storage service
|
5
5
|
Home-page: https://github.com/MenxLi/lfss
|
6
6
|
Author: li_mengxun
|
@@ -57,10 +57,12 @@ The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/ur
|
|
57
57
|
The authentication can be acheived through one of the following methods:
|
58
58
|
1. `Authorization` header with the value `Bearer sha256(<username><password>)`.
|
59
59
|
2. `token` query parameter with the value `sha256(<username><password>)`.
|
60
|
-
3. HTTP Basic Authentication with the username and password.
|
60
|
+
3. HTTP Basic Authentication with the username and password (If WebDAV is enabled).
|
61
61
|
|
62
62
|
You can refer to `frontend` as an application example, `lfss/api/connector.py` for more APIs.
|
63
63
|
|
64
64
|
By default, the service exposes all files to the public for `GET` requests,
|
65
65
|
but file-listing is restricted to the user's own files.
|
66
|
-
Please refer to [docs/Permission.md](./docs/Permission.md) for more details on the permission system.
|
66
|
+
Please refer to [docs/Permission.md](./docs/Permission.md) for more details on the permission system.
|
67
|
+
|
68
|
+
More can be found in the [docs](./docs) directory.
|
@@ -1,8 +1,10 @@
|
|
1
|
-
Readme.md,sha256=
|
2
|
-
docs/
|
1
|
+
Readme.md,sha256=JVe9T6N1Rz4hTiiCVoDYe2VB0dAi60VcBgb2twQdfZc,1834
|
2
|
+
docs/Changelog.md,sha256=3mRHcda4UK8c105XtBfbeTWij0S4xNc-U8JTTPUqCJk,769
|
3
|
+
docs/Enviroment_variables.md,sha256=LUZF1o70emp-5UPsvXPjcxapP940OqEZzSyyUUT9bEQ,569
|
4
|
+
docs/Known_issues.md,sha256=ZqETcWP8lzTOel9b2mxEgCnADFF8IxOrEtiVO1NoMAk,251
|
3
5
|
docs/Permission.md,sha256=mvK8gVBBgoIFJqikcaReU_bUo-mTq_ECqJaDDJoQF7Q,3126
|
4
|
-
docs/Webdav.md,sha256
|
5
|
-
frontend/api.js,sha256=
|
6
|
+
docs/Webdav.md,sha256=-Ja-BTWSY1BEMAyZycvEMNnkNTPZ49gSPzmf3Lbib70,1547
|
7
|
+
frontend/api.js,sha256=GlQsNoZFEcy7QUUsLbXv7aP-KxRnIxM37FQHTaakGiQ,19387
|
6
8
|
frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
|
7
9
|
frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
|
8
10
|
frontend/info.js,sha256=xGUJPCSrtDhuSu0ELLQZ77PmVWldg-prU1mwQGbdEoA,5797
|
@@ -10,14 +12,14 @@ frontend/login.css,sha256=VMM0QfbDFYerxKWKSGhMI1yg5IRBXg0TTdLJEEhQZNk,355
|
|
10
12
|
frontend/login.js,sha256=QoO8yKmBHDVP-ZomCMOaV7xVUVIhpl7esJrb6T5aHQE,2466
|
11
13
|
frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
|
12
14
|
frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
|
13
|
-
frontend/scripts.js,sha256=
|
15
|
+
frontend/scripts.js,sha256=2-Omsb1-s4Wc859_SYw8JGyeUSiADaH9va4w87Mozns,24134
|
14
16
|
frontend/state.js,sha256=vbNL5DProRKmSEY7xu9mZH6IY0PBenF8WGxPtGgDnLI,1680
|
15
17
|
frontend/styles.css,sha256=xcNLqI3KBsY5TLnku8UIP0Jfr7QLajr1_KNlZj9eheM,4935
|
16
18
|
frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
|
17
19
|
frontend/thumb.js,sha256=46ViD2TlTTWy0fx6wjoAs_5CQ4ajYB90vVzM7UO2IHw,6182
|
18
20
|
frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
|
19
21
|
lfss/api/__init__.py,sha256=8IJqrpWK1doIyVVbntvVic82A57ncwl5b0BRHX4Ri6A,6660
|
20
|
-
lfss/api/connector.py,sha256=
|
22
|
+
lfss/api/connector.py,sha256=hHSEEWecKQGZH6oxAmYoG3q7lFfacCbOKVZiUIXT2y8,11819
|
21
23
|
lfss/cli/__init__.py,sha256=lPwPmqpa7EXQ4zlU7E7LOe6X2kw_xATGdwoHphUEirA,827
|
22
24
|
lfss/cli/balance.py,sha256=fUbKKAUyaDn74f7mmxMfBL4Q4voyBLHu6Lg_g8GfMOQ,4121
|
23
25
|
lfss/cli/cli.py,sha256=aYjB8d4k6JUd9efxZK-XOj-mlG4JeOr_0lnj2qqCiK0,8066
|
@@ -28,22 +30,22 @@ lfss/cli/vacuum.py,sha256=GOG72d3NYe9bYCNc3y8JecEmM-DrKlGq3JQcisv_xBg,3702
|
|
28
30
|
lfss/eng/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
29
31
|
lfss/eng/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
|
30
32
|
lfss/eng/config.py,sha256=DmnUYMeLOL-45OstysyMpSBPmLofgzvcSrsWjHvssYs,915
|
31
|
-
lfss/eng/connection_pool.py,sha256
|
32
|
-
lfss/eng/database.py,sha256=
|
33
|
+
lfss/eng/connection_pool.py,sha256=4xOF1kXXGqCWeLX5ZVFALKjdY8N1VVAVSSTRfCzbj94,6141
|
34
|
+
lfss/eng/database.py,sha256=2i8gbh1odOA09tS5VU9cUZy3poZUdCx3XX7UX7umtxw,47188
|
33
35
|
lfss/eng/datatype.py,sha256=27UB7-l9SICy5lAvKjdzpTL_GohZjzstQcr9PtAq7nM,2709
|
34
|
-
lfss/eng/error.py,sha256=
|
36
|
+
lfss/eng/error.py,sha256=dAlQHXOnQcSkA2vTugJFSxcyDqoFlPucBoFpTZ7GI6w,654
|
35
37
|
lfss/eng/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
|
36
|
-
lfss/eng/thumb.py,sha256=
|
37
|
-
lfss/eng/utils.py,sha256=
|
38
|
+
lfss/eng/thumb.py,sha256=x9jIHHU1tskmp-TavPPcxGpbmEjCp9gbH6ZlsEfqUxY,3383
|
39
|
+
lfss/eng/utils.py,sha256=CYEQvPiM28k53hCJBE7N6O6a1xC_wvnP3KZx4DCnD0k,6723
|
38
40
|
lfss/sql/init.sql,sha256=8LjHx0TBCkBD62xFfssSeHDqKYVQQJkZAg4rSm046f4,1496
|
39
41
|
lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
|
40
|
-
lfss/svc/app.py,sha256=
|
41
|
-
lfss/svc/app_base.py,sha256=
|
42
|
-
lfss/svc/app_dav.py,sha256=
|
43
|
-
lfss/svc/app_native.py,sha256=
|
44
|
-
lfss/svc/common_impl.py,sha256=
|
42
|
+
lfss/svc/app.py,sha256=ftWCpepBx-gTSG7i-TB-IdinPPstAYYQjCgnTfeMZeI,219
|
43
|
+
lfss/svc/app_base.py,sha256=BU_DndHW4sYiWUQcTis8iGljmUy8FHfZrzCkE0d1z-Y,6717
|
44
|
+
lfss/svc/app_dav.py,sha256=D0KSgjtTktPjIhyIKG5eRmBdh5X8HYFYH151E6gzlbc,18245
|
45
|
+
lfss/svc/app_native.py,sha256=6yBRJB8_p4RZgDVheDTv1ClBGc3etrQm94j1NiR4FUQ,9349
|
46
|
+
lfss/svc/common_impl.py,sha256=0fjbqHWgqDhLfBEu6aC0Z5qgNt67C7z0Qroj7aV3Iq4,13830
|
45
47
|
lfss/svc/request_log.py,sha256=v8yXEIzPjaksu76Oh5vgdbUEUrw8Kt4etLAXBWSGie8,3207
|
46
|
-
lfss-0.9.
|
47
|
-
lfss-0.9.
|
48
|
-
lfss-0.9.
|
49
|
-
lfss-0.9.
|
48
|
+
lfss-0.9.4.dist-info/METADATA,sha256=3wUuwMRn55Z2lnX9wZRGMVxLbfphSLOk1gX01haFaOw,2594
|
49
|
+
lfss-0.9.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
50
|
+
lfss-0.9.4.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
|
51
|
+
lfss-0.9.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|