lfss 0.3.0__py3-none-any.whl → 0.3.1__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.
frontend/index.html CHANGED
@@ -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
- <input type="text" id="file-name" placeholder="Destination file path" autocomplete="off">
52
- <span id='randomize-fname-btn'>🎲</span>
53
- <button id="upload-btn">Upload</button>
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
 
frontend/scripts.js CHANGED
@@ -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?path=' + dir.url + (dir.url.endsWith('/') ? '' : '/');
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');
lfss/client/api.py CHANGED
@@ -3,7 +3,7 @@ import os
3
3
  import requests
4
4
  import urllib.parse
5
5
  from lfss.src.database import (
6
- FileReadPermission, FileDBRecord, DBUserRecord, PathContents
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[FileDBRecord]:
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 FileDBRecord(**response.json())
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) -> DBUserRecord:
88
+ def whoami(self) -> UserRecord:
89
89
  """Gets information about the current user."""
90
90
  response = self._fetch('GET', '_api/whoami')()
91
- return DBUserRecord(**response.json())
91
+ return UserRecord(**response.json())
lfss/src/database.py CHANGED
@@ -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 DBUserRecord:
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 = DBUserRecord(0, 'decoy', 'decoy', False, '2021-01-01 00:00:00', '2021-01-01 00:00:00', 0, FileReadPermission.PRIVATE)
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) -> DBUserRecord:
79
- return DBUserRecord(*record)
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[DBUserRecord]:
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[DBUserRecord]:
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[DBUserRecord]:
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 FileDBRecord:
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[FileDBRecord]
209
+ files: list[FileRecord]
210
210
 
211
211
  class FileConn(DBConnBase):
212
212
 
213
213
  @staticmethod
214
- def parse_record(record) -> FileDBRecord:
215
- return FileDBRecord(*record)
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[FileDBRecord]:
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[FileDBRecord]:
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[FileDBRecord]:
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[FileDBRecord]:
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[FileDBRecord]:...
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[FileDBRecord] | PathContents:
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[DBUserRecord]:
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[FileDBRecord]:
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 zip_path(self, top_url: str, urls: Optional[list[str]]) -> io.BytesIO:
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 url in urls:
642
- if not url.startswith(top_url):
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: DBUserRecord, owner: DBUserRecord, file: FileDBRecord) -> tuple[bool, str]:
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
 
lfss/src/server.py CHANGED
@@ -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, DBUserRecord, DECOY_USER, FileDBRecord, check_user_permission, FileReadPermission
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: DBUserRecord = Depends(get_current_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: DBUserRecord = Depends(get_current_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: DBUserRecord = Depends(get_current_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: DBUserRecord = Depends(get_current_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: FileDBRecord):
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: DBUserRecord = Depends(get_current_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: DBUserRecord = Depends(get_current_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: DBUserRecord = Depends(get_current_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__"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -2,25 +2,25 @@ 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
- frontend/index.html,sha256=JP6Sd-1JdlEfWQ4fjmSs-CrNw-2iq1RlS55SuXJq5lg,2019
5
+ frontend/index.html,sha256=VPJDs2LG8ep9kjlsKzjWzpN9vc1VGgdvOUlNTZWyQoQ,2088
6
6
  frontend/popup.css,sha256=VzkjG1ZTLxhHMtTyobnlvqYmVsTmdbJJed2Pu1cc06c,1007
7
7
  frontend/popup.js,sha256=dH5n7C2Vo9gCsMfQ4ajL4D1ETl0Wk9rIldxUb7x0f_c,4634
8
- frontend/scripts.js,sha256=lp5EalD0Ikpy4Tw5dhEORsOB_44Z88I4gJI4e8C1SDE,18175
8
+ frontend/scripts.js,sha256=1Cd5cb84SWolozA0Q1MpWaXiAqeQ5R6qZyk7gdoRPgU,18266
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
14
  lfss/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- lfss/client/api.py,sha256=SSrs1rafALmK_Pc7MfqeQm0El1rcGc-dXP8H6XMpmrY,3455
15
+ lfss/client/api.py,sha256=VBiU-JxGaN6S-fXYD_KvzG7FsGBIUzZ5IisJJSMooDk,3443
16
16
  lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  lfss/src/config.py,sha256=mc8QoiWoPgiZ5JnSvKBH8jWbp5uLSyG3l5boDiQPXS0,325
18
- lfss/src/database.py,sha256=6LDXaYO_dFuR8KrOPcvXS1_-sszFwvpyhbXaS2MTpq4,27576
18
+ lfss/src/database.py,sha256=jsDcDrc5r4mnuFiY10YEMwIRMvx6c4PY4LN9C9BinDA,27854
19
19
  lfss/src/error.py,sha256=S5ui3tJ0uKX4EZnt2Db-KbDmJXlECI_OY95cNMkuegc,218
20
20
  lfss/src/log.py,sha256=7mRHFwhx7GKtm_cRryoEIlRQhHTLQC3Qd-N81YsoKao,5174
21
- lfss/src/server.py,sha256=BeJ15QxN66k7UETCkUe03NDIIKWYdGYRtOMjox_CxIQ,11872
21
+ lfss/src/server.py,sha256=lAGfwHZasxKl9UIj8xAlTqTjMBOffU94oxOOXWuwiXM,11852
22
22
  lfss/src/utils.py,sha256=MrjKc8W2Y7AbgVGadSNAA50tRMbGYWRrA4KUhOCwuUU,694
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,,
23
+ lfss-0.3.1.dist-info/METADATA,sha256=6qCJopZvi-0F5E8qJhipUjYG7vVZeVivDNPMOjkH5bU,1787
24
+ lfss-0.3.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
25
+ lfss-0.3.1.dist-info/entry_points.txt,sha256=nUIJhenyZbcymvPVhrzV7SAtRhd7O52DflbRrpQUC04,110
26
+ lfss-0.3.1.dist-info/RECORD,,
File without changes