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.
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, UserConn, FileConn
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).get_path_record(path)
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.zip_path(path, op_user=user)).getvalue()
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.zip_path_stream(path, op_user=user),
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.get_path_record(path)
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.move_path(path, new_path, user)
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.count_path_files(url = path, flat = flat) }
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.list_path_files(
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).count_path_files(path, flat=True) > 0:
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.delete_path(path, user)
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.count_path_files(dst_path, flat=True)
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.copy_path(src_path, dst_path, op_user)
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.1
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 (==23.*)
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=xaL8qBwT8B2Qe11FaOU3xWrRCh1mJ1VyTFCeFbkd0rs,570
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=QYej_hmGnv9t8wjFHXBvmrBOvY7aACZ82oa5SVkIyzM,882
7
- frontend/api.js,sha256=GlQsNoZFEcy7QUUsLbXv7aP-KxRnIxM37FQHTaakGiQ,19387
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=QoO8yKmBHDVP-ZomCMOaV7xVUVIhpl7esJrb6T5aHQE,2466
12
+ frontend/login.js,sha256=xJkulk8dlvV4BhevADLeUrnZwShiFTWv3Wg2iJFUZlY,2423
13
13
  frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
14
- frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
15
- frontend/scripts.js,sha256=OAx6o3Aabx-cE41uBABP62myZM8WbLxY37uXITMl8nY,24204
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=jqAZ7Xhlk8ZI97BRnd1dpFJcW0kPrN216xSFnrTT6zk,6069
20
+ frontend/utils.js,sha256=XP5hM_mROYaxK5dqn9qZVwv7GdQuiDzByilFskbrnxA,6068
21
21
  lfss/api/__init__.py,sha256=zT1JCiUM76wX-GtRrmKhTUzSYYfcmoyI1vYwN0fCcLw,6818
22
- lfss/api/connector.py,sha256=xl_WrvupplepZSYJs4pN9zN7GDnuZR2A8-pc08ILutI,13231
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=tPeUgj0BR_M649AGcBYwfsrGioes0qzGc0lghFkrjoo,8086
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=SciDsIdy7cfRqrXcCKBAFb9FOLyXriZBZnXlCuy6F5I,6232
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=FcTtPL7bOpg54nVL_gX-VTIjfN1cafy423ezoWGvouY,874
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=-6-IgR6hXe4ouMH8e0Ryeh2gZXJBpna1ech41sZ3UYs,53267
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=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
38
+ lfss/eng/log.py,sha256=yciFQ7Utz1AItNekS4YtdP6bM7i1krA6qSAU2wVQv24,7698
38
39
  lfss/eng/thumb.py,sha256=AFyWEkkpuCKGWOB9bLlaDwPKzQ9JtCSSmHMhX2Gu3CI,3096
39
- lfss/eng/utils.py,sha256=WYoXFFi5308UWtFC8VP792gpzrVbHZZHhP3PaFjxIEY,6770
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=bTQbz945xalyB3UZLlqVBvL6JKGNQ8Fm2KpIvvucPZQ,6850
44
- lfss/svc/app_dav.py,sha256=D0KSgjtTktPjIhyIKG5eRmBdh5X8HYFYH151E6gzlbc,18245
45
- lfss/svc/app_native.py,sha256=JbPge-F9irl26tXKAzfA5DfyjCh0Dgttflztqqrvt0A,8890
46
- lfss/svc/common_impl.py,sha256=5ZRM24zVZpAeipgDtZUVBMFtArkydlAkn17ic_XL7v8,13733
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.1.dist-info/METADATA,sha256=qXJcsBI6dboEavUMZcRuCQFLzQ8i5cUqWg5OJWrTr8k,2712
49
- lfss-0.11.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
50
- lfss-0.11.1.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
51
- lfss-0.11.1.dist-info/RECORD,,
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,,
@@ -1,6 +1,7 @@
1
1
  [console_scripts]
2
2
  lfss-balance=lfss.cli.balance:main
3
3
  lfss-cli=lfss.cli.cli:main
4
+ lfss-log=lfss.cli.log:main
4
5
  lfss-panel=lfss.cli.panel:main
5
6
  lfss-serve=lfss.cli.serve:main
6
7
  lfss-user=lfss.cli.user:main
File without changes