lfss 0.8.3__py3-none-any.whl → 0.9.0__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 +12 -4
- docs/Permission.md +38 -26
- frontend/api.js +21 -10
- frontend/thumb.js +8 -4
- lfss/api/connector.py +10 -6
- lfss/cli/cli.py +6 -10
- lfss/cli/user.py +28 -1
- lfss/sql/init.sql +9 -0
- lfss/src/connection_pool.py +22 -3
- lfss/src/database.py +184 -66
- lfss/src/datatype.py +18 -3
- lfss/src/error.py +3 -0
- lfss/src/server.py +50 -67
- lfss/src/utils.py +9 -6
- {lfss-0.8.3.dist-info → lfss-0.9.0.dist-info}/METADATA +13 -5
- {lfss-0.8.3.dist-info → lfss-0.9.0.dist-info}/RECORD +18 -18
- {lfss-0.8.3.dist-info → lfss-0.9.0.dist-info}/WHEEL +0 -0
- {lfss-0.8.3.dist-info → lfss-0.9.0.dist-info}/entry_points.txt +0 -0
lfss/src/server.py
CHANGED
@@ -16,10 +16,10 @@ from .stat import RequestDB
|
|
16
16
|
from .config import MAX_BUNDLE_BYTES, CHUNK_SIZE
|
17
17
|
from .utils import ensure_uri_compnents, format_last_modified, now_stamp, wait_for_debounce_tasks
|
18
18
|
from .connection_pool import global_connection_init, global_connection_close, unique_cursor
|
19
|
-
from .database import Database, DECOY_USER,
|
19
|
+
from .database import Database, DECOY_USER, check_file_read_permission, check_path_permission, UserConn, FileConn
|
20
20
|
from .database import delayed_log_activity, delayed_log_access
|
21
21
|
from .datatype import (
|
22
|
-
FileReadPermission, FileRecord, UserRecord, PathContents,
|
22
|
+
FileReadPermission, FileRecord, UserRecord, PathContents, AccessLevel,
|
23
23
|
FileSortKey, DirSortKey
|
24
24
|
)
|
25
25
|
from .thumb import get_thumb
|
@@ -54,6 +54,7 @@ def handle_exception(fn):
|
|
54
54
|
if isinstance(e, FileNotFoundError): raise HTTPException(status_code=404, detail=str(e))
|
55
55
|
if isinstance(e, FileExistsError): raise HTTPException(status_code=409, detail=str(e))
|
56
56
|
if isinstance(e, TooManyItemsError): raise HTTPException(status_code=400, detail=str(e))
|
57
|
+
if isinstance(e, DatabaseLockedError): raise HTTPException(status_code=503, detail=str(e))
|
57
58
|
logger.error(f"Uncaptured error in {fn.__name__}: {e}")
|
58
59
|
raise
|
59
60
|
return wrapper
|
@@ -228,39 +229,37 @@ async def get_file_impl(
|
|
228
229
|
if path == "": path = "/"
|
229
230
|
if path.endswith("/"):
|
230
231
|
# return file under the path as json
|
231
|
-
async with unique_cursor() as
|
232
|
-
fconn = FileConn(
|
232
|
+
async with unique_cursor() as cur:
|
233
|
+
fconn = FileConn(cur)
|
233
234
|
if user.id == 0:
|
234
235
|
raise HTTPException(status_code=401, detail="Permission denied, credential required")
|
235
236
|
if thumb:
|
236
237
|
return await emit_thumbnail(path, download, create_time=None)
|
237
238
|
|
238
239
|
if path == "/":
|
240
|
+
peer_users = await UserConn(cur).list_peer_users(user.id, AccessLevel.READ)
|
239
241
|
return PathContents(
|
240
|
-
dirs = await fconn.list_root_dirs(user.username, skim=True) \
|
242
|
+
dirs = await fconn.list_root_dirs(user.username, *[x.username for x in peer_users], skim=True) \
|
241
243
|
if not user.is_admin else await fconn.list_root_dirs(skim=True),
|
242
244
|
files = []
|
243
245
|
)
|
244
246
|
|
245
|
-
if not path
|
246
|
-
raise HTTPException(status_code=403, detail="Permission denied
|
247
|
+
if not await check_path_permission(path, user, cursor=cur) >= AccessLevel.READ:
|
248
|
+
raise HTTPException(status_code=403, detail="Permission denied")
|
247
249
|
|
248
250
|
return await fconn.list_path(path)
|
249
251
|
|
250
252
|
# handle file query
|
251
|
-
async with unique_cursor() as
|
252
|
-
fconn = FileConn(
|
253
|
-
file_record = await fconn.get_file_record(path)
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
allow_access, reason = check_user_permission(user, owner, file_record)
|
262
|
-
if not allow_access:
|
263
|
-
raise HTTPException(status_code=403, detail=reason)
|
253
|
+
async with unique_cursor() as cur:
|
254
|
+
fconn = FileConn(cur)
|
255
|
+
file_record = await fconn.get_file_record(path, throw=True)
|
256
|
+
uconn = UserConn(cur)
|
257
|
+
owner = await uconn.get_user_by_id(file_record.owner_id, throw=True)
|
258
|
+
|
259
|
+
if not await check_path_permission(path, user, cursor=cur) >= AccessLevel.READ:
|
260
|
+
allow_access, reason = check_file_read_permission(user, owner, file_record)
|
261
|
+
if not allow_access:
|
262
|
+
raise HTTPException(status_code=403 if user.id != 0 else 401, detail=reason)
|
264
263
|
|
265
264
|
req_range = request.headers.get("Range", None)
|
266
265
|
if not req_range is None:
|
@@ -326,17 +325,11 @@ async def put_file(
|
|
326
325
|
):
|
327
326
|
path = ensure_uri_compnents(path)
|
328
327
|
assert not path.endswith("/"), "Path must not end with /"
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
first_comp = path.split("/")[0]
|
335
|
-
async with unique_cursor() as c:
|
336
|
-
uconn = UserConn(c)
|
337
|
-
owner = await uconn.get_user(first_comp)
|
338
|
-
if not owner:
|
339
|
-
raise HTTPException(status_code=404, detail="Owner not found")
|
328
|
+
|
329
|
+
access_level = await check_path_permission(path, user)
|
330
|
+
if access_level < AccessLevel.WRITE:
|
331
|
+
logger.debug(f"Reject put request from {user.username} to {path}")
|
332
|
+
raise HTTPException(status_code=403, detail="Permission denied")
|
340
333
|
|
341
334
|
logger.info(f"PUT {path}, user: {user.username}")
|
342
335
|
exists_flag = False
|
@@ -352,7 +345,7 @@ async def put_file(
|
|
352
345
|
"Content-Type": "application/json",
|
353
346
|
}, content=json.dumps({"url": path}))
|
354
347
|
exists_flag = True
|
355
|
-
if
|
348
|
+
if await check_path_permission(path, user) < AccessLevel.WRITE:
|
356
349
|
raise HTTPException(status_code=403, detail="Permission denied, cannot overwrite other's file")
|
357
350
|
await db.delete_file(path)
|
358
351
|
|
@@ -386,17 +379,11 @@ async def post_file(
|
|
386
379
|
):
|
387
380
|
path = ensure_uri_compnents(path)
|
388
381
|
assert not path.endswith("/"), "Path must not end with /"
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
first_comp = path.split("/")[0]
|
395
|
-
async with unique_cursor() as conn:
|
396
|
-
uconn = UserConn(conn)
|
397
|
-
owner = await uconn.get_user(first_comp)
|
398
|
-
if not owner:
|
399
|
-
raise HTTPException(status_code=404, detail="Owner not found")
|
382
|
+
|
383
|
+
access_level = await check_path_permission(path, user)
|
384
|
+
if access_level < AccessLevel.WRITE:
|
385
|
+
logger.debug(f"Reject post request from {user.username} to {path}")
|
386
|
+
raise HTTPException(status_code=403, detail="Permission denied")
|
400
387
|
|
401
388
|
logger.info(f"POST {path}, user: {user.username}")
|
402
389
|
exists_flag = False
|
@@ -412,7 +399,7 @@ async def post_file(
|
|
412
399
|
"Content-Type": "application/json",
|
413
400
|
}, content=json.dumps({"url": path}))
|
414
401
|
exists_flag = True
|
415
|
-
if
|
402
|
+
if await check_path_permission(path, user) < AccessLevel.WRITE:
|
416
403
|
raise HTTPException(status_code=403, detail="Permission denied, cannot overwrite other's file")
|
417
404
|
await db.delete_file(path)
|
418
405
|
|
@@ -431,7 +418,7 @@ async def post_file(
|
|
431
418
|
@handle_exception
|
432
419
|
async def delete_file(path: str, user: UserRecord = Depends(registered_user)):
|
433
420
|
path = ensure_uri_compnents(path)
|
434
|
-
if
|
421
|
+
if await check_path_permission(path, user) < AccessLevel.WRITE:
|
435
422
|
raise HTTPException(status_code=403, detail="Permission denied")
|
436
423
|
|
437
424
|
logger.info(f"DELETE {path}, user: {user.username}")
|
@@ -458,6 +445,7 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
|
|
458
445
|
if not path == "" and path[0] == "/": # adapt to both /path and path
|
459
446
|
path = path[1:]
|
460
447
|
|
448
|
+
# TODO: may check peer users here
|
461
449
|
owner_records_cache: dict[int, UserRecord] = {} # cache owner records, ID -> UserRecord
|
462
450
|
async def is_access_granted(file_record: FileRecord):
|
463
451
|
owner_id = file_record.owner_id
|
@@ -465,11 +453,10 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
|
|
465
453
|
if owner is None:
|
466
454
|
async with unique_cursor() as conn:
|
467
455
|
uconn = UserConn(conn)
|
468
|
-
owner = await uconn.get_user_by_id(owner_id)
|
469
|
-
assert owner is not None, "Owner not found"
|
456
|
+
owner = await uconn.get_user_by_id(owner_id, throw=True)
|
470
457
|
owner_records_cache[owner_id] = owner
|
471
458
|
|
472
|
-
allow_access, _ =
|
459
|
+
allow_access, _ = check_file_read_permission(user, owner, file_record)
|
473
460
|
return allow_access
|
474
461
|
|
475
462
|
async with unique_cursor() as conn:
|
@@ -502,23 +489,20 @@ async def get_file_meta(path: str, user: UserRecord = Depends(registered_user)):
|
|
502
489
|
logger.info(f"GET meta({path}), user: {user.username}")
|
503
490
|
path = ensure_uri_compnents(path)
|
504
491
|
is_file = not path.endswith("/")
|
505
|
-
async with unique_cursor() as
|
506
|
-
fconn = FileConn(
|
492
|
+
async with unique_cursor() as cur:
|
493
|
+
fconn = FileConn(cur)
|
507
494
|
if is_file:
|
508
|
-
record = await fconn.get_file_record(path)
|
509
|
-
if
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
owner = await uconn.get_user_by_id(record.owner_id)
|
514
|
-
assert owner is not None, "Owner not found"
|
515
|
-
is_allowed, reason = check_user_permission(user, owner, record)
|
495
|
+
record = await fconn.get_file_record(path, throw=True)
|
496
|
+
if await check_path_permission(path, user, cursor=cur) < AccessLevel.READ:
|
497
|
+
uconn = UserConn(cur)
|
498
|
+
owner = await uconn.get_user_by_id(record.owner_id, throw=True)
|
499
|
+
is_allowed, reason = check_file_read_permission(user, owner, record)
|
516
500
|
if not is_allowed:
|
517
501
|
raise HTTPException(status_code=403, detail=reason)
|
518
502
|
else:
|
519
|
-
|
520
|
-
if not path.startswith(f"{user.username}/") and not user.is_admin:
|
503
|
+
if await check_path_permission(path, user, cursor=cur) < AccessLevel.READ:
|
521
504
|
raise HTTPException(status_code=403, detail="Permission denied")
|
505
|
+
record = await fconn.get_path_record(path)
|
522
506
|
return record
|
523
507
|
|
524
508
|
@router_api.post("/meta")
|
@@ -559,15 +543,14 @@ async def update_file_meta(
|
|
559
543
|
|
560
544
|
return Response(status_code=200, content="OK")
|
561
545
|
|
562
|
-
async def
|
546
|
+
async def validate_path_read_permission(path: str, user: UserRecord):
|
563
547
|
if not path.endswith("/"):
|
564
548
|
raise HTTPException(status_code=400, detail="Path must end with /")
|
565
|
-
if not path
|
549
|
+
if not await check_path_permission(path, user) >= AccessLevel.READ:
|
566
550
|
raise HTTPException(status_code=403, detail="Permission denied")
|
567
|
-
|
568
551
|
@router_api.get("/count-files")
|
569
552
|
async def count_files(path: str, flat: bool = False, user: UserRecord = Depends(registered_user)):
|
570
|
-
await
|
553
|
+
await validate_path_read_permission(path, user)
|
571
554
|
path = ensure_uri_compnents(path)
|
572
555
|
async with unique_cursor() as conn:
|
573
556
|
fconn = FileConn(conn)
|
@@ -578,7 +561,7 @@ async def list_files(
|
|
578
561
|
order_by: FileSortKey = "", order_desc: bool = False,
|
579
562
|
flat: bool = False, user: UserRecord = Depends(registered_user)
|
580
563
|
):
|
581
|
-
await
|
564
|
+
await validate_path_read_permission(path, user)
|
582
565
|
path = ensure_uri_compnents(path)
|
583
566
|
async with unique_cursor() as conn:
|
584
567
|
fconn = FileConn(conn)
|
@@ -590,7 +573,7 @@ async def list_files(
|
|
590
573
|
|
591
574
|
@router_api.get("/count-dirs")
|
592
575
|
async def count_dirs(path: str, user: UserRecord = Depends(registered_user)):
|
593
|
-
await
|
576
|
+
await validate_path_read_permission(path, user)
|
594
577
|
path = ensure_uri_compnents(path)
|
595
578
|
async with unique_cursor() as conn:
|
596
579
|
fconn = FileConn(conn)
|
@@ -601,7 +584,7 @@ async def list_dirs(
|
|
601
584
|
order_by: DirSortKey = "", order_desc: bool = False,
|
602
585
|
skim: bool = True, user: UserRecord = Depends(registered_user)
|
603
586
|
):
|
604
|
-
await
|
587
|
+
await validate_path_read_permission(path, user)
|
605
588
|
path = ensure_uri_compnents(path)
|
606
589
|
async with unique_cursor() as conn:
|
607
590
|
fconn = FileConn(conn)
|
lfss/src/utils.py
CHANGED
@@ -47,13 +47,15 @@ def debounce_async(delay: float = 0.1, max_wait: float = 1.):
|
|
47
47
|
"""
|
48
48
|
def debounce_wrap(func):
|
49
49
|
task_record: tuple[str, asyncio.Task] | None = None
|
50
|
+
fn_execution_lock = Lock()
|
50
51
|
last_execution_time = 0
|
51
52
|
|
52
53
|
async def delayed_func(*args, **kwargs):
|
53
54
|
nonlocal last_execution_time
|
54
55
|
await asyncio.sleep(delay)
|
55
|
-
|
56
|
-
|
56
|
+
async with fn_execution_lock:
|
57
|
+
await func(*args, **kwargs)
|
58
|
+
last_execution_time = time.monotonic()
|
57
59
|
|
58
60
|
@functools.wraps(func)
|
59
61
|
async def wrapper(*args, **kwargs):
|
@@ -64,10 +66,11 @@ def debounce_async(delay: float = 0.1, max_wait: float = 1.):
|
|
64
66
|
task_record[1].cancel()
|
65
67
|
g_debounce_tasks.pop(task_record[0], None)
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
async with fn_execution_lock:
|
70
|
+
if time.monotonic() - last_execution_time > max_wait:
|
71
|
+
await func(*args, **kwargs)
|
72
|
+
last_execution_time = time.monotonic()
|
73
|
+
return
|
71
74
|
|
72
75
|
task = asyncio.create_task(delayed_func(*args, **kwargs))
|
73
76
|
task_uid = uuid4().hex
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lfss
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.9.0
|
4
4
|
Summary: Lightweight file storage service
|
5
5
|
Home-page: https://github.com/MenxLi/lfss
|
6
6
|
Author: li, mengxun
|
@@ -24,9 +24,17 @@ Description-Content-Type: text/markdown
|
|
24
24
|
# Lightweight File Storage Service (LFSS)
|
25
25
|
[](https://pypi.org/project/lfss/)
|
26
26
|
|
27
|
-
My experiment on a lightweight and high-performance file/object storage service
|
27
|
+
My experiment on a lightweight and high-performance file/object storage service...
|
28
|
+
|
29
|
+
**Highlights:**
|
30
|
+
|
31
|
+
- User storage limit and access control.
|
32
|
+
- Pagination and sorted file listing for vast number of files.
|
33
|
+
- High performance: high concurrency, near-native speed on stress tests.
|
34
|
+
- Support range requests, so you can stream large files / resume download.
|
35
|
+
|
28
36
|
It stores small files and metadata in sqlite, large files in the filesystem.
|
29
|
-
Tested on 2 million files, and it
|
37
|
+
Tested on 2 million files, and it is still fast.
|
30
38
|
|
31
39
|
Usage:
|
32
40
|
```sh
|
@@ -45,8 +53,8 @@ lfss-panel --open
|
|
45
53
|
Or, you can start a web server at `/frontend` and open `index.html` in your browser.
|
46
54
|
|
47
55
|
The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/url` path.
|
48
|
-
Authentication
|
49
|
-
You can refer to `frontend` as an application example,
|
56
|
+
Authentication via `Authorization` header with the value `Bearer <token>`, or through the `token` query parameter.
|
57
|
+
You can refer to `frontend` as an application example, `lfss/api/connector.py` for more APIs.
|
50
58
|
|
51
59
|
By default, the service exposes all files to the public for `GET` requests,
|
52
60
|
but file-listing is restricted to the user's own files.
|
@@ -1,7 +1,7 @@
|
|
1
|
-
Readme.md,sha256=
|
1
|
+
Readme.md,sha256=J1tGk7B9EyIXT-RN7VGz_229UeKvZHVLpn1FvzNDxL4,1538
|
2
2
|
docs/Known_issues.md,sha256=rfdG3j1OJF-59S9E06VPyn0nZKbW-ybPxkoZ7MEZWp8,81
|
3
|
-
docs/Permission.md,sha256=
|
4
|
-
frontend/api.js,sha256=
|
3
|
+
docs/Permission.md,sha256=mvK8gVBBgoIFJqikcaReU_bUo-mTq_ECqJaDDJoQF7Q,3126
|
4
|
+
frontend/api.js,sha256=hMV6Fc1JxkFQgv7BV1Y_Su7pqsWeF_92hPMmDBcXC04,18485
|
5
5
|
frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
|
6
6
|
frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
|
7
7
|
frontend/info.js,sha256=xGUJPCSrtDhuSu0ELLQZ77PmVWldg-prU1mwQGbdEoA,5797
|
@@ -13,32 +13,32 @@ frontend/scripts.js,sha256=nWH6NgavZTVmjK44i2DeRi6mJzGSe4qeQPUbDaEVt58,21735
|
|
13
13
|
frontend/state.js,sha256=vbNL5DProRKmSEY7xu9mZH6IY0PBenF8WGxPtGgDnLI,1680
|
14
14
|
frontend/styles.css,sha256=xcNLqI3KBsY5TLnku8UIP0Jfr7QLajr1_KNlZj9eheM,4935
|
15
15
|
frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
|
16
|
-
frontend/thumb.js,sha256=
|
16
|
+
frontend/thumb.js,sha256=46ViD2TlTTWy0fx6wjoAs_5CQ4ajYB90vVzM7UO2IHw,6182
|
17
17
|
frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
|
18
18
|
lfss/api/__init__.py,sha256=c_-GokKJ_3REgve16AwgXRfFyl9mwy7__FxCIIVlk1Q,6660
|
19
|
-
lfss/api/connector.py,sha256=
|
19
|
+
lfss/api/connector.py,sha256=sSYy8gDewOosQiOzn8rvl7NsfFkIuhumHDefAVCgess,11573
|
20
20
|
lfss/cli/__init__.py,sha256=lPwPmqpa7EXQ4zlU7E7LOe6X2kw_xATGdwoHphUEirA,827
|
21
21
|
lfss/cli/balance.py,sha256=R2rbO2tg9TVnnQIVeU0GJVeMS-5LDhEdk4mbOE9qGq0,4121
|
22
|
-
lfss/cli/cli.py,sha256=
|
22
|
+
lfss/cli/cli.py,sha256=iQkZm5Ltlhw7EWM4gOv_N0vjxiteGDH_aGhh06YMPYk,8066
|
23
23
|
lfss/cli/panel.py,sha256=iGdVmdWYjA_7a78ZzWEB_3ggIOBeUKTzg6F5zLaB25c,1401
|
24
24
|
lfss/cli/serve.py,sha256=T-jz_PJcaY9FJRRfyWV9MbjDI73YdOOCn4Nr2PO-s0c,993
|
25
|
-
lfss/cli/user.py,sha256=
|
25
|
+
lfss/cli/user.py,sha256=4ynarVvSybxEQaoAzCn2dN2h6-9A61XDNQ-83-lwK4s,5364
|
26
26
|
lfss/cli/vacuum.py,sha256=4TMMYC_5yEt7jeaFTVC3iX0X6aUzYXBiCsrcIYgw_uA,3695
|
27
|
-
lfss/sql/init.sql,sha256=
|
27
|
+
lfss/sql/init.sql,sha256=8LjHx0TBCkBD62xFfssSeHDqKYVQQJkZAg4rSm046f4,1496
|
28
28
|
lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
|
29
29
|
lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
30
|
lfss/src/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
|
31
31
|
lfss/src/config.py,sha256=7k_6L_aG7vD-lVcmgpwMIg-ggpMMDNU8IbaB4y1effE,861
|
32
|
-
lfss/src/connection_pool.py,sha256
|
33
|
-
lfss/src/database.py,sha256=
|
34
|
-
lfss/src/datatype.py,sha256=
|
35
|
-
lfss/src/error.py,sha256=
|
32
|
+
lfss/src/connection_pool.py,sha256=-tePasJxiZZ73ymgWf_kFnaKouc4Rrr4K6EXwjb7Mm4,6141
|
33
|
+
lfss/src/database.py,sha256=jahoLa3kU6wGovwyfuvaMuGI8rLxcJYSfMkTT8_LI3c,41883
|
34
|
+
lfss/src/datatype.py,sha256=27UB7-l9SICy5lAvKjdzpTL_GohZjzstQcr9PtAq7nM,2709
|
35
|
+
lfss/src/error.py,sha256=oUQPkXzrlJ6Mtvm26T_3SYW_4j9unfudG3HMa1Q-JF0,421
|
36
36
|
lfss/src/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
|
37
|
-
lfss/src/server.py,sha256=
|
37
|
+
lfss/src/server.py,sha256=TMsZBt-hF4dh_-e_v5odki09S36kJ33Gi_GbtUnGQ-M,23310
|
38
38
|
lfss/src/stat.py,sha256=WlRiiAl0rHX9oPi3thi6K4GKn70WHpClSDCt7iZUow4,3197
|
39
39
|
lfss/src/thumb.py,sha256=qjCNMpnCozMuzkhm-2uAYy1eAuYTeWG6xqs-13HX-7k,3266
|
40
|
-
lfss/src/utils.py,sha256=
|
41
|
-
lfss-0.
|
42
|
-
lfss-0.
|
43
|
-
lfss-0.
|
44
|
-
lfss-0.
|
40
|
+
lfss/src/utils.py,sha256=XHBDBODU6RPpYywArmMBVnghPk3crPLrSW4cKxqiP5Q,5887
|
41
|
+
lfss-0.9.0.dist-info/METADATA,sha256=QNSYV-4VzdS4VCmKtSvjSMurXTb1qvUhvW-Wvpz3dcA,2298
|
42
|
+
lfss-0.9.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
43
|
+
lfss-0.9.0.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
|
44
|
+
lfss-0.9.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|