lfss 0.9.5__py3-none-any.whl → 0.10.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 +1 -1
- docs/Permission.md +4 -4
- lfss/api/connector.py +13 -4
- lfss/eng/connection_pool.py +11 -7
- lfss/eng/database.py +40 -28
- lfss/eng/error.py +2 -0
- lfss/svc/app_base.py +1 -0
- lfss/svc/app_native.py +25 -11
- lfss/svc/common_impl.py +1 -4
- {lfss-0.9.5.dist-info → lfss-0.10.0.dist-info}/METADATA +2 -2
- {lfss-0.9.5.dist-info → lfss-0.10.0.dist-info}/RECORD +13 -13
- {lfss-0.9.5.dist-info → lfss-0.10.0.dist-info}/WHEEL +0 -0
- {lfss-0.9.5.dist-info → lfss-0.10.0.dist-info}/entry_points.txt +0 -0
Readme.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# Lite File Storage Service (LFSS)
|
2
|
-
[](https://pypi.org/project/lfss/)
|
2
|
+
[](https://pypi.org/project/lfss/) [](https://pypi.org/project/lfss/)
|
3
3
|
|
4
4
|
My experiment on a lightweight and high-performance file/object storage service...
|
5
5
|
|
docs/Permission.md
CHANGED
@@ -22,16 +22,16 @@ A file is owned by the user who created it, may not necessarily be the user unde
|
|
22
22
|
## File access with `GET` permission
|
23
23
|
|
24
24
|
### File access
|
25
|
-
For accessing file content, the user must have `GET` permission of the file, which is determined by the `permission` field of both the owner and the file.
|
25
|
+
For accessing file content, the user must have `GET` permission of the file, which is determined by the `permission` field of both the path-owner and the file.
|
26
26
|
|
27
27
|
There are four types of permissions: `unset`, `public`, `protected`, `private`.
|
28
28
|
Non-admin users can access files based on:
|
29
29
|
|
30
30
|
- If the file is `public`, then all users can access it.
|
31
31
|
- If the file is `protected`, then only the logged-in user can access it.
|
32
|
-
- If the file is `private`, then only the owner can access it.
|
33
|
-
- If the file is `unset`, then the file's permission is inherited from the owner's permission.
|
34
|
-
- If both the owner and the file have `unset` permission, then the file is `public`.
|
32
|
+
- If the file is `private`, then only the owner/path-owner can access it.
|
33
|
+
- If the file is `unset`, then the file's permission is inherited from the path-owner's permission.
|
34
|
+
- If both the path-owner and the file have `unset` permission, then the file is `public`.
|
35
35
|
|
36
36
|
## File creation with `PUT`/`POST` permission
|
37
37
|
`PUT`/`POST` permission is not allowed for non-peer users.
|
lfss/api/connector.py
CHANGED
@@ -15,12 +15,13 @@ from lfss.eng.utils import ensure_uri_compnents
|
|
15
15
|
|
16
16
|
_default_endpoint = os.environ.get('LFSS_ENDPOINT', 'http://localhost:8000')
|
17
17
|
_default_token = os.environ.get('LFSS_TOKEN', '')
|
18
|
+
num_t = float | int
|
18
19
|
|
19
20
|
class Connector:
|
20
21
|
class Session:
|
21
22
|
def __init__(
|
22
23
|
self, connector: Connector, pool_size: int = 10,
|
23
|
-
retry: int = 1, backoff_factor:
|
24
|
+
retry: int = 1, backoff_factor: num_t = 0.5, status_forcelist: list[int] = [503]
|
24
25
|
):
|
25
26
|
self.connector = connector
|
26
27
|
self.pool_size = pool_size
|
@@ -47,13 +48,21 @@ class Connector:
|
|
47
48
|
def __exit__(self, exc_type, exc_value, traceback):
|
48
49
|
self.close()
|
49
50
|
|
50
|
-
def __init__(self, endpoint=_default_endpoint, token=_default_token):
|
51
|
+
def __init__(self, endpoint=_default_endpoint, token=_default_token, timeout: Optional[num_t | tuple[num_t, num_t]]=None, verify: Optional[bool | str] = None):
|
52
|
+
"""
|
53
|
+
- endpoint: the URL of the LFSS server. Default to $LFSS_ENDPOINT or http://localhost:8000.
|
54
|
+
- token: the access token. Default to $LFSS_TOKEN.
|
55
|
+
- timeout: the timeout for each request, can be either a single value or a tuple of two values (connect, read), refer to requests.Session.request.
|
56
|
+
- verify: either a boolean or a string, to control SSL verification. Default to True, refer to requests.Session.request.
|
57
|
+
"""
|
51
58
|
assert token, "No token provided. Please set LFSS_TOKEN environment variable."
|
52
59
|
self.config = {
|
53
60
|
"endpoint": endpoint,
|
54
61
|
"token": token
|
55
62
|
}
|
56
63
|
self._session: Optional[requests.Session] = None
|
64
|
+
self.timeout = timeout
|
65
|
+
self.verify = verify
|
57
66
|
|
58
67
|
def session( self, pool_size: int = 10, **kwargs):
|
59
68
|
""" avoid creating a new session for each request. """
|
@@ -74,11 +83,11 @@ class Connector:
|
|
74
83
|
})
|
75
84
|
headers.update(extra_headers)
|
76
85
|
if self._session is not None:
|
77
|
-
response = self._session.request(method, url, headers=headers, **kwargs)
|
86
|
+
response = self._session.request(method, url, headers=headers, timeout=self.timeout, verify=self.verify, **kwargs)
|
78
87
|
response.raise_for_status()
|
79
88
|
else:
|
80
89
|
with requests.Session() as s:
|
81
|
-
response = s.request(method, url, headers=headers, **kwargs)
|
90
|
+
response = s.request(method, url, headers=headers, timeout=self.timeout, verify=self.verify, **kwargs)
|
82
91
|
response.raise_for_status()
|
83
92
|
return response
|
84
93
|
return f
|
lfss/eng/connection_pool.py
CHANGED
@@ -8,7 +8,7 @@ from functools import wraps
|
|
8
8
|
from typing import Callable, Awaitable
|
9
9
|
|
10
10
|
from .log import get_logger
|
11
|
-
from .error import DatabaseLockedError
|
11
|
+
from .error import DatabaseLockedError, DatabaseTransactionError
|
12
12
|
from .config import DATA_HOME
|
13
13
|
|
14
14
|
async def execute_sql(conn: aiosqlite.Connection | aiosqlite.Cursor, name: str):
|
@@ -147,6 +147,14 @@ def global_entrance(n_read: int = 1):
|
|
147
147
|
return wrapper
|
148
148
|
return decorator
|
149
149
|
|
150
|
+
def handle_sqlite_error(e: Exception):
|
151
|
+
if 'database is locked' in str(e):
|
152
|
+
raise DatabaseLockedError from e
|
153
|
+
if 'cannot start a transaction within a transaction' in str(e):
|
154
|
+
get_logger('database', global_instance=True).error(f"Unexpected error: {e}")
|
155
|
+
raise DatabaseTransactionError from e
|
156
|
+
raise e
|
157
|
+
|
150
158
|
@asynccontextmanager
|
151
159
|
async def unique_cursor(is_write: bool = False):
|
152
160
|
if not is_write:
|
@@ -155,9 +163,7 @@ async def unique_cursor(is_write: bool = False):
|
|
155
163
|
try:
|
156
164
|
yield await connection_obj.conn.cursor()
|
157
165
|
except Exception as e:
|
158
|
-
|
159
|
-
raise DatabaseLockedError from e
|
160
|
-
raise e
|
166
|
+
handle_sqlite_error(e)
|
161
167
|
finally:
|
162
168
|
await g_pool.release(connection_obj)
|
163
169
|
else:
|
@@ -166,9 +172,7 @@ async def unique_cursor(is_write: bool = False):
|
|
166
172
|
try:
|
167
173
|
yield await connection_obj.conn.cursor()
|
168
174
|
except Exception as e:
|
169
|
-
|
170
|
-
raise DatabaseLockedError from e
|
171
|
-
raise e
|
175
|
+
handle_sqlite_error(e)
|
172
176
|
finally:
|
173
177
|
await g_pool.release(connection_obj)
|
174
178
|
|
lfss/eng/database.py
CHANGED
@@ -965,6 +965,11 @@ class Database:
|
|
965
965
|
async def zip_path(self, top_url: str, op_user: Optional[UserRecord]) -> io.BytesIO:
|
966
966
|
if top_url.startswith('/'):
|
967
967
|
top_url = top_url[1:]
|
968
|
+
|
969
|
+
if op_user:
|
970
|
+
if await check_path_permission(top_url, op_user) < AccessLevel.READ:
|
971
|
+
raise PermissionDeniedError(f"Permission denied: {op_user.username} cannot zip path {top_url}")
|
972
|
+
|
968
973
|
buffer = io.BytesIO()
|
969
974
|
with zipfile.ZipFile(buffer, 'w') as zf:
|
970
975
|
async for (r, blob) in self.iter_path(top_url, None):
|
@@ -979,39 +984,50 @@ class Database:
|
|
979
984
|
buffer.seek(0)
|
980
985
|
return buffer
|
981
986
|
|
982
|
-
def
|
987
|
+
async def _get_path_owner(cur: aiosqlite.Cursor, path: str) -> UserRecord:
|
988
|
+
path_username = path.split('/')[0]
|
989
|
+
uconn = UserConn(cur)
|
990
|
+
path_user = await uconn.get_user(path_username)
|
991
|
+
if path_user is None:
|
992
|
+
raise PathNotFoundError(f"Invalid path: {path_username} is not a valid username")
|
993
|
+
return path_user
|
994
|
+
|
995
|
+
async def check_file_read_permission(user: UserRecord, file: FileRecord, cursor: Optional[aiosqlite.Cursor] = None) -> tuple[bool, str]:
|
983
996
|
"""
|
984
997
|
This does not consider alias level permission,
|
985
998
|
use check_path_permission for alias level permission check first:
|
986
999
|
```
|
987
1000
|
if await check_path_permission(path, user) < AccessLevel.READ:
|
988
|
-
read_allowed, reason = check_file_read_permission(user,
|
1001
|
+
read_allowed, reason = check_file_read_permission(user, file)
|
989
1002
|
```
|
1003
|
+
The implementation assumes the user is not admin and is not the owner of the file/path
|
990
1004
|
"""
|
991
|
-
|
992
|
-
|
1005
|
+
@asynccontextmanager
|
1006
|
+
async def this_cur():
|
1007
|
+
if cursor is None:
|
1008
|
+
async with unique_cursor() as _cur:
|
1009
|
+
yield _cur
|
1010
|
+
else:
|
1011
|
+
yield cursor
|
1012
|
+
|
1013
|
+
f_perm = file.permission
|
1014
|
+
|
1015
|
+
# if file permission unset, use path owner's permission as fallback
|
1016
|
+
if f_perm == FileReadPermission.UNSET:
|
1017
|
+
async with this_cur() as cur:
|
1018
|
+
path_owner = await _get_path_owner(cur, file.url)
|
1019
|
+
f_perm = path_owner.permission
|
993
1020
|
|
994
1021
|
# check permission of the file
|
995
|
-
if
|
996
|
-
|
997
|
-
|
998
|
-
elif file.permission == FileReadPermission.PROTECTED:
|
1022
|
+
if f_perm == FileReadPermission.PRIVATE:
|
1023
|
+
return False, "Permission denied, private file"
|
1024
|
+
elif f_perm == FileReadPermission.PROTECTED:
|
999
1025
|
if user.id == 0:
|
1000
1026
|
return False, "Permission denied, protected file"
|
1001
|
-
elif
|
1027
|
+
elif f_perm == FileReadPermission.PUBLIC:
|
1002
1028
|
return True, ""
|
1003
1029
|
else:
|
1004
|
-
assert
|
1005
|
-
|
1006
|
-
# use owner's permission as fallback
|
1007
|
-
if owner.permission == FileReadPermission.PRIVATE:
|
1008
|
-
if user.id != owner.id:
|
1009
|
-
return False, "Permission denied, private user file"
|
1010
|
-
elif owner.permission == FileReadPermission.PROTECTED:
|
1011
|
-
if user.id == 0:
|
1012
|
-
return False, "Permission denied, protected user file"
|
1013
|
-
else:
|
1014
|
-
assert owner.permission == FileReadPermission.PUBLIC or owner.permission == FileReadPermission.UNSET
|
1030
|
+
assert f_perm == FileReadPermission.UNSET
|
1015
1031
|
|
1016
1032
|
return True, ""
|
1017
1033
|
|
@@ -1034,15 +1050,11 @@ async def check_path_permission(path: str, user: UserRecord, cursor: Optional[ai
|
|
1034
1050
|
yield cursor
|
1035
1051
|
|
1036
1052
|
# check if path user exists
|
1037
|
-
path_username = path.split('/')[0]
|
1038
1053
|
async with this_cur() as cur:
|
1039
|
-
|
1040
|
-
path_user = await uconn.get_user(path_username)
|
1041
|
-
if path_user is None:
|
1042
|
-
raise PathNotFoundError(f"Invalid path: {path_username} is not a valid username")
|
1054
|
+
path_owner = await _get_path_owner(cur, path)
|
1043
1055
|
|
1044
|
-
# check if user is admin
|
1045
|
-
if user.is_admin or user.
|
1056
|
+
# check if user is admin or the owner of the path
|
1057
|
+
if user.is_admin or user.id == path_owner.id:
|
1046
1058
|
return AccessLevel.ALL
|
1047
1059
|
|
1048
1060
|
# if the path is a file, check if the user is the owner
|
@@ -1056,4 +1068,4 @@ async def check_path_permission(path: str, user: UserRecord, cursor: Optional[ai
|
|
1056
1068
|
# check alias level
|
1057
1069
|
async with this_cur() as cur:
|
1058
1070
|
uconn = UserConn(cur)
|
1059
|
-
return await uconn.query_peer_level(user.id,
|
1071
|
+
return await uconn.query_peer_level(user.id, path_owner.id)
|
lfss/eng/error.py
CHANGED
@@ -12,6 +12,8 @@ class InvalidPathError(LFSSExceptionBase, ValueError):...
|
|
12
12
|
|
13
13
|
class DatabaseLockedError(LFSSExceptionBase, sqlite3.DatabaseError):...
|
14
14
|
|
15
|
+
class DatabaseTransactionError(LFSSExceptionBase, sqlite3.DatabaseError):...
|
16
|
+
|
15
17
|
class PathNotFoundError(LFSSExceptionBase, FileNotFoundError):...
|
16
18
|
|
17
19
|
class FileDuplicateError(LFSSExceptionBase, FileExistsError):...
|
lfss/svc/app_base.py
CHANGED
@@ -54,6 +54,7 @@ def handle_exception(fn):
|
|
54
54
|
if isinstance(e, FileExistsError): raise HTTPException(status_code=409, detail=str(e))
|
55
55
|
if isinstance(e, TooManyItemsError): raise HTTPException(status_code=400, detail=str(e))
|
56
56
|
if isinstance(e, DatabaseLockedError): raise HTTPException(status_code=503, detail=str(e))
|
57
|
+
if isinstance(e, DatabaseTransactionError): raise HTTPException(status_code=503, detail=str(e))
|
57
58
|
if isinstance(e, FileLockedError): raise HTTPException(status_code=423, detail=str(e))
|
58
59
|
logger.error(f"Uncaptured error in {fn.__name__}: {e}")
|
59
60
|
raise
|
lfss/svc/app_native.py
CHANGED
@@ -5,6 +5,7 @@ from fastapi.responses import StreamingResponse
|
|
5
5
|
from fastapi.exceptions import HTTPException
|
6
6
|
|
7
7
|
from ..eng.utils import ensure_uri_compnents
|
8
|
+
from ..eng.config import MAX_MEM_FILE_BYTES
|
8
9
|
from ..eng.connection_pool import unique_cursor
|
9
10
|
from ..eng.database import check_file_read_permission, check_path_permission, UserConn, FileConn
|
10
11
|
from ..eng.datatype import (
|
@@ -92,14 +93,29 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
|
|
92
93
|
dir_record = await FileConn(cur).get_path_record(path)
|
93
94
|
|
94
95
|
pathname = f"{path.split('/')[-2]}"
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
96
|
+
|
97
|
+
if dir_record.size < MAX_MEM_FILE_BYTES:
|
98
|
+
logger.debug(f"Bundle {path} in memory")
|
99
|
+
dir_bytes = (await db.zip_path(path, op_user=user)).getvalue()
|
100
|
+
return Response(
|
101
|
+
content = dir_bytes,
|
102
|
+
media_type = "application/zip",
|
103
|
+
headers = {
|
104
|
+
f"Content-Disposition": f"attachment; filename=bundle-{pathname}.zip",
|
105
|
+
"Content-Length": str(len(dir_bytes)),
|
106
|
+
"X-Content-Bytes": str(dir_record.size),
|
107
|
+
}
|
108
|
+
)
|
109
|
+
else:
|
110
|
+
logger.debug(f"Bundle {path} in stream")
|
111
|
+
return StreamingResponse(
|
112
|
+
content = await db.zip_path_stream(path, op_user=user),
|
113
|
+
media_type = "application/zip",
|
114
|
+
headers = {
|
115
|
+
f"Content-Disposition": f"attachment; filename=bundle-{pathname}.zip",
|
116
|
+
"X-Content-Bytes": str(dir_record.size),
|
117
|
+
}
|
118
|
+
)
|
103
119
|
|
104
120
|
@router_api.get("/meta")
|
105
121
|
@handle_exception
|
@@ -112,9 +128,7 @@ async def get_file_meta(path: str, user: UserRecord = Depends(registered_user)):
|
|
112
128
|
if is_file:
|
113
129
|
record = await fconn.get_file_record(path, throw=True)
|
114
130
|
if await check_path_permission(path, user, cursor=cur) < AccessLevel.READ:
|
115
|
-
|
116
|
-
owner = await uconn.get_user_by_id(record.owner_id, throw=True)
|
117
|
-
is_allowed, reason = check_file_read_permission(user, owner, record)
|
131
|
+
is_allowed, reason = await check_file_read_permission(user, record, cursor=cur)
|
118
132
|
if not is_allowed:
|
119
133
|
raise HTTPException(status_code=403, detail=reason)
|
120
134
|
else:
|
lfss/svc/common_impl.py
CHANGED
@@ -112,11 +112,8 @@ async def get_impl(
|
|
112
112
|
async with unique_cursor() as cur:
|
113
113
|
fconn = FileConn(cur)
|
114
114
|
file_record = await fconn.get_file_record(path, throw=True)
|
115
|
-
uconn = UserConn(cur)
|
116
|
-
owner = await uconn.get_user_by_id(file_record.owner_id, throw=True)
|
117
|
-
|
118
115
|
if not await check_path_permission(path, user, cursor=cur) >= AccessLevel.READ:
|
119
|
-
allow_access, reason = check_file_read_permission(user,
|
116
|
+
allow_access, reason = await check_file_read_permission(user, file_record, cursor=cur)
|
120
117
|
if not allow_access:
|
121
118
|
raise HTTPException(status_code=403 if user.id != 0 else 401, detail=reason)
|
122
119
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lfss
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.10.0
|
4
4
|
Summary: Lightweight file storage service
|
5
5
|
Home-page: https://github.com/MenxLi/lfss
|
6
6
|
Author: li_mengxun
|
@@ -23,7 +23,7 @@ Project-URL: Repository, https://github.com/MenxLi/lfss
|
|
23
23
|
Description-Content-Type: text/markdown
|
24
24
|
|
25
25
|
# Lite File Storage Service (LFSS)
|
26
|
-
[](https://pypi.org/project/lfss/)
|
26
|
+
[](https://pypi.org/project/lfss/) [](https://pypi.org/project/lfss/)
|
27
27
|
|
28
28
|
My experiment on a lightweight and high-performance file/object storage service...
|
29
29
|
|
@@ -1,8 +1,8 @@
|
|
1
|
-
Readme.md,sha256=
|
1
|
+
Readme.md,sha256=B-foESzFWoSI5MEd89AWUzKcVRrTwipM28TK8GN0o8c,1920
|
2
2
|
docs/Changelog.md,sha256=QYej_hmGnv9t8wjFHXBvmrBOvY7aACZ82oa5SVkIyzM,882
|
3
3
|
docs/Enviroment_variables.md,sha256=xaL8qBwT8B2Qe11FaOU3xWrRCh1mJ1VyTFCeFbkd0rs,570
|
4
4
|
docs/Known_issues.md,sha256=ZqETcWP8lzTOel9b2mxEgCnADFF8IxOrEtiVO1NoMAk,251
|
5
|
-
docs/Permission.md,sha256=
|
5
|
+
docs/Permission.md,sha256=thUJx7YRoU63Pb-eqo5l5450DrZN3QYZ36GCn8r66no,3152
|
6
6
|
docs/Webdav.md,sha256=-Ja-BTWSY1BEMAyZycvEMNnkNTPZ49gSPzmf3Lbib70,1547
|
7
7
|
frontend/api.js,sha256=GlQsNoZFEcy7QUUsLbXv7aP-KxRnIxM37FQHTaakGiQ,19387
|
8
8
|
frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
|
@@ -19,7 +19,7 @@ frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
|
|
19
19
|
frontend/thumb.js,sha256=46ViD2TlTTWy0fx6wjoAs_5CQ4ajYB90vVzM7UO2IHw,6182
|
20
20
|
frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
|
21
21
|
lfss/api/__init__.py,sha256=8IJqrpWK1doIyVVbntvVic82A57ncwl5b0BRHX4Ri6A,6660
|
22
|
-
lfss/api/connector.py,sha256=
|
22
|
+
lfss/api/connector.py,sha256=Duh57M3dOeG_M5UidZ4hMHK7ot1JsUC6RdXgIn6KTC8,12913
|
23
23
|
lfss/cli/__init__.py,sha256=lPwPmqpa7EXQ4zlU7E7LOe6X2kw_xATGdwoHphUEirA,827
|
24
24
|
lfss/cli/balance.py,sha256=fUbKKAUyaDn74f7mmxMfBL4Q4voyBLHu6Lg_g8GfMOQ,4121
|
25
25
|
lfss/cli/cli.py,sha256=aYjB8d4k6JUd9efxZK-XOj-mlG4JeOr_0lnj2qqCiK0,8066
|
@@ -30,22 +30,22 @@ lfss/cli/vacuum.py,sha256=GOG72d3NYe9bYCNc3y8JecEmM-DrKlGq3JQcisv_xBg,3702
|
|
30
30
|
lfss/eng/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
31
31
|
lfss/eng/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
|
32
32
|
lfss/eng/config.py,sha256=Vni6h52Ce0njVrHZLAWFL8g34YDBdlmGrmRhpxElxQ8,868
|
33
|
-
lfss/eng/connection_pool.py,sha256=
|
34
|
-
lfss/eng/database.py,sha256=
|
33
|
+
lfss/eng/connection_pool.py,sha256=1aq7nSgd7hB9YNV4PjD1RDRyl_moDw3ubBtSLyfgGBs,6320
|
34
|
+
lfss/eng/database.py,sha256=RYIG2506_-S84f6CsOQ6pgpr1vgPT3p1kP1FsZwTOnM,49098
|
35
35
|
lfss/eng/datatype.py,sha256=27UB7-l9SICy5lAvKjdzpTL_GohZjzstQcr9PtAq7nM,2709
|
36
|
-
lfss/eng/error.py,sha256=
|
36
|
+
lfss/eng/error.py,sha256=JGf5NV-f4rL6tNIDSAx5-l9MG8dEj7F2w_MuOjj1d1o,732
|
37
37
|
lfss/eng/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
|
38
38
|
lfss/eng/thumb.py,sha256=x9jIHHU1tskmp-TavPPcxGpbmEjCp9gbH6ZlsEfqUxY,3383
|
39
39
|
lfss/eng/utils.py,sha256=WYoXFFi5308UWtFC8VP792gpzrVbHZZHhP3PaFjxIEY,6770
|
40
40
|
lfss/sql/init.sql,sha256=8LjHx0TBCkBD62xFfssSeHDqKYVQQJkZAg4rSm046f4,1496
|
41
41
|
lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
|
42
42
|
lfss/svc/app.py,sha256=ftWCpepBx-gTSG7i-TB-IdinPPstAYYQjCgnTfeMZeI,219
|
43
|
-
lfss/svc/app_base.py,sha256=
|
43
|
+
lfss/svc/app_base.py,sha256=bTQbz945xalyB3UZLlqVBvL6JKGNQ8Fm2KpIvvucPZQ,6850
|
44
44
|
lfss/svc/app_dav.py,sha256=D0KSgjtTktPjIhyIKG5eRmBdh5X8HYFYH151E6gzlbc,18245
|
45
|
-
lfss/svc/app_native.py,sha256=
|
46
|
-
lfss/svc/common_impl.py,sha256=
|
45
|
+
lfss/svc/app_native.py,sha256=JbPge-F9irl26tXKAzfA5DfyjCh0Dgttflztqqrvt0A,8890
|
46
|
+
lfss/svc/common_impl.py,sha256=5ZRM24zVZpAeipgDtZUVBMFtArkydlAkn17ic_XL7v8,13733
|
47
47
|
lfss/svc/request_log.py,sha256=v8yXEIzPjaksu76Oh5vgdbUEUrw8Kt4etLAXBWSGie8,3207
|
48
|
-
lfss-0.
|
49
|
-
lfss-0.
|
50
|
-
lfss-0.
|
51
|
-
lfss-0.
|
48
|
+
lfss-0.10.0.dist-info/METADATA,sha256=NewpmEw8OUj28rPkujuPi3ZySJG_JCvSEKY5JBxg6cw,2715
|
49
|
+
lfss-0.10.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
50
|
+
lfss-0.10.0.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
|
51
|
+
lfss-0.10.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|