lfss 0.11.1__py3-none-any.whl → 0.11.3__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.
- docs/Enviroment_variables.md +3 -1
- docs/changelog.md +27 -0
- frontend/api.js +66 -4
- frontend/login.js +0 -1
- frontend/popup.js +18 -3
- frontend/scripts.js +8 -5
- frontend/utils.js +4 -5
- lfss/api/connector.py +17 -2
- lfss/cli/cli.py +7 -7
- lfss/cli/log.py +77 -0
- lfss/cli/vacuum.py +10 -3
- lfss/eng/config.py +6 -3
- lfss/eng/database.py +99 -40
- lfss/eng/log.py +91 -21
- lfss/eng/utils.py +1 -2
- lfss/svc/app_base.py +4 -1
- lfss/svc/app_dav.py +7 -7
- lfss/svc/app_native.py +58 -11
- lfss/svc/common_impl.py +4 -4
- {lfss-0.11.1.dist-info → lfss-0.11.3.dist-info}/METADATA +3 -2
- {lfss-0.11.1.dist-info → lfss-0.11.3.dist-info}/RECORD +23 -22
- {lfss-0.11.1.dist-info → lfss-0.11.3.dist-info}/entry_points.txt +1 -0
- {lfss-0.11.1.dist-info → lfss-0.11.3.dist-info}/WHEEL +0 -0
lfss/svc/app_native.py
CHANGED
@@ -1,17 +1,19 @@
|
|
1
|
-
from typing import Optional, Literal
|
1
|
+
from typing import Optional, Literal, Annotated
|
2
|
+
from collections import OrderedDict
|
2
3
|
|
3
|
-
from fastapi import Depends, Request, Response, UploadFile
|
4
|
-
from fastapi.responses import StreamingResponse
|
4
|
+
from fastapi import Depends, Request, Response, UploadFile, Query
|
5
|
+
from fastapi.responses import StreamingResponse, JSONResponse
|
5
6
|
from fastapi.exceptions import HTTPException
|
6
7
|
|
7
8
|
from ..eng.utils import ensure_uri_compnents
|
8
9
|
from ..eng.config import MAX_MEM_FILE_BYTES
|
9
10
|
from ..eng.connection_pool import unique_cursor
|
10
|
-
from ..eng.database import check_file_read_permission, check_path_permission,
|
11
|
+
from ..eng.database import check_file_read_permission, check_path_permission, FileConn, delayed_log_access
|
11
12
|
from ..eng.datatype import (
|
12
13
|
FileReadPermission, UserRecord, AccessLevel,
|
13
14
|
FileSortKey, DirSortKey
|
14
15
|
)
|
16
|
+
from ..eng.error import InvalidPathError
|
15
17
|
|
16
18
|
from .app_base import *
|
17
19
|
from .common_impl import get_impl, put_file_impl, post_file_impl, delete_impl, copy_impl
|
@@ -90,13 +92,13 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
|
|
90
92
|
raise HTTPException(status_code=400, detail="Cannot bundle root")
|
91
93
|
|
92
94
|
async with unique_cursor() as cur:
|
93
|
-
dir_record = await FileConn(cur).
|
95
|
+
dir_record = await FileConn(cur).get_dir_record(path)
|
94
96
|
|
95
97
|
pathname = f"{path.split('/')[-2]}"
|
96
98
|
|
97
99
|
if dir_record.size < MAX_MEM_FILE_BYTES:
|
98
100
|
logger.debug(f"Bundle {path} in memory")
|
99
|
-
dir_bytes = (await db.
|
101
|
+
dir_bytes = (await db.zip_dir(path, op_user=user)).getvalue()
|
100
102
|
return Response(
|
101
103
|
content = dir_bytes,
|
102
104
|
media_type = "application/zip",
|
@@ -109,7 +111,7 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
|
|
109
111
|
else:
|
110
112
|
logger.debug(f"Bundle {path} in stream")
|
111
113
|
return StreamingResponse(
|
112
|
-
content = await db.
|
114
|
+
content = await db.zip_dir_stream(path, op_user=user),
|
113
115
|
media_type = "application/zip",
|
114
116
|
headers = {
|
115
117
|
f"Content-Disposition": f"attachment; filename=bundle-{pathname}.zip",
|
@@ -134,7 +136,7 @@ async def get_file_meta(path: str, user: UserRecord = Depends(registered_user)):
|
|
134
136
|
else:
|
135
137
|
if await check_path_permission(path, user, cursor=cur) < AccessLevel.READ:
|
136
138
|
raise HTTPException(status_code=403, detail="Permission denied")
|
137
|
-
record = await fconn.
|
139
|
+
record = await fconn.get_dir_record(path)
|
138
140
|
return record
|
139
141
|
|
140
142
|
@router_api.post("/meta")
|
@@ -171,7 +173,7 @@ async def update_file_meta(
|
|
171
173
|
new_path = ensure_uri_compnents(new_path)
|
172
174
|
logger.info(f"Update path of {path} to {new_path}")
|
173
175
|
# will raise duplicate path error if same name path exists in the new path
|
174
|
-
await db.
|
176
|
+
await db.move_dir(path, new_path, user)
|
175
177
|
|
176
178
|
return Response(status_code=200, content="OK")
|
177
179
|
|
@@ -189,13 +191,15 @@ async def validate_path_read_permission(path: str, user: UserRecord):
|
|
189
191
|
if not await check_path_permission(path, user) >= AccessLevel.READ:
|
190
192
|
raise HTTPException(status_code=403, detail="Permission denied")
|
191
193
|
@router_api.get("/count-files")
|
194
|
+
@handle_exception
|
192
195
|
async def count_files(path: str, flat: bool = False, user: UserRecord = Depends(registered_user)):
|
193
196
|
await validate_path_read_permission(path, user)
|
194
197
|
path = ensure_uri_compnents(path)
|
195
198
|
async with unique_cursor() as conn:
|
196
199
|
fconn = FileConn(conn)
|
197
|
-
return { "count": await fconn.
|
200
|
+
return { "count": await fconn.count_dir_files(url = path, flat = flat) }
|
198
201
|
@router_api.get("/list-files")
|
202
|
+
@handle_exception
|
199
203
|
async def list_files(
|
200
204
|
path: str, offset: int = 0, limit: int = 1000,
|
201
205
|
order_by: FileSortKey = "", order_desc: bool = False,
|
@@ -205,13 +209,14 @@ async def list_files(
|
|
205
209
|
path = ensure_uri_compnents(path)
|
206
210
|
async with unique_cursor() as conn:
|
207
211
|
fconn = FileConn(conn)
|
208
|
-
return await fconn.
|
212
|
+
return await fconn.list_dir_files(
|
209
213
|
url = path, offset = offset, limit = limit,
|
210
214
|
order_by=order_by, order_desc=order_desc,
|
211
215
|
flat=flat
|
212
216
|
)
|
213
217
|
|
214
218
|
@router_api.get("/count-dirs")
|
219
|
+
@handle_exception
|
215
220
|
async def count_dirs(path: str, user: UserRecord = Depends(registered_user)):
|
216
221
|
await validate_path_read_permission(path, user)
|
217
222
|
path = ensure_uri_compnents(path)
|
@@ -219,6 +224,7 @@ async def count_dirs(path: str, user: UserRecord = Depends(registered_user)):
|
|
219
224
|
fconn = FileConn(conn)
|
220
225
|
return { "count": await fconn.count_path_dirs(url = path) }
|
221
226
|
@router_api.get("/list-dirs")
|
227
|
+
@handle_exception
|
222
228
|
async def list_dirs(
|
223
229
|
path: str, offset: int = 0, limit: int = 1000,
|
224
230
|
order_by: DirSortKey = "", order_desc: bool = False,
|
@@ -232,6 +238,47 @@ async def list_dirs(
|
|
232
238
|
url = path, offset = offset, limit = limit,
|
233
239
|
order_by=order_by, order_desc=order_desc, skim=skim
|
234
240
|
)
|
241
|
+
|
242
|
+
# https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#query-parameter-list-multiple-values
|
243
|
+
@router_api.get("/get-multiple")
|
244
|
+
@handle_exception
|
245
|
+
async def get_multiple_files(
|
246
|
+
path: Annotated[list[str], Query()],
|
247
|
+
skip_content: bool = False,
|
248
|
+
user: UserRecord = Depends(registered_user)
|
249
|
+
):
|
250
|
+
"""
|
251
|
+
Get multiple files by path.
|
252
|
+
Please note that the content is supposed to be text and are small enough to fit in memory.
|
253
|
+
|
254
|
+
Not existing files will have content null, and the response will be 206 Partial Content if not all files are found.
|
255
|
+
if skip_content is True, the content of the files will always be ''
|
256
|
+
"""
|
257
|
+
for p in path:
|
258
|
+
if p.endswith("/"):
|
259
|
+
raise InvalidPathError(f"Path '{p}' must not end with /")
|
260
|
+
|
261
|
+
# here we unify the path, so need to keep a record of the inputs
|
262
|
+
# make output keys consistent with inputs
|
263
|
+
upath2path = OrderedDict[str, str]()
|
264
|
+
for p in path:
|
265
|
+
p_ = p if not p.startswith("/") else p[1:]
|
266
|
+
upath2path[ensure_uri_compnents(p_)] = p
|
267
|
+
upaths = list(upath2path.keys())
|
268
|
+
|
269
|
+
# get files
|
270
|
+
raw_res = await db.read_files_bulk(upaths, skip_content=skip_content, op_user=user)
|
271
|
+
for k in raw_res.keys():
|
272
|
+
await delayed_log_access(k)
|
273
|
+
partial_content = len(raw_res) != len(upaths)
|
274
|
+
|
275
|
+
return JSONResponse(
|
276
|
+
content = {
|
277
|
+
upath2path[k]: v.decode('utf-8') if v is not None else None for k, v in raw_res.items()
|
278
|
+
},
|
279
|
+
status_code = 206 if partial_content else 200
|
280
|
+
)
|
281
|
+
|
235
282
|
|
236
283
|
@router_api.get("/whoami")
|
237
284
|
@handle_exception
|
lfss/svc/common_impl.py
CHANGED
@@ -180,7 +180,7 @@ async def _get_dir_impl(
|
|
180
180
|
else:
|
181
181
|
raise HTTPException(status_code=404, detail="User not found")
|
182
182
|
else:
|
183
|
-
if await FileConn(cur).
|
183
|
+
if await FileConn(cur).count_dir_files(path, flat=True) > 0:
|
184
184
|
return Response(status_code=200)
|
185
185
|
else:
|
186
186
|
raise HTTPException(status_code=404, detail="Path not found")
|
@@ -295,7 +295,7 @@ async def delete_impl(path: str, user: UserRecord):
|
|
295
295
|
logger.info(f"DELETE {path}, user: {user.username}")
|
296
296
|
|
297
297
|
if path.endswith("/"):
|
298
|
-
res = await db.
|
298
|
+
res = await db.delete_dir(path, user)
|
299
299
|
else:
|
300
300
|
res = await db.delete_file(path, user)
|
301
301
|
|
@@ -327,8 +327,8 @@ async def copy_impl(
|
|
327
327
|
else:
|
328
328
|
async with unique_cursor() as cur:
|
329
329
|
fconn = FileConn(cur)
|
330
|
-
dst_fcount = await fconn.
|
330
|
+
dst_fcount = await fconn.count_dir_files(dst_path, flat=True)
|
331
331
|
if dst_fcount > 0:
|
332
332
|
raise HTTPException(status_code=409, detail="Destination exists")
|
333
|
-
await db.
|
333
|
+
await db.copy_dir(src_path, dst_path, op_user)
|
334
334
|
return Response(status_code=201, content="OK")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lfss
|
3
|
-
Version: 0.11.
|
3
|
+
Version: 0.11.3
|
4
4
|
Summary: Lightweight file storage service
|
5
5
|
Home-page: https://github.com/MenxLi/lfss
|
6
6
|
Author: Li, Mengxun
|
@@ -10,13 +10,14 @@ Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.10
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
13
|
-
Requires-Dist: aiofiles (==
|
13
|
+
Requires-Dist: aiofiles (==24.*)
|
14
14
|
Requires-Dist: aiosqlite (==0.*)
|
15
15
|
Requires-Dist: fastapi (==0.*)
|
16
16
|
Requires-Dist: mimesniff (==1.*)
|
17
17
|
Requires-Dist: pillow
|
18
18
|
Requires-Dist: python-multipart
|
19
19
|
Requires-Dist: requests (==2.*)
|
20
|
+
Requires-Dist: rich
|
20
21
|
Requires-Dist: stream-zip (==0.*)
|
21
22
|
Requires-Dist: uvicorn (==0.*)
|
22
23
|
Project-URL: Repository, https://github.com/MenxLi/lfss
|
@@ -1,51 +1,52 @@
|
|
1
1
|
Readme.md,sha256=B-foESzFWoSI5MEd89AWUzKcVRrTwipM28TK8GN0o8c,1920
|
2
|
-
docs/Enviroment_variables.md,sha256=
|
2
|
+
docs/Enviroment_variables.md,sha256=CZ5DrrXSLU5RLBEVQ-gLMaOIuFthd7dEiTzO7ODrPRQ,788
|
3
3
|
docs/Known_issues.md,sha256=ZqETcWP8lzTOel9b2mxEgCnADFF8IxOrEtiVO1NoMAk,251
|
4
4
|
docs/Permission.md,sha256=thUJx7YRoU63Pb-eqo5l5450DrZN3QYZ36GCn8r66no,3152
|
5
5
|
docs/Webdav.md,sha256=-Ja-BTWSY1BEMAyZycvEMNnkNTPZ49gSPzmf3Lbib70,1547
|
6
|
-
docs/changelog.md,sha256=
|
7
|
-
frontend/api.js,sha256=
|
6
|
+
docs/changelog.md,sha256=fE0rE2IcovbxMhdTeqhnCnknT1vtVr7A860zIh7AEnE,1581
|
7
|
+
frontend/api.js,sha256=F35jQjWF2LITkuO-wZJuEKyafLWFx_M4C2tEYJV8zak,22631
|
8
8
|
frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
|
9
9
|
frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
|
10
10
|
frontend/info.js,sha256=xGUJPCSrtDhuSu0ELLQZ77PmVWldg-prU1mwQGbdEoA,5797
|
11
11
|
frontend/login.css,sha256=VMM0QfbDFYerxKWKSGhMI1yg5IRBXg0TTdLJEEhQZNk,355
|
12
|
-
frontend/login.js,sha256=
|
12
|
+
frontend/login.js,sha256=xJkulk8dlvV4BhevADLeUrnZwShiFTWv3Wg2iJFUZlY,2423
|
13
13
|
frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
|
14
|
-
frontend/popup.js,sha256=
|
15
|
-
frontend/scripts.js,sha256=
|
14
|
+
frontend/popup.js,sha256=cyUjtO0wbtqbEodHfwyUsak9iWbcDXeWMGDhpCPbcoE,5453
|
15
|
+
frontend/scripts.js,sha256=T3kMjTxrjOkp93OV4ZMGgCLRRaQgRmNzzxriOMGVeZM,24412
|
16
16
|
frontend/state.js,sha256=vbNL5DProRKmSEY7xu9mZH6IY0PBenF8WGxPtGgDnLI,1680
|
17
17
|
frontend/styles.css,sha256=xcNLqI3KBsY5TLnku8UIP0Jfr7QLajr1_KNlZj9eheM,4935
|
18
18
|
frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
|
19
19
|
frontend/thumb.js,sha256=46ViD2TlTTWy0fx6wjoAs_5CQ4ajYB90vVzM7UO2IHw,6182
|
20
|
-
frontend/utils.js,sha256=
|
20
|
+
frontend/utils.js,sha256=XP5hM_mROYaxK5dqn9qZVwv7GdQuiDzByilFskbrnxA,6068
|
21
21
|
lfss/api/__init__.py,sha256=zT1JCiUM76wX-GtRrmKhTUzSYYfcmoyI1vYwN0fCcLw,6818
|
22
|
-
lfss/api/connector.py,sha256=
|
22
|
+
lfss/api/connector.py,sha256=o0_Ws1cmDJdM5YFKy5EhwStU9nW9a05CrjVYDex0Hmo,13931
|
23
23
|
lfss/cli/__init__.py,sha256=lPwPmqpa7EXQ4zlU7E7LOe6X2kw_xATGdwoHphUEirA,827
|
24
24
|
lfss/cli/balance.py,sha256=fUbKKAUyaDn74f7mmxMfBL4Q4voyBLHu6Lg_g8GfMOQ,4121
|
25
|
-
lfss/cli/cli.py,sha256=
|
25
|
+
lfss/cli/cli.py,sha256=QLItJBjCJv6mWMUp5T6M0tUBuBzWr8yxoqn6V55Mb7I,8193
|
26
|
+
lfss/cli/log.py,sha256=TBlt8mhHMouv8ZBUMHYfGZiV6-0yPdajJQ5mkGHEojI,3016
|
26
27
|
lfss/cli/panel.py,sha256=Xq3I_n-ctveym-Gh9LaUpzHiLlvt3a_nuDiwUS-MGrg,1597
|
27
28
|
lfss/cli/serve.py,sha256=vTo6_BiD7Dn3VLvHsC5RKRBC3lMu45JVr_0SqpgHdj0,1086
|
28
29
|
lfss/cli/user.py,sha256=1mTroQbaKxHjFCPHT67xwd08v-zxH0RZ_OnVc-4MzL0,5364
|
29
|
-
lfss/cli/vacuum.py,sha256=
|
30
|
+
lfss/cli/vacuum.py,sha256=arEY89kYJKEpzuzjKtf21V7s0QzM1t3QWa1hNghhT0Q,6611
|
30
31
|
lfss/eng/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
32
|
lfss/eng/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
|
32
|
-
lfss/eng/config.py,sha256=
|
33
|
+
lfss/eng/config.py,sha256=0dncHYn3tYw4pKBwXuP_huz0u7ud23fJ6SUmUPfLmeM,967
|
33
34
|
lfss/eng/connection_pool.py,sha256=1aq7nSgd7hB9YNV4PjD1RDRyl_moDw3ubBtSLyfgGBs,6320
|
34
|
-
lfss/eng/database.py,sha256
|
35
|
+
lfss/eng/database.py,sha256=c-hMyPmF6G-_fPkaR53KVNKyKCiPYLcOOekXlK8vIBE,56014
|
35
36
|
lfss/eng/datatype.py,sha256=27UB7-l9SICy5lAvKjdzpTL_GohZjzstQcr9PtAq7nM,2709
|
36
37
|
lfss/eng/error.py,sha256=JGf5NV-f4rL6tNIDSAx5-l9MG8dEj7F2w_MuOjj1d1o,732
|
37
|
-
lfss/eng/log.py,sha256=
|
38
|
+
lfss/eng/log.py,sha256=yciFQ7Utz1AItNekS4YtdP6bM7i1krA6qSAU2wVQv24,7698
|
38
39
|
lfss/eng/thumb.py,sha256=AFyWEkkpuCKGWOB9bLlaDwPKzQ9JtCSSmHMhX2Gu3CI,3096
|
39
|
-
lfss/eng/utils.py,sha256=
|
40
|
+
lfss/eng/utils.py,sha256=jQUJWWmzOPmXdTCId2Y307m1cZfB4hpzHcTjO0mkOrU,6683
|
40
41
|
lfss/sql/init.sql,sha256=FBmVzkNjYUnWjEELRFzf7xb50GngmzmeDVffT1Uk8u8,1625
|
41
42
|
lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
|
42
43
|
lfss/svc/app.py,sha256=r1KUO3sPaaJWbkJF0bcVTD7arPKLs2jFlq52Ixicomo,220
|
43
|
-
lfss/svc/app_base.py,sha256=
|
44
|
-
lfss/svc/app_dav.py,sha256=
|
45
|
-
lfss/svc/app_native.py,sha256=
|
46
|
-
lfss/svc/common_impl.py,sha256=
|
44
|
+
lfss/svc/app_base.py,sha256=s5ieQVI4BT0CBYavRx0dyBqwts7PYnjyCovHNYPHul8,7014
|
45
|
+
lfss/svc/app_dav.py,sha256=H3aL3MEdYaPK1w3FQvTzrGYGaaow4m8LZ7R35MN351A,18238
|
46
|
+
lfss/svc/app_native.py,sha256=zQM9o6HKtMVpq2WDZ77oc8tRnBdUdTkYHeV92NonGoo,10601
|
47
|
+
lfss/svc/common_impl.py,sha256=7QflWnxRqghLOSMpDz2UCRqEn49X1GLS3agCb5msia8,13729
|
47
48
|
lfss/svc/request_log.py,sha256=v8yXEIzPjaksu76Oh5vgdbUEUrw8Kt4etLAXBWSGie8,3207
|
48
|
-
lfss-0.11.
|
49
|
-
lfss-0.11.
|
50
|
-
lfss-0.11.
|
51
|
-
lfss-0.11.
|
49
|
+
lfss-0.11.3.dist-info/METADATA,sha256=OtKMj9dIs-FEgxYIPIwFTFGg1fWoTdnEc2LHyWFIgo0,2732
|
50
|
+
lfss-0.11.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
51
|
+
lfss-0.11.3.dist-info/entry_points.txt,sha256=R4uOP1y6eD0Qp3j1ySA8kRPVMdt6_W_9o-Zj9Ra4D0A,232
|
52
|
+
lfss-0.11.3.dist-info/RECORD,,
|
File without changes
|