lfss 0.11.5__tar.gz → 0.11.6__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.
Files changed (51) hide show
  1. {lfss-0.11.5 → lfss-0.11.6}/PKG-INFO +2 -2
  2. {lfss-0.11.5 → lfss-0.11.6}/docs/Client.md +3 -0
  3. {lfss-0.11.5 → lfss-0.11.6}/docs/changelog.md +5 -0
  4. {lfss-0.11.5 → lfss-0.11.6}/frontend/api.js +1 -0
  5. {lfss-0.11.5 → lfss-0.11.6}/frontend/scripts.js +2 -0
  6. {lfss-0.11.5 → lfss-0.11.6}/lfss/api/connector.py +8 -2
  7. {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/cli.py +40 -6
  8. {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/user.py +6 -3
  9. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/database.py +16 -10
  10. {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/app_native.py +11 -1
  11. {lfss-0.11.5 → lfss-0.11.6}/pyproject.toml +2 -2
  12. {lfss-0.11.5 → lfss-0.11.6}/Readme.md +0 -0
  13. {lfss-0.11.5 → lfss-0.11.6}/docs/Enviroment_variables.md +0 -0
  14. {lfss-0.11.5 → lfss-0.11.6}/docs/Known_issues.md +0 -0
  15. {lfss-0.11.5 → lfss-0.11.6}/docs/Permission.md +0 -0
  16. {lfss-0.11.5 → lfss-0.11.6}/docs/Webdav.md +0 -0
  17. {lfss-0.11.5 → lfss-0.11.6}/frontend/index.html +0 -0
  18. {lfss-0.11.5 → lfss-0.11.6}/frontend/info.css +0 -0
  19. {lfss-0.11.5 → lfss-0.11.6}/frontend/info.js +0 -0
  20. {lfss-0.11.5 → lfss-0.11.6}/frontend/login.css +0 -0
  21. {lfss-0.11.5 → lfss-0.11.6}/frontend/login.js +0 -0
  22. {lfss-0.11.5 → lfss-0.11.6}/frontend/popup.css +0 -0
  23. {lfss-0.11.5 → lfss-0.11.6}/frontend/popup.js +0 -0
  24. {lfss-0.11.5 → lfss-0.11.6}/frontend/state.js +0 -0
  25. {lfss-0.11.5 → lfss-0.11.6}/frontend/styles.css +0 -0
  26. {lfss-0.11.5 → lfss-0.11.6}/frontend/thumb.css +0 -0
  27. {lfss-0.11.5 → lfss-0.11.6}/frontend/thumb.js +0 -0
  28. {lfss-0.11.5 → lfss-0.11.6}/frontend/utils.js +0 -0
  29. {lfss-0.11.5 → lfss-0.11.6}/lfss/api/__init__.py +0 -0
  30. {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/__init__.py +0 -0
  31. {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/balance.py +0 -0
  32. {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/log.py +0 -0
  33. {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/panel.py +0 -0
  34. {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/serve.py +0 -0
  35. {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/vacuum.py +0 -0
  36. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/__init__.py +0 -0
  37. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/bounded_pool.py +0 -0
  38. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/config.py +0 -0
  39. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/connection_pool.py +0 -0
  40. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/datatype.py +0 -0
  41. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/error.py +0 -0
  42. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/log.py +0 -0
  43. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/thumb.py +0 -0
  44. {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/utils.py +0 -0
  45. {lfss-0.11.5 → lfss-0.11.6}/lfss/sql/init.sql +0 -0
  46. {lfss-0.11.5 → lfss-0.11.6}/lfss/sql/pragma.sql +0 -0
  47. {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/app.py +0 -0
  48. {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/app_base.py +0 -0
  49. {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/app_dav.py +0 -0
  50. {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/common_impl.py +0 -0
  51. {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/request_log.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.11.5
4
- Summary: Lightweight file storage service
3
+ Version: 0.11.6
4
+ Summary: Lite file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: Li, Mengxun
7
7
  Author-email: mengxunli@whu.edu.cn
@@ -10,6 +10,9 @@ pip install lfss --no-deps
10
10
  Then set the `LFSS_ENDPOINT`, `LFSS_TOKEN` environment variables,
11
11
  then you can use the following commands:
12
12
  ```sh
13
+ # Check current user information
14
+ lfss whoami
15
+
13
16
  # Query a path
14
17
  lfss query remote/file[/or_dir/]
15
18
 
@@ -1,5 +1,10 @@
1
1
  ## 0.11
2
2
 
3
+ ### 0.11.6
4
+ - Hint copy and move success for frontend.
5
+ - Add query user info and list peers api.
6
+ - Add user with random password if not specified.
7
+
3
8
  ### 0.11.5
4
9
  - Script entry default to client CLI.
5
10
  - Fix single file download name deduce with decoding.
@@ -249,6 +249,7 @@ export default class Connector {
249
249
  /**
250
250
  * @param {string} path - the path to the file directory, should ends with '/'
251
251
  * @returns {Promise<PathListResponse>} - the promise of the request
252
+ * NOTE: will deprecated in future
252
253
  */
253
254
  async listPath(path){
254
255
  path = this._sanitizeDirPath(path);
@@ -347,6 +347,7 @@ async function refreshFileList(){
347
347
  console.log("Moving", dirurl, "to", dstPath);
348
348
  conn.move(dirurl, dstPath)
349
349
  .then(() => {
350
+ showPopup('Successfully moved path.', {level: 'success', timeout: 3000});
350
351
  refreshFileList();
351
352
  },
352
353
  (err) => {
@@ -371,6 +372,7 @@ async function refreshFileList(){
371
372
  console.log("Copying", dirurl, "to", dstPath);
372
373
  conn.copy(dirurl, dstPath)
373
374
  .then(() => {
375
+ showPopup('Successfully copied path.', {level: 'success', timeout: 3000});
374
376
  refreshFileList();
375
377
  },
376
378
  (err) => {
@@ -8,7 +8,7 @@ import urllib.parse
8
8
  from tempfile import SpooledTemporaryFile
9
9
  from lfss.eng.error import PathNotFoundError
10
10
  from lfss.eng.datatype import (
11
- FileReadPermission, FileRecord, DirectoryRecord, UserRecord, PathContents,
11
+ FileReadPermission, FileRecord, DirectoryRecord, UserRecord, PathContents, AccessLevel,
12
12
  FileSortKey, DirSortKey
13
13
  )
14
14
  from lfss.eng.utils import ensure_uri_components
@@ -316,4 +316,10 @@ class Connector:
316
316
  def whoami(self) -> UserRecord:
317
317
  """Gets information about the current user."""
318
318
  response = self._fetch_factory('GET', '_api/whoami')()
319
- return UserRecord(**response.json())
319
+ return UserRecord(**response.json())
320
+
321
+ def list_peers(self, level: AccessLevel = AccessLevel.READ, incoming: bool = False) -> list[UserRecord]:
322
+ """List all users that have at least the given access level to the current user."""
323
+ response = self._fetch_factory('GET', '_api/list-peers', {'level': int(level), 'incoming': incoming})()
324
+ users = [UserRecord(**u) for u in response.json()]
325
+ return users
@@ -1,8 +1,8 @@
1
1
  from pathlib import Path
2
2
  import argparse, typing, sys
3
3
  from lfss.api import Connector, upload_directory, upload_file, download_file, download_directory
4
- from lfss.eng.datatype import FileReadPermission, FileSortKey, DirSortKey
5
- from lfss.eng.utils import decode_uri_components
4
+ from lfss.eng.datatype import FileReadPermission, FileSortKey, DirSortKey, AccessLevel
5
+ from lfss.eng.utils import decode_uri_components, fmt_storage_size
6
6
  from . import catch_request_error, line_sep
7
7
 
8
8
  def parse_permission(s: str) -> FileReadPermission:
@@ -10,14 +10,27 @@ def parse_permission(s: str) -> FileReadPermission:
10
10
  if p.name.lower() == s.lower():
11
11
  return p
12
12
  raise ValueError(f"Invalid permission {s}")
13
+ def parse_access_level(s: str) -> AccessLevel:
14
+ for p in AccessLevel:
15
+ if p.name.lower() == s.lower():
16
+ return p
17
+ raise ValueError(f"Invalid access level {s}")
13
18
 
14
19
  def parse_arguments():
15
20
  parser = argparse.ArgumentParser(description="Client-side command line interface, set LFSS_ENDPOINT and LFSS_TOKEN environment variables for authentication.")
16
21
 
17
22
  sp = parser.add_subparsers(dest="command", required=True)
18
23
 
24
+ # whoami
25
+ sp_whoami = sp.add_parser("whoami", help="Show current user information")
26
+
27
+ # list peers
28
+ sp_peers = sp.add_parser("peers", help="Query users that you have access to or users that have access to you")
29
+ sp_peers.add_argument('-l', "--level", type=parse_access_level, default=AccessLevel.READ, help="Access level filter")
30
+ sp_peers.add_argument('-i', '--incoming', action='store_true', help="List users that have access to you (rather than you have access to them")
31
+
19
32
  # upload
20
- sp_upload = sp.add_parser("upload", help="Upload files")
33
+ sp_upload = sp.add_parser("upload", help="Upload file(s)")
21
34
  sp_upload.add_argument("src", help="Source file or directory", type=str)
22
35
  sp_upload.add_argument("dst", help="Destination url path", type=str)
23
36
  sp_upload.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
@@ -28,7 +41,7 @@ def parse_arguments():
28
41
  sp_upload.add_argument("--retries", type=int, default=0, help="Number of retries")
29
42
 
30
43
  # download
31
- sp_download = sp.add_parser("download", help="Download files")
44
+ sp_download = sp.add_parser("download", help="Download file(s)")
32
45
  sp_download.add_argument("src", help="Source url path", type=str)
33
46
  sp_download.add_argument("dst", help="Destination file or directory", type=str)
34
47
  sp_download.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
@@ -38,7 +51,7 @@ def parse_arguments():
38
51
  sp_download.add_argument("--retries", type=int, default=0, help="Number of retries")
39
52
 
40
53
  # query
41
- sp_query = sp.add_parser("query", help="Query files or directories metadata from the server")
54
+ sp_query = sp.add_parser("query", help="Query file or directories metadata from the server")
42
55
  sp_query.add_argument("path", help="Path to query", nargs="*", type=str)
43
56
 
44
57
  # list directories
@@ -65,7 +78,28 @@ def parse_arguments():
65
78
  def main():
66
79
  args = parse_arguments()
67
80
  connector = Connector()
68
- if args.command == "upload":
81
+ if args.command == "whoami":
82
+ with catch_request_error():
83
+ user = connector.whoami()
84
+ print("Username:", user.username)
85
+ print("User ID:", user.id)
86
+ print("Is Admin:", bool(user.is_admin))
87
+ print("Max Storage:", fmt_storage_size(user.max_storage))
88
+ print("Default Permission:", user.permission.name)
89
+ print("Created At:", user.create_time)
90
+ print("Last Active:", user.last_active)
91
+
92
+ elif args.command == "peers":
93
+ with catch_request_error():
94
+ users = connector.list_peers(level=args.level, incoming=args.incoming)
95
+ if not args.incoming:
96
+ print(f"Peers that you have {args.level.name} access to:")
97
+ else:
98
+ print(f"Peers that have {args.level.name} access to you:")
99
+ for i, u in enumerate(line_sep(users)):
100
+ print(f"[{i+1}] {u.username} (id={u.id})")
101
+
102
+ elif args.command == "upload":
69
103
  src_path = Path(args.src)
70
104
  if src_path.is_dir():
71
105
  failed_upload = upload_directory(
@@ -1,4 +1,4 @@
1
- import argparse, asyncio, os
1
+ import argparse, asyncio, os, secrets
2
2
  from contextlib import asynccontextmanager
3
3
  from .cli import parse_permission, FileReadPermission
4
4
  from ..eng.utils import parse_storage_size, fmt_storage_size
@@ -18,10 +18,10 @@ async def _main():
18
18
  sp = parser.add_subparsers(dest='subparser_name', required=True)
19
19
  sp_add = sp.add_parser('add')
20
20
  sp_add.add_argument('username', type=str)
21
- sp_add.add_argument('password', type=str)
21
+ sp_add.add_argument('password', nargs='?', type=str, default=None)
22
22
  sp_add.add_argument('--admin', action='store_true', help='Set user as admin')
23
23
  sp_add.add_argument("--permission", type=parse_permission, default=FileReadPermission.UNSET, help="File permission, can be public, protected, private, or unset")
24
- sp_add.add_argument('--max-storage', type=parse_storage_size, default="1G", help="Maximum storage size, e.g. 1G, 100M, 10K")
24
+ sp_add.add_argument('--max-storage', type=parse_storage_size, default="10G", help="Maximum storage size, e.g. 1G, 100M, 10K, default is 10G")
25
25
 
26
26
  sp_delete = sp.add_parser('delete')
27
27
  sp_delete.add_argument('username', type=str)
@@ -58,6 +58,9 @@ async def _main():
58
58
 
59
59
  if args.subparser_name == 'add':
60
60
  async with get_uconn() as uconn:
61
+ if args.password is None:
62
+ passwd = secrets.token_urlsafe(16)
63
+ args.password = passwd
61
64
  await uconn.create_user(args.username, args.password, args.admin, max_storage=args.max_storage, permission=args.permission)
62
65
  user = await uconn.get_user(args.username)
63
66
  assert user is not None
@@ -163,25 +163,31 @@ class UserConn(DBObjectBase):
163
163
  return AccessLevel.NONE
164
164
  return AccessLevel(res[0])
165
165
 
166
- async def list_peer_users(self, src_user: int | str, level: AccessLevel) -> list[UserRecord]:
166
+ async def list_peer_users(self, user: int | str, level: AccessLevel, incoming = False) -> list[UserRecord]:
167
167
  """
168
- List all users that src_user can do [AliasLevel] to, with level >= level,
169
- Note: the returned list does not include src_user and is not apporiate for admin (who has all permissions for all users)
168
+ if not incoming:
169
+ List all users that user can do [AliasLevel] to, with level >= level,
170
+ else:
171
+ List all users that can do [AliasLevel] to user, with level >= level
172
+ Note: the returned list does not include user and is not apporiate for admin (who has all permissions for all users)
170
173
  """
171
174
  assert int(level) > AccessLevel.NONE, f"Invalid level, {level}"
172
- match src_user:
175
+ aim_field = 'src_user_id' if incoming else 'dst_user_id'
176
+ query_field = 'dst_user_id' if incoming else 'src_user_id'
177
+
178
+ match user:
173
179
  case int():
174
- await self.cur.execute("""
180
+ await self.cur.execute(f"""
175
181
  SELECT * FROM user WHERE id IN (
176
- SELECT dst_user_id FROM upeer WHERE src_user_id = ? AND access_level >= ?
182
+ SELECT {aim_field} FROM upeer WHERE {query_field} = ? AND access_level >= ?
177
183
  )
178
- """, (src_user, int(level)))
184
+ """, (user, int(level)))
179
185
  case str():
180
- await self.cur.execute("""
186
+ await self.cur.execute(f"""
181
187
  SELECT * FROM user WHERE id IN (
182
- SELECT dst_user_id FROM upeer WHERE src_user_id = (SELECT id FROM user WHERE username = ?) AND access_level >= ?
188
+ SELECT {aim_field} FROM upeer WHERE {query_field} = (SELECT id FROM user WHERE username = ?) AND access_level >= ?
183
189
  )
184
- """, (src_user, int(level)))
190
+ """, (user, int(level)))
185
191
  case _:
186
192
  raise ValueError("Invalid arguments")
187
193
  res = await self.cur.fetchall()
@@ -8,7 +8,7 @@ from fastapi.exceptions import HTTPException
8
8
  from ..eng.utils import ensure_uri_components
9
9
  from ..eng.config import MAX_MEM_FILE_BYTES
10
10
  from ..eng.connection_pool import unique_cursor
11
- from ..eng.database import check_file_read_permission, check_path_permission, FileConn, delayed_log_access
11
+ from ..eng.database import check_file_read_permission, check_path_permission, FileConn, delayed_log_access, UserConn
12
12
  from ..eng.datatype import (
13
13
  FileReadPermission, UserRecord, AccessLevel,
14
14
  FileSortKey, DirSortKey
@@ -185,6 +185,16 @@ async def copy_file(
185
185
  ):
186
186
  return await copy_impl(src_path = src, dst_path = dst, op_user = user)
187
187
 
188
+ @router_api.get("/list-peers")
189
+ @handle_exception
190
+ async def list_peers(user: UserRecord = Depends(registered_user), level: AccessLevel = AccessLevel.READ, incoming: bool = False):
191
+ async with unique_cursor() as conn:
192
+ uconn = UserConn(conn)
193
+ peer_users = await uconn.list_peer_users(user.id, level, incoming=incoming)
194
+ for u in peer_users:
195
+ u.credential = "__HIDDEN__"
196
+ return peer_users
197
+
188
198
  async def validate_path_read_permission(path: str, user: UserRecord):
189
199
  if not path.endswith("/"):
190
200
  raise HTTPException(status_code=400, detail="Path must end with /")
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "lfss"
3
- version = "0.11.5"
4
- description = "Lightweight file storage service"
3
+ version = "0.11.6"
4
+ description = "Lite file storage service"
5
5
  authors = ["Li, Mengxun <mengxunli@whu.edu.cn>"]
6
6
  readme = "Readme.md"
7
7
  homepage = "https://github.com/MenxLi/lfss"
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
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
File without changes
File without changes
File without changes
File without changes
File without changes