lfss 0.7.15__py3-none-any.whl → 0.8.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.
lfss/src/server.py CHANGED
@@ -16,9 +16,13 @@ from .error import *
16
16
  from .log import get_logger
17
17
  from .stat import RequestDB
18
18
  from .config import MAX_BUNDLE_BYTES, MAX_FILE_BYTES, LARGE_FILE_BYTES, CHUNK_SIZE
19
- from .utils import ensure_uri_compnents, format_last_modified, now_stamp
19
+ from .utils import ensure_uri_compnents, format_last_modified, now_stamp, wait_for_debounce_tasks
20
20
  from .connection_pool import global_connection_init, global_connection_close, unique_cursor
21
- from .database import Database, UserRecord, DECOY_USER, FileRecord, check_user_permission, FileReadPermission, UserConn, FileConn, PathContents
21
+ from .database import Database, DECOY_USER, check_user_permission, UserConn, FileConn, delayed_log_activity
22
+ from .datatype import (
23
+ FileReadPermission, FileRecord, UserRecord, PathContents,
24
+ FileSortKey, DirSortKey
25
+ )
22
26
  from .thumb import get_thumb
23
27
 
24
28
  logger = get_logger("server", term_level="DEBUG")
@@ -35,6 +39,7 @@ async def lifespan(app: FastAPI):
35
39
  yield
36
40
  await req_conn.commit()
37
41
  finally:
42
+ await wait_for_debounce_tasks()
38
43
  await asyncio.gather(req_conn.close(), global_connection_close())
39
44
 
40
45
  def handle_exception(fn):
@@ -49,6 +54,7 @@ def handle_exception(fn):
49
54
  if isinstance(e, InvalidPathError): raise HTTPException(status_code=400, detail=str(e))
50
55
  if isinstance(e, FileNotFoundError): raise HTTPException(status_code=404, detail=str(e))
51
56
  if isinstance(e, FileExistsError): raise HTTPException(status_code=409, detail=str(e))
57
+ if isinstance(e, TooManyItemsError): raise HTTPException(status_code=400, detail=str(e))
52
58
  logger.error(f"Uncaptured error in {fn.__name__}: {e}")
53
59
  raise
54
60
  return wrapper
