lfss 0.3.0__tar.gz → 0.3.1__tar.gz
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-0.3.0 → lfss-0.3.1}/PKG-INFO +1 -1
- {lfss-0.3.0 → lfss-0.3.1}/frontend/index.html +5 -3
- {lfss-0.3.0 → lfss-0.3.1}/frontend/scripts.js +3 -1
- {lfss-0.3.0 → lfss-0.3.1}/lfss/client/api.py +5 -5
- {lfss-0.3.0 → lfss-0.3.1}/lfss/src/database.py +41 -36
- {lfss-0.3.0 → lfss-0.3.1}/lfss/src/server.py +9 -9
- {lfss-0.3.0 → lfss-0.3.1}/pyproject.toml +1 -1
- {lfss-0.3.0 → lfss-0.3.1}/Readme.md +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/docs/Known_issues.md +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/docs/Permission.md +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/frontend/api.js +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/frontend/popup.css +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/frontend/popup.js +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/frontend/styles.css +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/frontend/utils.js +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/lfss/cli/panel.py +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/lfss/cli/serve.py +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/lfss/cli/user.py +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/lfss/client/__init__.py +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/lfss/src/__init__.py +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/lfss/src/config.py +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/lfss/src/error.py +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/lfss/src/log.py +0 -0
- {lfss-0.3.0 → lfss-0.3.1}/lfss/src/utils.py +0 -0
@@ -48,9 +48,11 @@
|
|
48
48
|
<div class="input-group">
|
49
49
|
<input type="file" id="file-selector" accept="*">
|
50
50
|
<label for="file-name" id="upload-file-prefix"></label>
|
51
|
-
<
|
52
|
-
|
53
|
-
|
51
|
+
<div class="input-group">
|
52
|
+
<input type="text" id="file-name" placeholder="Destination file path" autocomplete="off">
|
53
|
+
<span id='randomize-fname-btn'>🎲</span>
|
54
|
+
<button id="upload-btn">Upload</button>
|
55
|
+
</div>
|
54
56
|
</div>
|
55
57
|
</div>
|
56
58
|
|
@@ -264,7 +264,9 @@ function refreshFileList(){
|
|
264
264
|
|
265
265
|
const downloadButton = document.createElement('a');
|
266
266
|
downloadButton.textContent = 'Download';
|
267
|
-
downloadButton.href = conn.config.endpoint + '/_api/bundle?
|
267
|
+
downloadButton.href = conn.config.endpoint + '/_api/bundle?' +
|
268
|
+
'token=' + conn.config.token + '&' +
|
269
|
+
'path=' + dir.url + (dir.url.endsWith('/') ? '' : '/');
|
268
270
|
actContainer.appendChild(downloadButton);
|
269
271
|
|
270
272
|
const deleteButton = document.createElement('a');
|
@@ -3,7 +3,7 @@ import os
|
|
3
3
|
import requests
|
4
4
|
import urllib.parse
|
5
5
|
from lfss.src.database import (
|
6
|
-
FileReadPermission,
|
6
|
+
FileReadPermission, FileRecord, UserRecord, PathContents
|
7
7
|
)
|
8
8
|
|
9
9
|
_default_endpoint = os.environ.get('LFSS_ENDPOINT', 'http://localhost:8000')
|
@@ -58,11 +58,11 @@ class Connector:
|
|
58
58
|
path = path[1:]
|
59
59
|
self._fetch('DELETE', path)()
|
60
60
|
|
61
|
-
def get_metadata(self, path: str) -> Optional[
|
61
|
+
def get_metadata(self, path: str) -> Optional[FileRecord]:
|
62
62
|
"""Gets the metadata for the file at the specified path."""
|
63
63
|
try:
|
64
64
|
response = self._fetch('GET', '_api/fmeta', {'path': path})()
|
65
|
-
return
|
65
|
+
return FileRecord(**response.json())
|
66
66
|
except requests.exceptions.HTTPError as e:
|
67
67
|
if e.response.status_code == 404:
|
68
68
|
return None
|
@@ -85,7 +85,7 @@ class Connector:
|
|
85
85
|
headers = {'Content-Type': 'application/www-form-urlencoded'}
|
86
86
|
)
|
87
87
|
|
88
|
-
def whoami(self) ->
|
88
|
+
def whoami(self) -> UserRecord:
|
89
89
|
"""Gets information about the current user."""
|
90
90
|
response = self._fetch('GET', '_api/whoami')()
|
91
|
-
return
|
91
|
+
return UserRecord(**response.json())
|
@@ -1,5 +1,5 @@
|
|
1
1
|
|
2
|
-
from typing import Optional, overload, Literal
|
2
|
+
from typing import Optional, overload, Literal, AsyncIterable
|
3
3
|
from abc import ABC, abstractmethod
|
4
4
|
|
5
5
|
import urllib.parse
|
@@ -58,7 +58,7 @@ class FileReadPermission(IntEnum):
|
|
58
58
|
PRIVATE = 3 # accessible by owner only (including admin)
|
59
59
|
|
60
60
|
@dataclasses.dataclass
|
61
|
-
class
|
61
|
+
class UserRecord:
|
62
62
|
id: int
|
63
63
|
username: str
|
64
64
|
credential: str
|
@@ -71,12 +71,12 @@ class DBUserRecord:
|
|
71
71
|
def __str__(self):
|
72
72
|
return f"User {self.username} (id={self.id}, admin={self.is_admin}, created at {self.create_time}, last active at {self.last_active}), storage={self.max_storage}, permission={self.permission}"
|
73
73
|
|
74
|
-
DECOY_USER =
|
74
|
+
DECOY_USER = UserRecord(0, 'decoy', 'decoy', False, '2021-01-01 00:00:00', '2021-01-01 00:00:00', 0, FileReadPermission.PRIVATE)
|
75
75
|
class UserConn(DBConnBase):
|
76
76
|
|
77
77
|
@staticmethod
|
78
|
-
def parse_record(record) ->
|
79
|
-
return
|
78
|
+
def parse_record(record) -> UserRecord:
|
79
|
+
return UserRecord(*record)
|
80
80
|
|
81
81
|
async def init(self):
|
82
82
|
await super().init()
|
@@ -102,21 +102,21 @@ class UserConn(DBConnBase):
|
|
102
102
|
|
103
103
|
return self
|
104
104
|
|
105
|
-
async def get_user(self, username: str) -> Optional[
|
105
|
+
async def get_user(self, username: str) -> Optional[UserRecord]:
|
106
106
|
async with self.conn.execute("SELECT * FROM user WHERE username = ?", (username, )) as cursor:
|
107
107
|
res = await cursor.fetchone()
|
108
108
|
|
109
109
|
if res is None: return None
|
110
110
|
return self.parse_record(res)
|
111
111
|
|
112
|
-
async def get_user_by_id(self, user_id: int) -> Optional[
|
112
|
+
async def get_user_by_id(self, user_id: int) -> Optional[UserRecord]:
|
113
113
|
async with self.conn.execute("SELECT * FROM user WHERE id = ?", (user_id, )) as cursor:
|
114
114
|
res = await cursor.fetchone()
|
115
115
|
|
116
116
|
if res is None: return None
|
117
117
|
return self.parse_record(res)
|
118
118
|
|
119
|
-
async def get_user_by_credential(self, credential: str) -> Optional[
|
119
|
+
async def get_user_by_credential(self, credential: str) -> Optional[UserRecord]:
|
120
120
|
async with self.conn.execute("SELECT * FROM user WHERE credential = ?", (credential, )) as cursor:
|
121
121
|
res = await cursor.fetchone()
|
122
122
|
|
@@ -182,7 +182,7 @@ class UserConn(DBConnBase):
|
|
182
182
|
self.logger.info(f"Delete user {username}")
|
183
183
|
|
184
184
|
@dataclasses.dataclass
|
185
|
-
class
|
185
|
+
class FileRecord:
|
186
186
|
url: str
|
187
187
|
owner_id: int
|
188
188
|
file_id: str # defines mapping from fmata to fdata
|
@@ -206,13 +206,13 @@ class DirectoryRecord:
|
|
206
206
|
@dataclasses.dataclass
|
207
207
|
class PathContents:
|
208
208
|
dirs: list[DirectoryRecord]
|
209
|
-
files: list[
|
209
|
+
files: list[FileRecord]
|
210
210
|
|
211
211
|
class FileConn(DBConnBase):
|
212
212
|
|
213
213
|
@staticmethod
|
214
|
-
def parse_record(record) ->
|
215
|
-
return
|
214
|
+
def parse_record(record) -> FileRecord:
|
215
|
+
return FileRecord(*record)
|
216
216
|
|
217
217
|
async def init(self):
|
218
218
|
await super().init()
|
@@ -259,26 +259,26 @@ class FileConn(DBConnBase):
|
|
259
259
|
|
260
260
|
return self
|
261
261
|
|
262
|
-
async def get_file_record(self, url: str) -> Optional[
|
262
|
+
async def get_file_record(self, url: str) -> Optional[FileRecord]:
|
263
263
|
async with self.conn.execute("SELECT * FROM fmeta WHERE url = ?", (url, )) as cursor:
|
264
264
|
res = await cursor.fetchone()
|
265
265
|
if res is None:
|
266
266
|
return None
|
267
267
|
return self.parse_record(res)
|
268
268
|
|
269
|
-
async def get_file_records(self, urls: list[str]) -> list[
|
269
|
+
async def get_file_records(self, urls: list[str]) -> list[FileRecord]:
|
270
270
|
async with self.conn.execute("SELECT * FROM fmeta WHERE url IN ({})".format(','.join(['?'] * len(urls))), urls) as cursor:
|
271
271
|
res = await cursor.fetchall()
|
272
272
|
if res is None:
|
273
273
|
return []
|
274
274
|
return [self.parse_record(r) for r in res]
|
275
275
|
|
276
|
-
async def get_user_file_records(self, owner_id: int) -> list[
|
276
|
+
async def get_user_file_records(self, owner_id: int) -> list[FileRecord]:
|
277
277
|
async with self.conn.execute("SELECT * FROM fmeta WHERE owner_id = ?", (owner_id, )) as cursor:
|
278
278
|
res = await cursor.fetchall()
|
279
279
|
return [self.parse_record(r) for r in res]
|
280
280
|
|
281
|
-
async def get_path_records(self, url: str) -> list[
|
281
|
+
async def get_path_records(self, url: str) -> list[FileRecord]:
|
282
282
|
async with self.conn.execute("SELECT * FROM fmeta WHERE url LIKE ?", (url + '%', )) as cursor:
|
283
283
|
res = await cursor.fetchall()
|
284
284
|
return [self.parse_record(r) for r in res]
|
@@ -301,11 +301,11 @@ class FileConn(DBConnBase):
|
|
301
301
|
return dirs
|
302
302
|
|
303
303
|
@overload
|
304
|
-
async def list_path(self, url: str, flat: Literal[True]) -> list[
|
304
|
+
async def list_path(self, url: str, flat: Literal[True]) -> list[FileRecord]:...
|
305
305
|
@overload
|
306
306
|
async def list_path(self, url: str, flat: Literal[False]) -> PathContents:...
|
307
307
|
|
308
|
-
async def list_path(self, url: str, flat: bool = False) -> list[
|
308
|
+
async def list_path(self, url: str, flat: bool = False) -> list[FileRecord] | PathContents:
|
309
309
|
"""
|
310
310
|
List all files and directories under the given path,
|
311
311
|
if flat is True, return a list of FileDBRecord, recursively including all subdirectories.
|
@@ -495,7 +495,7 @@ def validate_url(url: str, is_file = True):
|
|
495
495
|
if not ret:
|
496
496
|
raise InvalidPathError(f"Invalid URL: {url}")
|
497
497
|
|
498
|
-
async def get_user(db: "Database", user: int | str) -> Optional[
|
498
|
+
async def get_user(db: "Database", user: int | str) -> Optional[UserRecord]:
|
499
499
|
if isinstance(user, str):
|
500
500
|
return await db.user.get_user(user)
|
501
501
|
elif isinstance(user, int):
|
@@ -591,7 +591,7 @@ class Database:
|
|
591
591
|
|
592
592
|
return blob
|
593
593
|
|
594
|
-
async def delete_file(self, url: str) -> Optional[
|
594
|
+
async def delete_file(self, url: str) -> Optional[FileRecord]:
|
595
595
|
validate_url(url)
|
596
596
|
|
597
597
|
async with transaction(self):
|
@@ -631,32 +631,37 @@ class Database:
|
|
631
631
|
await self.file.delete_file_blobs([r.file_id for r in records])
|
632
632
|
await self.file.delete_user_file_records(user.id)
|
633
633
|
await self.user.delete_user(user.username)
|
634
|
-
|
635
|
-
async def
|
634
|
+
|
635
|
+
async def iter_path(self, top_url: str, urls: Optional[list[str]]) -> AsyncIterable[tuple[FileRecord, bytes]]:
|
636
636
|
if urls is None:
|
637
637
|
urls = [r.url for r in await self.file.list_path(top_url, flat=True)]
|
638
638
|
|
639
|
+
for url in urls:
|
640
|
+
if not url.startswith(top_url):
|
641
|
+
continue
|
642
|
+
r = await self.file.get_file_record(url)
|
643
|
+
if r is None:
|
644
|
+
continue
|
645
|
+
f_id = r.file_id
|
646
|
+
blob = await self.file.get_file_blob(f_id)
|
647
|
+
if blob is None:
|
648
|
+
self.logger.warning(f"Blob not found for {url}")
|
649
|
+
continue
|
650
|
+
yield r, blob
|
651
|
+
|
652
|
+
async def zip_path(self, top_url: str, urls: Optional[list[str]]) -> io.BytesIO:
|
653
|
+
if top_url.startswith('/'):
|
654
|
+
top_url = top_url[1:]
|
639
655
|
buffer = io.BytesIO()
|
640
656
|
with zipfile.ZipFile(buffer, 'w') as zf:
|
641
|
-
for
|
642
|
-
|
643
|
-
continue
|
644
|
-
r = await self.file.get_file_record(url)
|
645
|
-
if r is None:
|
646
|
-
continue
|
647
|
-
f_id = r.file_id
|
648
|
-
blob = await self.file.get_file_blob(f_id)
|
649
|
-
if blob is None:
|
650
|
-
continue
|
651
|
-
|
652
|
-
rel_path = url[len(top_url):]
|
657
|
+
async for (r, blob) in self.iter_path(top_url, urls):
|
658
|
+
rel_path = r.url[len(top_url):]
|
653
659
|
rel_path = decode_uri_compnents(rel_path)
|
654
660
|
zf.writestr(rel_path, blob)
|
655
|
-
|
656
661
|
buffer.seek(0)
|
657
662
|
return buffer
|
658
663
|
|
659
|
-
def check_user_permission(user:
|
664
|
+
def check_user_permission(user: UserRecord, owner: UserRecord, file: FileRecord) -> tuple[bool, str]:
|
660
665
|
if user.is_admin:
|
661
666
|
return True, ""
|
662
667
|
|
@@ -15,7 +15,7 @@ from .error import *
|
|
15
15
|
from .log import get_logger
|
16
16
|
from .config import MAX_BUNDLE_BYTES, MAX_FILE_BYTES
|
17
17
|
from .utils import ensure_uri_compnents
|
18
|
-
from .database import Database,
|
18
|
+
from .database import Database, UserRecord, DECOY_USER, FileRecord, check_user_permission, FileReadPermission
|
19
19
|
|
20
20
|
logger = get_logger("server")
|
21
21
|
conn = Database()
|
@@ -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, download = False, user:
|
81
|
+
async def get_file(path: str, download = False, user: UserRecord = Depends(get_current_user)):
|
82
82
|
path = ensure_uri_compnents(path)
|
83
83
|
|
84
84
|
# handle directory query
|
@@ -130,7 +130,7 @@ async def get_file(path: str, download = False, user: DBUserRecord = Depends(get
|
|
130
130
|
return await send(None, "inline")
|
131
131
|
|
132
132
|
@router_fs.put("/{path:path}")
|
133
|
-
async def put_file(request: Request, path: str, user:
|
133
|
+
async def put_file(request: Request, path: str, user: UserRecord = Depends(get_current_user)):
|
134
134
|
path = ensure_uri_compnents(path)
|
135
135
|
if user.id == 0:
|
136
136
|
logger.debug("Reject put request from DECOY_USER")
|
@@ -185,7 +185,7 @@ async def put_file(request: Request, path: str, user: DBUserRecord = Depends(get
|
|
185
185
|
}, content=json.dumps({"url": path}))
|
186
186
|
|
187
187
|
@router_fs.delete("/{path:path}")
|
188
|
-
async def delete_file(path: str, user:
|
188
|
+
async def delete_file(path: str, user: UserRecord = Depends(get_current_user)):
|
189
189
|
path = ensure_uri_compnents(path)
|
190
190
|
if user.id == 0:
|
191
191
|
raise HTTPException(status_code=401, detail="Permission denied")
|
@@ -207,7 +207,7 @@ async def delete_file(path: str, user: DBUserRecord = Depends(get_current_user))
|
|
207
207
|
router_api = APIRouter(prefix="/_api")
|
208
208
|
|
209
209
|
@router_api.get("/bundle")
|
210
|
-
async def bundle_files(path: str, user:
|
210
|
+
async def bundle_files(path: str, user: UserRecord = Depends(get_current_user)):
|
211
211
|
logger.info(f"GET bundle({path}), user: {user.username}")
|
212
212
|
if user.id == 0:
|
213
213
|
raise HTTPException(status_code=401, detail="Permission denied")
|
@@ -218,7 +218,7 @@ async def bundle_files(path: str, user: DBUserRecord = Depends(get_current_user)
|
|
218
218
|
path = path[1:]
|
219
219
|
|
220
220
|
owner_records_cache = {} # cache owner records, ID -> UserRecord
|
221
|
-
async def is_access_granted(file_record:
|
221
|
+
async def is_access_granted(file_record: FileRecord):
|
222
222
|
owner_id = file_record.owner_id
|
223
223
|
owner = owner_records_cache.get(owner_id, None)
|
224
224
|
if owner is None:
|
@@ -249,7 +249,7 @@ async def bundle_files(path: str, user: DBUserRecord = Depends(get_current_user)
|
|
249
249
|
)
|
250
250
|
|
251
251
|
@router_api.get("/fmeta")
|
252
|
-
async def get_file_meta(path: str, user:
|
252
|
+
async def get_file_meta(path: str, user: UserRecord = Depends(get_current_user)):
|
253
253
|
logger.info(f"GET meta({path}), user: {user.username}")
|
254
254
|
if path.endswith("/"):
|
255
255
|
raise HTTPException(status_code=400, detail="Invalid path")
|
@@ -264,7 +264,7 @@ async def update_file_meta(
|
|
264
264
|
path: str,
|
265
265
|
perm: Optional[int] = None,
|
266
266
|
new_path: Optional[str] = None,
|
267
|
-
user:
|
267
|
+
user: UserRecord = Depends(get_current_user)
|
268
268
|
):
|
269
269
|
if user.id == 0:
|
270
270
|
raise HTTPException(status_code=401, detail="Permission denied")
|
@@ -293,7 +293,7 @@ async def update_file_meta(
|
|
293
293
|
return Response(status_code=200, content="OK")
|
294
294
|
|
295
295
|
@router_api.get("/whoami")
|
296
|
-
async def whoami(user:
|
296
|
+
async def whoami(user: UserRecord = Depends(get_current_user)):
|
297
297
|
if user.id == 0:
|
298
298
|
raise HTTPException(status_code=401, detail="Login required")
|
299
299
|
user.credential = "__HIDDEN__"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|