lfss 0.2.4__py3-none-any.whl → 0.3.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 +6 -6
- frontend/scripts.js +4 -1
- lfss/client/__init__.py +0 -0
- lfss/client/api.py +91 -0
- lfss/src/config.py +2 -1
- lfss/src/database.py +21 -14
- lfss/src/server.py +11 -8
- {lfss-0.2.4.dist-info → lfss-0.3.0.dist-info}/METADATA +7 -7
- {lfss-0.2.4.dist-info → lfss-0.3.0.dist-info}/RECORD +11 -9
- {lfss-0.2.4.dist-info → lfss-0.3.0.dist-info}/WHEEL +0 -0
- {lfss-0.2.4.dist-info → lfss-0.3.0.dist-info}/entry_points.txt +0 -0
Readme.md
CHANGED
@@ -5,7 +5,7 @@ A lightweight file/object storage service!
|
|
5
5
|
|
6
6
|
Usage:
|
7
7
|
```sh
|
8
|
-
pip install
|
8
|
+
pip install lfss
|
9
9
|
lfss-user add <username> <password>
|
10
10
|
lfss-serve
|
11
11
|
```
|
@@ -13,15 +13,15 @@ lfss-serve
|
|
13
13
|
By default, the data will be stored in `.storage_data`.
|
14
14
|
You can change storage directory using the `LFSS_DATA` environment variable.
|
15
15
|
|
16
|
-
I provide a simple client to interact with the service
|
17
|
-
Just start a web server at `/frontend` and open `index.html` in your browser, or use:
|
16
|
+
I provide a simple client to interact with the service:
|
18
17
|
```sh
|
19
|
-
lfss-panel
|
18
|
+
lfss-panel --open
|
20
19
|
```
|
20
|
+
Or, you can start a web server at `/frontend` and open `index.html` in your browser.
|
21
21
|
|
22
22
|
The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/url` path.
|
23
|
-
Authentication is done via `Authorization` header
|
24
|
-
You can refer to `frontend` as an application example, and `frontend/api.js` for the API usage.
|
23
|
+
Authentication is done via `Authorization` header with the value `Bearer <token>`, or through the `token` query parameter.
|
24
|
+
You can refer to `frontend` as an application example, and `frontend/api.js` or `lfss.client.api.py` for the API usage.
|
25
25
|
|
26
26
|
By default, the service exposes all files to the public for `GET` requests,
|
27
27
|
but file-listing is restricted to the user's own files.
|
frontend/scripts.js
CHANGED
@@ -129,6 +129,9 @@ uploadButton.addEventListener('click', () => {
|
|
129
129
|
refreshFileList();
|
130
130
|
uploadFileNameInput.value = '';
|
131
131
|
onFileNameInpuChange();
|
132
|
+
},
|
133
|
+
(err) => {
|
134
|
+
showPopup('Failed to upload file: ' + err, {level: 'error', timeout: 5000});
|
132
135
|
}
|
133
136
|
);
|
134
137
|
});
|
@@ -401,7 +404,7 @@ function refreshFileList(){
|
|
401
404
|
|
402
405
|
const downloadBtn = document.createElement('a');
|
403
406
|
downloadBtn.textContent = 'Download';
|
404
|
-
downloadBtn.href = conn.config.endpoint + '/' + file.url + '?
|
407
|
+
downloadBtn.href = conn.config.endpoint + '/' + file.url + '?download=true&token=' + conn.config.token;
|
405
408
|
actContainer.appendChild(downloadBtn);
|
406
409
|
|
407
410
|
const deleteButton = document.createElement('a');
|
lfss/client/__init__.py
ADDED
File without changes
|
lfss/client/api.py
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
from typing import Optional, Literal
|
2
|
+
import os
|
3
|
+
import requests
|
4
|
+
import urllib.parse
|
5
|
+
from lfss.src.database import (
|
6
|
+
FileReadPermission, FileDBRecord, DBUserRecord, PathContents
|
7
|
+
)
|
8
|
+
|
9
|
+
_default_endpoint = os.environ.get('LFSS_ENDPOINT', 'http://localhost:8000')
|
10
|
+
_default_token = os.environ.get('LFSS_TOKEN', '')
|
11
|
+
|
12
|
+
class Connector:
|
13
|
+
def __init__(self, endpoint=_default_endpoint, token=_default_token):
|
14
|
+
assert token, "No token provided. Please set LFSS_TOKEN environment variable."
|
15
|
+
self.config = {
|
16
|
+
"endpoint": endpoint,
|
17
|
+
"token": token
|
18
|
+
}
|
19
|
+
|
20
|
+
def _fetch(
|
21
|
+
self, method: Literal['GET', 'POST', 'PUT', 'DELETE'],
|
22
|
+
path: str, search_params: dict = {}
|
23
|
+
):
|
24
|
+
if path.startswith('/'):
|
25
|
+
path = path[1:]
|
26
|
+
def f(**kwargs):
|
27
|
+
url = f"{self.config['endpoint']}/{path}" + "?" + urllib.parse.urlencode(search_params)
|
28
|
+
headers: dict = kwargs.pop('headers', {})
|
29
|
+
headers.update({
|
30
|
+
'Authorization': f"Bearer {self.config['token']}",
|
31
|
+
})
|
32
|
+
response = requests.request(method, url, headers=headers, **kwargs)
|
33
|
+
response.raise_for_status()
|
34
|
+
return response
|
35
|
+
return f
|
36
|
+
|
37
|
+
def put(self, path: str, file_data: bytes):
|
38
|
+
"""Uploads a file to the specified path."""
|
39
|
+
response = self._fetch('PUT', path)(
|
40
|
+
data=file_data,
|
41
|
+
headers={'Content-Type': 'application/octet-stream'}
|
42
|
+
)
|
43
|
+
return response.json()
|
44
|
+
|
45
|
+
def get(self, path: str) -> Optional[bytes]:
|
46
|
+
"""Downloads a file from the specified path."""
|
47
|
+
try:
|
48
|
+
response = self._fetch('GET', path)()
|
49
|
+
except requests.exceptions.HTTPError as e:
|
50
|
+
if e.response.status_code == 404:
|
51
|
+
return None
|
52
|
+
raise e
|
53
|
+
return response.content
|
54
|
+
|
55
|
+
def delete(self, path: str):
|
56
|
+
"""Deletes the file at the specified path."""
|
57
|
+
if path.startswith('/'):
|
58
|
+
path = path[1:]
|
59
|
+
self._fetch('DELETE', path)()
|
60
|
+
|
61
|
+
def get_metadata(self, path: str) -> Optional[FileDBRecord]:
|
62
|
+
"""Gets the metadata for the file at the specified path."""
|
63
|
+
try:
|
64
|
+
response = self._fetch('GET', '_api/fmeta', {'path': path})()
|
65
|
+
return FileDBRecord(**response.json())
|
66
|
+
except requests.exceptions.HTTPError as e:
|
67
|
+
if e.response.status_code == 404:
|
68
|
+
return None
|
69
|
+
raise e
|
70
|
+
|
71
|
+
def list_path(self, path: str) -> PathContents:
|
72
|
+
assert path.endswith('/')
|
73
|
+
response = self._fetch('GET', path)()
|
74
|
+
return PathContents(**response.json())
|
75
|
+
|
76
|
+
def set_file_permission(self, path: str, permission: int | FileReadPermission):
|
77
|
+
"""Sets the file permission for the specified path."""
|
78
|
+
self._fetch('POST', '_api/fmeta', {'path': path, 'perm': int(permission)})(
|
79
|
+
headers={'Content-Type': 'application/www-form-urlencoded'}
|
80
|
+
)
|
81
|
+
|
82
|
+
def move_file(self, path: str, new_path: str):
|
83
|
+
"""Moves a file to a new location."""
|
84
|
+
self._fetch('POST', '_api/fmeta', {'path': path, 'new_path': new_path})(
|
85
|
+
headers = {'Content-Type': 'application/www-form-urlencoded'}
|
86
|
+
)
|
87
|
+
|
88
|
+
def whoami(self) -> DBUserRecord:
|
89
|
+
"""Gets information about the current user."""
|
90
|
+
response = self._fetch('GET', '_api/whoami')()
|
91
|
+
return DBUserRecord(**response.json())
|
lfss/src/config.py
CHANGED
lfss/src/database.py
CHANGED
@@ -24,10 +24,7 @@ def hash_credential(username, password):
|
|
24
24
|
|
25
25
|
_atomic_lock = Lock()
|
26
26
|
def atomic(func):
|
27
|
-
"""
|
28
|
-
Ensure non-reentrancy.
|
29
|
-
Can be skipped if the function only executes a single SQL statement.
|
30
|
-
"""
|
27
|
+
""" Ensure non-reentrancy """
|
31
28
|
@wraps(func)
|
32
29
|
async def wrapper(*args, **kwargs):
|
33
30
|
async with _atomic_lock:
|
@@ -175,9 +172,11 @@ class UserConn(DBConnBase):
|
|
175
172
|
async for record in cursor:
|
176
173
|
yield self.parse_record(record)
|
177
174
|
|
175
|
+
@atomic
|
178
176
|
async def set_active(self, username: str):
|
179
177
|
await self.conn.execute("UPDATE user SET last_active = CURRENT_TIMESTAMP WHERE username = ?", (username, ))
|
180
178
|
|
179
|
+
@atomic
|
181
180
|
async def delete_user(self, username: str):
|
182
181
|
await self.conn.execute("DELETE FROM user WHERE username = ?", (username, ))
|
183
182
|
self.logger.info(f"Delete user {username}")
|
@@ -203,6 +202,11 @@ class DirectoryRecord:
|
|
203
202
|
|
204
203
|
def __str__(self):
|
205
204
|
return f"Directory {self.url} (size={self.size})"
|
205
|
+
|
206
|
+
@dataclasses.dataclass
|
207
|
+
class PathContents:
|
208
|
+
dirs: list[DirectoryRecord]
|
209
|
+
files: list[FileDBRecord]
|
206
210
|
|
207
211
|
class FileConn(DBConnBase):
|
208
212
|
|
@@ -251,7 +255,7 @@ class FileConn(DBConnBase):
|
|
251
255
|
async with self.conn.execute("SELECT SUM(file_size) FROM fmeta WHERE owner_id = ?", (r[0], )) as cursor:
|
252
256
|
size = await cursor.fetchone()
|
253
257
|
if size is not None and size[0] is not None:
|
254
|
-
await self.
|
258
|
+
await self._user_size_inc(r[0], size[0])
|
255
259
|
|
256
260
|
return self
|
257
261
|
|
@@ -299,9 +303,9 @@ class FileConn(DBConnBase):
|
|
299
303
|
@overload
|
300
304
|
async def list_path(self, url: str, flat: Literal[True]) -> list[FileDBRecord]:...
|
301
305
|
@overload
|
302
|
-
async def list_path(self, url: str, flat: Literal[False]) ->
|
306
|
+
async def list_path(self, url: str, flat: Literal[False]) -> PathContents:...
|
303
307
|
|
304
|
-
async def list_path(self, url: str, flat: bool = False) -> list[FileDBRecord] |
|
308
|
+
async def list_path(self, url: str, flat: bool = False) -> list[FileDBRecord] | PathContents:
|
305
309
|
"""
|
306
310
|
List all files and directories under the given path,
|
307
311
|
if flat is True, return a list of FileDBRecord, recursively including all subdirectories.
|
@@ -318,7 +322,7 @@ class FileConn(DBConnBase):
|
|
318
322
|
return [self.parse_record(r) for r in res]
|
319
323
|
|
320
324
|
else:
|
321
|
-
return (await self.list_root(), [])
|
325
|
+
return PathContents(await self.list_root(), [])
|
322
326
|
|
323
327
|
if flat:
|
324
328
|
async with self.conn.execute("SELECT * FROM fmeta WHERE url LIKE ?", (url + '%', )) as cursor:
|
@@ -346,7 +350,7 @@ class FileConn(DBConnBase):
|
|
346
350
|
dirs_str = [r[0] + '/' for r in res if r[0] != '/']
|
347
351
|
dirs = [DirectoryRecord(url + d, await self.path_size(url + d, include_subpath=True)) for d in dirs_str]
|
348
352
|
|
349
|
-
return (dirs, files)
|
353
|
+
return PathContents(dirs, files)
|
350
354
|
|
351
355
|
async def user_size(self, user_id: int) -> int:
|
352
356
|
async with self.conn.execute("SELECT size FROM usize WHERE user_id = ?", (user_id, )) as cursor:
|
@@ -354,10 +358,10 @@ class FileConn(DBConnBase):
|
|
354
358
|
if res is None:
|
355
359
|
return -1
|
356
360
|
return res[0]
|
357
|
-
async def
|
361
|
+
async def _user_size_inc(self, user_id: int, inc: int):
|
358
362
|
self.logger.debug(f"Increasing user {user_id} size by {inc}")
|
359
363
|
await self.conn.execute("INSERT OR REPLACE INTO usize (user_id, size) VALUES (?, COALESCE((SELECT size FROM usize WHERE user_id = ?), 0) + ?)", (user_id, user_id, inc))
|
360
|
-
async def
|
364
|
+
async def _user_size_dec(self, user_id: int, dec: int):
|
361
365
|
self.logger.debug(f"Decreasing user {user_id} size by {dec}")
|
362
366
|
await self.conn.execute("INSERT OR REPLACE INTO usize (user_id, size) VALUES (?, COALESCE((SELECT size FROM usize WHERE user_id = ?), 0) - ?)", (user_id, user_id, dec))
|
363
367
|
|
@@ -406,7 +410,7 @@ class FileConn(DBConnBase):
|
|
406
410
|
"INSERT INTO fmeta (url, owner_id, file_id, file_size, permission) VALUES (?, ?, ?, ?, ?)",
|
407
411
|
(url, owner_id, file_id, file_size, int(permission))
|
408
412
|
)
|
409
|
-
await self.
|
413
|
+
await self._user_size_inc(owner_id, file_size)
|
410
414
|
self.logger.info(f"File {url} created")
|
411
415
|
|
412
416
|
@atomic
|
@@ -428,7 +432,7 @@ class FileConn(DBConnBase):
|
|
428
432
|
file_record = await self.get_file_record(url)
|
429
433
|
if file_record is None: return
|
430
434
|
await self.conn.execute("DELETE FROM fmeta WHERE url = ?", (url, ))
|
431
|
-
await self.
|
435
|
+
await self._user_size_dec(file_record.owner_id, file_record.file_size)
|
432
436
|
self.logger.info(f"Deleted fmeta {url}")
|
433
437
|
|
434
438
|
@atomic
|
@@ -452,11 +456,12 @@ class FileConn(DBConnBase):
|
|
452
456
|
async with self.conn.execute("SELECT SUM(file_size) FROM fmeta WHERE owner_id = ? AND url LIKE ?", (r[0], path + '%')) as cursor:
|
453
457
|
size = await cursor.fetchone()
|
454
458
|
if size is not None:
|
455
|
-
await self.
|
459
|
+
await self._user_size_dec(r[0], size[0])
|
456
460
|
|
457
461
|
await self.conn.execute("DELETE FROM fmeta WHERE url LIKE ?", (path + '%', ))
|
458
462
|
self.logger.info(f"Deleted {len(all_f_rec)} files for path {path}") # type: ignore
|
459
463
|
|
464
|
+
@atomic
|
460
465
|
async def set_file_blob(self, file_id: str, blob: bytes):
|
461
466
|
await self.conn.execute("INSERT OR REPLACE INTO fdata (file_id, data) VALUES (?, ?)", (file_id, blob))
|
462
467
|
|
@@ -467,9 +472,11 @@ class FileConn(DBConnBase):
|
|
467
472
|
return None
|
468
473
|
return res[0]
|
469
474
|
|
475
|
+
@atomic
|
470
476
|
async def delete_file_blob(self, file_id: str):
|
471
477
|
await self.conn.execute("DELETE FROM fdata WHERE file_id = ?", (file_id, ))
|
472
478
|
|
479
|
+
@atomic
|
473
480
|
async def delete_file_blobs(self, file_ids: list[str]):
|
474
481
|
await self.conn.execute("DELETE FROM fdata WHERE file_id IN ({})".format(','.join(['?'] * len(file_ids))), file_ids)
|
475
482
|
|
lfss/src/server.py
CHANGED
@@ -13,7 +13,7 @@ from contextlib import asynccontextmanager
|
|
13
13
|
|
14
14
|
from .error import *
|
15
15
|
from .log import get_logger
|
16
|
-
from .config import MAX_BUNDLE_BYTES
|
16
|
+
from .config import MAX_BUNDLE_BYTES, MAX_FILE_BYTES
|
17
17
|
from .utils import ensure_uri_compnents
|
18
18
|
from .database import Database, DBUserRecord, DECOY_USER, FileDBRecord, check_user_permission, FileReadPermission
|
19
19
|
|
@@ -78,7 +78,7 @@ app.add_middleware(
|
|
78
78
|
router_fs = APIRouter(prefix="")
|
79
79
|
|
80
80
|
@router_fs.get("/{path:path}")
|
81
|
-
async def get_file(path: str,
|
81
|
+
async def get_file(path: str, download = False, user: DBUserRecord = Depends(get_current_user)):
|
82
82
|
path = ensure_uri_compnents(path)
|
83
83
|
|
84
84
|
# handle directory query
|
@@ -97,11 +97,7 @@ async def get_file(path: str, asfile = False, user: DBUserRecord = Depends(get_c
|
|
97
97
|
if not path.startswith(f"{user.username}/") and not user.is_admin:
|
98
98
|
raise HTTPException(status_code=403, detail="Permission denied, path must start with username")
|
99
99
|
|
100
|
-
|
101
|
-
return {
|
102
|
-
"dirs": dirs,
|
103
|
-
"files": files
|
104
|
-
}
|
100
|
+
return await conn.file.list_path(path, flat = False)
|
105
101
|
|
106
102
|
file_record = await conn.file.get_file_record(path)
|
107
103
|
if not file_record:
|
@@ -128,7 +124,7 @@ async def get_file(path: str, asfile = False, user: DBUserRecord = Depends(get_c
|
|
128
124
|
}
|
129
125
|
)
|
130
126
|
|
131
|
-
if
|
127
|
+
if download:
|
132
128
|
return await send('application/octet-stream', "attachment")
|
133
129
|
else:
|
134
130
|
return await send(None, "inline")
|
@@ -143,6 +139,13 @@ async def put_file(request: Request, path: str, user: DBUserRecord = Depends(get
|
|
143
139
|
logger.debug(f"Reject put request from {user.username} to {path}")
|
144
140
|
raise HTTPException(status_code=403, detail="Permission denied")
|
145
141
|
|
142
|
+
content_length = request.headers.get("Content-Length")
|
143
|
+
if content_length is not None:
|
144
|
+
content_length = int(content_length)
|
145
|
+
if content_length > MAX_FILE_BYTES:
|
146
|
+
logger.debug(f"Reject put request from {user.username} to {path}, file too large")
|
147
|
+
raise HTTPException(status_code=413, detail="File too large")
|
148
|
+
|
146
149
|
logger.info(f"PUT {path}, user: {user.username}")
|
147
150
|
exists_flag = False
|
148
151
|
file_record = await conn.file.get_file_record(path)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lfss
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Lightweight file storage service
|
5
5
|
Home-page: https://github.com/MenxLi/lfss
|
6
6
|
Author: li, mengxun
|
@@ -24,7 +24,7 @@ A lightweight file/object storage service!
|
|
24
24
|
|
25
25
|
Usage:
|
26
26
|
```sh
|
27
|
-
pip install
|
27
|
+
pip install lfss
|
28
28
|
lfss-user add <username> <password>
|
29
29
|
lfss-serve
|
30
30
|
```
|
@@ -32,15 +32,15 @@ lfss-serve
|
|
32
32
|
By default, the data will be stored in `.storage_data`.
|
33
33
|
You can change storage directory using the `LFSS_DATA` environment variable.
|
34
34
|
|
35
|
-
I provide a simple client to interact with the service
|
36
|
-
Just start a web server at `/frontend` and open `index.html` in your browser, or use:
|
35
|
+
I provide a simple client to interact with the service:
|
37
36
|
```sh
|
38
|
-
lfss-panel
|
37
|
+
lfss-panel --open
|
39
38
|
```
|
39
|
+
Or, you can start a web server at `/frontend` and open `index.html` in your browser.
|
40
40
|
|
41
41
|
The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/url` path.
|
42
|
-
Authentication is done via `Authorization` header
|
43
|
-
You can refer to `frontend` as an application example, and `frontend/api.js` for the API usage.
|
42
|
+
Authentication is done via `Authorization` header with the value `Bearer <token>`, or through the `token` query parameter.
|
43
|
+
You can refer to `frontend` as an application example, and `frontend/api.js` or `lfss.client.api.py` for the API usage.
|
44
44
|
|
45
45
|
By default, the service exposes all files to the public for `GET` requests,
|
46
46
|
but file-listing is restricted to the user's own files.
|
@@ -1,24 +1,26 @@
|
|
1
|
-
Readme.md,sha256=
|
1
|
+
Readme.md,sha256=8ZAADV1K20gEDhwiL-Qq_rBAePlwWQn8c5uJ_X7OCIg,1128
|
2
2
|
docs/Known_issues.md,sha256=rfdG3j1OJF-59S9E06VPyn0nZKbW-ybPxkoZ7MEZWp8,81
|
3
3
|
docs/Permission.md,sha256=EY1Y4tT5PDdHDb2pXsVgMAJXxkUihTZfPT0_z7Q53FA,1485
|
4
4
|
frontend/api.js,sha256=D_MaQlmBGzzSR30a23Kh3DxPeF8MpKYe5uXSb9srRTU,7281
|
5
5
|
frontend/index.html,sha256=JP6Sd-1JdlEfWQ4fjmSs-CrNw-2iq1RlS55SuXJq5lg,2019
|
6
6
|
frontend/popup.css,sha256=VzkjG1ZTLxhHMtTyobnlvqYmVsTmdbJJed2Pu1cc06c,1007
|
7
7
|
frontend/popup.js,sha256=dH5n7C2Vo9gCsMfQ4ajL4D1ETl0Wk9rIldxUb7x0f_c,4634
|
8
|
-
frontend/scripts.js,sha256=
|
8
|
+
frontend/scripts.js,sha256=lp5EalD0Ikpy4Tw5dhEORsOB_44Z88I4gJI4e8C1SDE,18175
|
9
9
|
frontend/styles.css,sha256=Ql_-W5dbh6LlJL2Kez8qsqPSF2fckFgmOYP4SMfKJVs,4065
|
10
10
|
frontend/utils.js,sha256=biE2te5ezswZyuwlDTYbHEt7VWKfCcUrusNt1lHjkLw,2263
|
11
11
|
lfss/cli/panel.py,sha256=iGdVmdWYjA_7a78ZzWEB_3ggIOBeUKTzg6F5zLaB25c,1401
|
12
12
|
lfss/cli/serve.py,sha256=bO3GT0kuylMGN-7bZWP4e71MlugGZ_lEMkYaYld_Ntg,985
|
13
13
|
lfss/cli/user.py,sha256=906MIQO1mr4XXzPpT8Kfri1G61bWlgjDzIglxypQ7z0,3251
|
14
|
+
lfss/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
+
lfss/client/api.py,sha256=SSrs1rafALmK_Pc7MfqeQm0El1rcGc-dXP8H6XMpmrY,3455
|
14
16
|
lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
-
lfss/src/config.py,sha256=
|
16
|
-
lfss/src/database.py,sha256=
|
17
|
+
lfss/src/config.py,sha256=mc8QoiWoPgiZ5JnSvKBH8jWbp5uLSyG3l5boDiQPXS0,325
|
18
|
+
lfss/src/database.py,sha256=6LDXaYO_dFuR8KrOPcvXS1_-sszFwvpyhbXaS2MTpq4,27576
|
17
19
|
lfss/src/error.py,sha256=S5ui3tJ0uKX4EZnt2Db-KbDmJXlECI_OY95cNMkuegc,218
|
18
20
|
lfss/src/log.py,sha256=7mRHFwhx7GKtm_cRryoEIlRQhHTLQC3Qd-N81YsoKao,5174
|
19
|
-
lfss/src/server.py,sha256=
|
21
|
+
lfss/src/server.py,sha256=BeJ15QxN66k7UETCkUe03NDIIKWYdGYRtOMjox_CxIQ,11872
|
20
22
|
lfss/src/utils.py,sha256=MrjKc8W2Y7AbgVGadSNAA50tRMbGYWRrA4KUhOCwuUU,694
|
21
|
-
lfss-0.
|
22
|
-
lfss-0.
|
23
|
-
lfss-0.
|
24
|
-
lfss-0.
|
23
|
+
lfss-0.3.0.dist-info/METADATA,sha256=xTlFyG26uh5X1Vk0PDGj6tPW_6ExBgMEQ3AK3SCyYLQ,1787
|
24
|
+
lfss-0.3.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
25
|
+
lfss-0.3.0.dist-info/entry_points.txt,sha256=nUIJhenyZbcymvPVhrzV7SAtRhd7O52DflbRrpQUC04,110
|
26
|
+
lfss-0.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|