@@ -182,7 +188,7 @@ async def emit_file(
182
188
  @handle_exception
183
189
  async def get_file(
184
190
  path: str,
185
- download: bool = False, flat: bool = False, thumb: bool = False,
191
+ download: bool = False, thumb: bool = False,
186
192
  user: UserRecord = Depends(get_current_user)
187
193
  ):
188
194
  path = ensure_uri_compnents(path)
@@ -199,8 +205,6 @@ async def get_file(
199
205
  return await emit_thumbnail(path, download, create_time=None)
200
206
 
201
207
  if path == "/":
202
- if flat:
203
- raise HTTPException(status_code=400, detail="Flat query not supported for root path")
204
208
  return PathContents(
205
209
  dirs = await fconn.list_root_dirs(user.username, skim=True) \
206
210
  if not user.is_admin else await fconn.list_root_dirs(skim=True),
@@ -210,7 +214,7 @@ async def get_file(
210
214
  if not path.startswith(f"{user.username}/") and not user.is_admin:
211
215
  raise HTTPException(status_code=403, detail="Permission denied, path must start with username")
212
216
 
213
- return await fconn.list_path(path, flat = flat)
217
+ return await fconn.list_path(path)
214
218
 
215
219
  # handle file query
216
220
  async with unique_cursor() as conn:
@@ -332,7 +336,7 @@ async def delete_file(path: str, user: UserRecord = Depends(registered_user)):
332
336
  else:
333
337
  res = await db.delete_file(path, user if not user.is_admin else None)
334
338
 
335
- await db.record_user_activity(user.username)
339
+ await delayed_log_activity(user.username)
336
340
  if res:
337
341
  return Response(status_code=200, content="Deleted")
338
342
  else:
@@ -366,7 +370,10 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
366
370
 
367
371
  async with unique_cursor() as conn:
368
372
  fconn = FileConn(conn)
369
- files = (await fconn.list_path(path, flat = True)).files
373
+ files = await fconn.list_path_files(
374
+ url = path, flat = True,
375
+ limit=(await fconn.count_path_files(url = path, flat = True))
376
+ )
370
377
  files = [f for f in files if await is_access_granted(f)]
371
378
  if len(files) == 0:
372
379
  raise HTTPException(status_code=404, detail="No files found")
@@ -421,7 +428,7 @@ async def update_file_meta(
421
428
  path = ensure_uri_compnents(path)
422
429
  if path.startswith("/"):
423
430
  path = path[1:]
424
- await db.record_user_activity(user.username)
431
+ await delayed_log_activity(user.username)
425
432
 
426
433
  # file
427
434
  if not path.endswith("/"):
@@ -448,6 +455,57 @@ async def update_file_meta(
448
455
  await db.move_path(user, path, new_path)
449
456
 
450
457
  return Response(status_code=200, content="OK")
458
+
459
+ async def validate_path_permission(path: str, user: UserRecord):
460
+ if not path.endswith("/"):
461
+ raise HTTPException(status_code=400, detail="Path must end with /")
462
+ if not path.startswith(f"{user.username}/") and not user.is_admin:
463
+ raise HTTPException(status_code=403, detail="Permission denied")
464
+
465
+ @router_api.get("/count-files")
466
+ async def count_files(path: str, flat: bool = False, user: UserRecord = Depends(registered_user)):
467
+ await validate_path_permission(path, user)
468
+ path = ensure_uri_compnents(path)
469
+ async with unique_cursor() as conn:
470
+ fconn = FileConn(conn)
471
+ return { "count": await fconn.count_path_files(url = path, flat = flat) }
472
+ @router_api.get("/list-files")
473
+ async def list_files(
474
+ path: str, offset: int = 0, limit: int = 1000,
475
+ order_by: FileSortKey = "", order_desc: bool = False,
476
+ flat: bool = False, user: UserRecord = Depends(registered_user)
477
+ ):
478
+ await validate_path_permission(path, user)
479
+ path = ensure_uri_compnents(path)
480
+ async with unique_cursor() as conn:
481
+ fconn = FileConn(conn)
482
+ return await fconn.list_path_files(
483
+ url = path, offset = offset, limit = limit,
484
+ order_by=order_by, order_desc=order_desc,
485
+ flat=flat
486
+ )
487
+
488
+ @router_api.get("/count-dirs")
489
+ async def count_dirs(path: str, user: UserRecord = Depends(registered_user)):
490
+ await validate_path_permission(path, user)
491
+ path = ensure_uri_compnents(path)
492
+ async with unique_cursor() as conn:
493
+ fconn = FileConn(conn)
494
+ return { "count": await fconn.count_path_dirs(url = path) }
495
+ @router_api.get("/list-dirs")
496
+ async def list_dirs(
497
+ path: str, offset: int = 0, limit: int = 1000,
498
+ order_by: DirSortKey = "", order_desc: bool = False,
499
+ skim: bool = True, user: UserRecord = Depends(registered_user)
500
+ ):
501
+ await validate_path_permission(path, user)
502
+ path = ensure_uri_compnents(path)
503
+ async with unique_cursor() as conn:
504
+ fconn = FileConn(conn)
505
+ return await fconn.list_path_dirs(
506
+ url = path, offset = offset, limit = limit,
507
+ order_by=order_by, order_desc=order_desc, skim=skim
508
+ )
451
509
 
452
510
  @router_api.get("/whoami")
453
511
  @handle_exception
lfss/src/stat.py CHANGED
@@ -42,7 +42,7 @@ class RequestDB:
42
42
  async def commit(self):
43
43
  await self.conn.commit()
44
44
 
45
- @debounce_async(0.05)
45
+ @debounce_async()
46
46
  async def ensure_commit_once(self):
47
47
  await self.commit()
48
48
 
lfss/src/utils.py CHANGED
@@ -1,11 +1,14 @@
1
- import datetime
1
+ import datetime, time
2
2
  import urllib.parse
3
3
  import asyncio
4
4
  import functools
5
5
  import hashlib
6
+ from asyncio import Lock
7
+ from collections import OrderedDict
6
8
  from concurrent.futures import ThreadPoolExecutor
7
9
  from typing import TypeVar, Callable, Awaitable
8
10
  from functools import wraps, partial
11
+ from uuid import uuid4
9
12
  import os
10
13
 
11
14
  def hash_credential(username: str, password: str):
@@ -25,25 +28,56 @@ def ensure_uri_compnents(path: str):
25
28
  """ Ensure the path components are safe to use """
26
29
  return encode_uri_compnents(decode_uri_compnents(path))
27
30
 
28
- def debounce_async(delay: float = 0):
29
- """ Debounce the async procedure """
31
+ g_debounce_tasks: OrderedDict[str, asyncio.Task] = OrderedDict()
32
+ lock_debounce_task_queue = Lock()
33
+ async def wait_for_debounce_tasks():
34
+ async def stop_task(task: asyncio.Task):
35
+ task.cancel()
36
+ try:
37
+ await task
38
+ except asyncio.CancelledError:
39
+ pass
40
+ await asyncio.gather(*map(stop_task, g_debounce_tasks.values()))
41
+ g_debounce_tasks.clear()
42
+
43
+ def debounce_async(delay: float = 0.1, max_wait: float = 1.):
44
+ """
45
+ Debounce the async procedure,
46
+ ensuring execution at least once every `max_wait` seconds.
47
+ """
30
48
  def debounce_wrap(func):
31
- # https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel
49
+ task_record: tuple[str, asyncio.Task] | None = None
50
+ last_execution_time = 0
51
+
32
52
  async def delayed_func(*args, **kwargs):
53
+ nonlocal last_execution_time
33
54
  await asyncio.sleep(delay)
34
55
  await func(*args, **kwargs)
56
+ last_execution_time = time.monotonic()
35
57
 
36
- task_record: asyncio.Task | None = None
37
58
  @functools.wraps(func)
38
59
  async def wrapper(*args, **kwargs):
39
- nonlocal task_record
40
- if task_record is not None:
41
- task_record.cancel()
42
- task_record = asyncio.create_task(delayed_func(*args, **kwargs))
43
- try:
44
- await task_record
45
- except asyncio.CancelledError:
46
- pass
60
+ nonlocal task_record, last_execution_time
61
+
62
+ async with lock_debounce_task_queue:
63
+ if task_record is not None:
64
+ task_record[1].cancel()
65
+ g_debounce_tasks.pop(task_record[0], None)
66
+
67
+ if time.monotonic() - last_execution_time > max_wait:
68
+ await func(*args, **kwargs)
69
+ last_execution_time = time.monotonic()
70
+ return
71
+
72
+ task = asyncio.create_task(delayed_func(*args, **kwargs))
73
+ task_uid = uuid4().hex
74
+ task_record = (task_uid, task)
75
+ async with lock_debounce_task_queue:
76
+ g_debounce_tasks[task_uid] = task
77
+ if len(g_debounce_tasks) > 2048:
78
+ # finished tasks are not removed from the dict
79
+ # so we need to clear it periodically
80
+ await wait_for_debounce_tasks()
47
81
  return wrapper
48
82
  return debounce_wrap
49
83
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.7.15
3
+ Version: 0.8.0
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -16,6 +16,7 @@ Requires-Dist: aiosqlite (==0.*)
16
16
  Requires-Dist: fastapi (==0.*)
17
17
  Requires-Dist: mimesniff (==1.*)
18
18
  Requires-Dist: pillow
19
+ Requires-Dist: requests (==2.*)
19
20
  Requires-Dist: uvicorn (==0.*)
20
21
  Project-URL: Repository, https://github.com/MenxLi/lfss
21
22
  Description-Content-Type: text/markdown
@@ -23,7 +24,7 @@ Description-Content-Type: text/markdown
23
24
  # Lightweight File Storage Service (LFSS)
24
25
  [![PyPI](https://img.shields.io/pypi/v/lfss)](https://pypi.org/project/lfss/)
25
26
 
26
- My experiment on a lightweight file/object storage service.
27
+ My experiment on a lightweight and high-performance file/object storage service.
27
28
  It stores small files and metadata in sqlite, large files in the filesystem.
28
29
  Tested on 2 million files, and it works fine...
29
30
 
@@ -45,7 +46,7 @@ Or, you can start a web server at `/frontend` and open `index.html` in your brow
45
46
 
46
47
  The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/url` path.
47
48
  Authentication is done via `Authorization` header with the value `Bearer <token>`, or through the `token` query parameter.
48
- You can refer to `frontend` as an application example, and `frontend/api.js` or `lfss/client/api.py` for the API usage.
49
+ You can refer to `frontend` as an application example, and `frontend/api.js` or `lfss/api/connector.py` for the API usage.
49
50
 
50
51
  By default, the service exposes all files to the public for `GET` requests,
51
52
  but file-listing is restricted to the user's own files.
@@ -0,0 +1,43 @@
1
+ Readme.md,sha256=LpbTvUWjCOv4keMNDrZvEnNAmCQnvaxvlq2srWixXn0,1299
2
+ docs/Known_issues.md,sha256=rfdG3j1OJF-59S9E06VPyn0nZKbW-ybPxkoZ7MEZWp8,81
3
+ docs/Permission.md,sha256=9r9nEmhqfz18RTS8FI0fZ9F0a31r86OoAyx3EQxxpk0,2317
4
+ frontend/api.js,sha256=9cR8ddaoF-ulauQ1tcISV2nmfakkplC7uS8-lWFqU58,15820
5
+ frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
6
+ frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
7
+ frontend/info.js,sha256=WhOGaeqMoezEAfg4nIpK26hvejC7AZ-ZDLiJmRj0kDk,5758
8
+ frontend/login.css,sha256=VMM0QfbDFYerxKWKSGhMI1yg5IRBXg0TTdLJEEhQZNk,355
9
+ frontend/login.js,sha256=QoO8yKmBHDVP-ZomCMOaV7xVUVIhpl7esJrb6T5aHQE,2466
10
+ frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
11
+ frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
12
+ frontend/scripts.js,sha256=PFgAZC-frV-qs6sYCb1ZzH_CAa7in17qbfqYiaIxNmA,21697
13
+ frontend/state.js,sha256=vbNL5DProRKmSEY7xu9mZH6IY0PBenF8WGxPtGgDnLI,1680
14
+ frontend/styles.css,sha256=xcNLqI3KBsY5TLnku8UIP0Jfr7QLajr1_KNlZj9eheM,4935
15
+ frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
16
+ frontend/thumb.js,sha256=RQ_whXNwmkdG4SEbNQGeh488YYzqwoNYDc210hPeuhQ,5703
17
+ frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
18
+ lfss/api/__init__.py,sha256=YI1_9nyW0E5lyXn_PmmIzIff1ccBC-KCA0twpsKDRIY,5453
19
+ lfss/api/connector.py,sha256=q9CJBOmN83tfpwI1IclSzq_lzI4Kq1SOKC3S5H-vuWo,9301
20
+ lfss/cli/balance.py,sha256=R2rbO2tg9TVnnQIVeU0GJVeMS-5LDhEdk4mbOE9qGq0,4121
21
+ lfss/cli/cli.py,sha256=8VKe41m_LhVSFxGlvgBxdz55sjscLNbbkNX1fOnmES4,4618
22
+ lfss/cli/panel.py,sha256=iGdVmdWYjA_7a78ZzWEB_3ggIOBeUKTzg6F5zLaB25c,1401
23
+ lfss/cli/serve.py,sha256=T-jz_PJcaY9FJRRfyWV9MbjDI73YdOOCn4Nr2PO-s0c,993
24
+ lfss/cli/user.py,sha256=wlR-xcJKCtr_y5QgYO9GM0JyDCKooIRlsAxw2eilPfs,3418
25
+ lfss/cli/vacuum.py,sha256=4TMMYC_5yEt7jeaFTVC3iX0X6aUzYXBiCsrcIYgw_uA,3695
26
+ lfss/sql/init.sql,sha256=C-JtQAlaOjESI8uoF1Y_9dKukEVSw5Ll-7yA3gG-XHU,1210
27
+ lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
28
+ lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ lfss/src/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
30
+ lfss/src/config.py,sha256=K1b5clNRO4EzxDG-p6U5aRLDaq-Up0tDHfn_D79D0ns,857
31
+ lfss/src/connection_pool.py,sha256=4YULPcmndy33FyBSo9w1fZjAlBX2q-xPP27xJOTA06I,5228
32
+ lfss/src/database.py,sha256=yFMoxHJ-ZRwi9qvSVdIl9m_gpycmL2eencdv7P0vzwQ,35678
33
+ lfss/src/datatype.py,sha256=q2lc8BJaB2sS7gtqPMqxM625mckgI2SmvuqbadiObLY,2158
34
+ lfss/src/error.py,sha256=Bh_GUtuNsMSMIKbFreU4CfZzL96TZdNAIcgmDxvGbQ0,333
35
+ lfss/src/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
36
+ lfss/src/server.py,sha256=OcyPuVyvBCxpgF8ESxzDYNZnUJIbkOGDv-eghUSVM9E,20063
37
+ lfss/src/stat.py,sha256=WlRiiAl0rHX9oPi3thi6K4GKn70WHpClSDCt7iZUow4,3197
38
+ lfss/src/thumb.py,sha256=qjCNMpnCozMuzkhm-2uAYy1eAuYTeWG6xqs-13HX-7k,3266
39
+ lfss/src/utils.py,sha256=nal2rpr00jq1PeFhGQXkvU0FIbtRhXTj8VmbeIyRyLI,5184
40
+ lfss-0.8.0.dist-info/METADATA,sha256=uNZzNHCActK1ok2aPmZZqMXE3JxyWHStAVAH5mzpvkc,2076
41
+ lfss-0.8.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
42
+ lfss-0.8.0.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
43
+ lfss-0.8.0.dist-info/RECORD,,
@@ -1,41 +0,0 @@
1
- Readme.md,sha256=vsPotlwPAaHI5plh4aaszpi3rr7ZGDn7-wLdEYTWQ0k,1275
2
- docs/Known_issues.md,sha256=rfdG3j1OJF-59S9E06VPyn0nZKbW-ybPxkoZ7MEZWp8,81
3
- docs/Permission.md,sha256=X0VNfBKU52f93QYqcVyiBFJ3yURiSkhIo9S_5fdSgzM,2265
4
- frontend/api.js,sha256=o1sP4rKxxnM-rebxnlMlPkhPHzKaVW4kZC7B4ufbOK4,8026
5
- frontend/index.html,sha256=i45ilkRCorqXP0bTfMiwT3QmmEF23V34lZJC1nODHLo,2862
6
- frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
7
- frontend/info.js,sha256=WhOGaeqMoezEAfg4nIpK26hvejC7AZ-ZDLiJmRj0kDk,5758
8
- frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
9
- frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
10
- frontend/scripts.js,sha256=tmA_tG3bLEEqn0jTgZ-DkcEl6egxdrFYJmstb9eZMr0,21862
11
- frontend/state.js,sha256=Dda-2G4QzyqdxffjJa3Lb7rgJOrg2LvJ3TCRcB8YCrU,1327
12
- frontend/styles.css,sha256=krMo6Ulroi8pqEq1exQsFEU-FJqT9GzI8vyARiNF11k,4484
13
- frontend/thumb.css,sha256=1i8wudiMWGwdrnTqs6yrS8YaiPeHPR-A2YqUNJN20Ok,165
14
- frontend/thumb.js,sha256=6m8bscQpi2sYaLRir2ZeY1H_1ZRKFVss5P28AnLvIVQ,5486
15
- frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
16
- lfss/cli/balance.py,sha256=R2rbO2tg9TVnnQIVeU0GJVeMS-5LDhEdk4mbOE9qGq0,4121
17
- lfss/cli/cli.py,sha256=LH1nx5wI1K2DZ3hvHz7oq5HcXVDoW2V6sr7q9gJ8gqo,4621
18
- lfss/cli/panel.py,sha256=iGdVmdWYjA_7a78ZzWEB_3ggIOBeUKTzg6F5zLaB25c,1401
19
- lfss/cli/serve.py,sha256=T-jz_PJcaY9FJRRfyWV9MbjDI73YdOOCn4Nr2PO-s0c,993
20
- lfss/cli/user.py,sha256=ETLtj0N-kmxv0mhmeAsO6cY7kPq7nOOP4DetxIRoQpQ,3405
21
- lfss/cli/vacuum.py,sha256=4TMMYC_5yEt7jeaFTVC3iX0X6aUzYXBiCsrcIYgw_uA,3695
22
- lfss/client/__init__.py,sha256=kcClDdHaQowAhHFYdrFyFqHIOe5MEjfENYq1icuj3Mg,4577
23
- lfss/client/api.py,sha256=aHorSuxl79xvRRfwuitXANYoTogWKVO11XX7mdrhHTE,5807
24
- lfss/sql/init.sql,sha256=C-JtQAlaOjESI8uoF1Y_9dKukEVSw5Ll-7yA3gG-XHU,1210
25
- lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
26
- lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- lfss/src/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
28
- lfss/src/config.py,sha256=K1b5clNRO4EzxDG-p6U5aRLDaq-Up0tDHfn_D79D0ns,857
29
- lfss/src/connection_pool.py,sha256=r4Ho5d_Gd4S_KbT7515UJoiyfIgS6xyttqMsKqOfaIg,5190
30
- lfss/src/database.py,sha256=w2QPE3h1Lx0D0fUmdtu9s1XHpNp9p27zqm8AVeP2UVg,32476
31
- lfss/src/datatype.py,sha256=WfrLALU_7wei5-i_b0TxY8xWI5mwxLUHFepHSps49zA,1767
32
- lfss/src/error.py,sha256=imbhwnbhnI3HLhkbfICROe3F0gleKrOk4XnqHJDOtuI,285
33
- lfss/src/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
34
- lfss/src/server.py,sha256=igkPC3gdJoIqcVTKBAKkVPRrclXR2ZNBdRIAEci4xMo,17717
35
- lfss/src/stat.py,sha256=Wr-ug_JqtbSIf3XwQnv1xheGhsDTEOlLWuYoKO_26Jo,3201
36
- lfss/src/thumb.py,sha256=qjCNMpnCozMuzkhm-2uAYy1eAuYTeWG6xqs-13HX-7k,3266
37
- lfss/src/utils.py,sha256=TBGYvgt6xMP8UC5wTGHAr9fmdhu0_gjOtxcSeyvGyVM,3918
38
- lfss-0.7.15.dist-info/METADATA,sha256=s6jCttzD9bRauwKiKGNgrBouK5UDNEUMTgDHZBY_VfY,2021
39
- lfss-0.7.15.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
40
- lfss-0.7.15.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
41
- lfss-0.7.15.dist-info/RECORD,,
File without changes