lfss 0.11.4__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.4 → lfss-0.11.6}/PKG-INFO +2 -2
  2. lfss-0.11.6/docs/Client.md +40 -0
  3. {lfss-0.11.4 → lfss-0.11.6}/docs/changelog.md +21 -0
  4. {lfss-0.11.4 → lfss-0.11.6}/frontend/api.js +1 -0
  5. {lfss-0.11.4 → lfss-0.11.6}/frontend/scripts.js +2 -0
  6. {lfss-0.11.4 → lfss-0.11.6}/lfss/api/__init__.py +3 -3
  7. {lfss-0.11.4 → lfss-0.11.6}/lfss/api/connector.py +10 -4
  8. {lfss-0.11.4 → lfss-0.11.6}/lfss/cli/cli.py +42 -8
  9. {lfss-0.11.4 → lfss-0.11.6}/lfss/cli/user.py +6 -3
  10. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/database.py +19 -13
  11. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/utils.py +4 -4
  12. {lfss-0.11.4 → lfss-0.11.6}/lfss/svc/app_dav.py +6 -6
  13. {lfss-0.11.4 → lfss-0.11.6}/lfss/svc/app_native.py +22 -12
  14. {lfss-0.11.4 → lfss-0.11.6}/lfss/svc/common_impl.py +7 -7
  15. {lfss-0.11.4 → lfss-0.11.6}/pyproject.toml +4 -3
  16. {lfss-0.11.4 → lfss-0.11.6}/Readme.md +0 -0
  17. {lfss-0.11.4 → lfss-0.11.6}/docs/Enviroment_variables.md +0 -0
  18. {lfss-0.11.4 → lfss-0.11.6}/docs/Known_issues.md +0 -0
  19. {lfss-0.11.4 → lfss-0.11.6}/docs/Permission.md +0 -0
  20. {lfss-0.11.4 → lfss-0.11.6}/docs/Webdav.md +0 -0
  21. {lfss-0.11.4 → lfss-0.11.6}/frontend/index.html +0 -0
  22. {lfss-0.11.4 → lfss-0.11.6}/frontend/info.css +0 -0
  23. {lfss-0.11.4 → lfss-0.11.6}/frontend/info.js +0 -0
  24. {lfss-0.11.4 → lfss-0.11.6}/frontend/login.css +0 -0
  25. {lfss-0.11.4 → lfss-0.11.6}/frontend/login.js +0 -0
  26. {lfss-0.11.4 → lfss-0.11.6}/frontend/popup.css +0 -0
  27. {lfss-0.11.4 → lfss-0.11.6}/frontend/popup.js +0 -0
  28. {lfss-0.11.4 → lfss-0.11.6}/frontend/state.js +0 -0
  29. {lfss-0.11.4 → lfss-0.11.6}/frontend/styles.css +0 -0
  30. {lfss-0.11.4 → lfss-0.11.6}/frontend/thumb.css +0 -0
  31. {lfss-0.11.4 → lfss-0.11.6}/frontend/thumb.js +0 -0
  32. {lfss-0.11.4 → lfss-0.11.6}/frontend/utils.js +0 -0
  33. {lfss-0.11.4 → lfss-0.11.6}/lfss/cli/__init__.py +0 -0
  34. {lfss-0.11.4 → lfss-0.11.6}/lfss/cli/balance.py +0 -0
  35. {lfss-0.11.4 → lfss-0.11.6}/lfss/cli/log.py +0 -0
  36. {lfss-0.11.4 → lfss-0.11.6}/lfss/cli/panel.py +0 -0
  37. {lfss-0.11.4 → lfss-0.11.6}/lfss/cli/serve.py +0 -0
  38. {lfss-0.11.4 → lfss-0.11.6}/lfss/cli/vacuum.py +0 -0
  39. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/__init__.py +0 -0
  40. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/bounded_pool.py +0 -0
  41. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/config.py +0 -0
  42. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/connection_pool.py +0 -0
  43. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/datatype.py +0 -0
  44. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/error.py +0 -0
  45. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/log.py +0 -0
  46. {lfss-0.11.4 → lfss-0.11.6}/lfss/eng/thumb.py +0 -0
  47. {lfss-0.11.4 → lfss-0.11.6}/lfss/sql/init.sql +0 -0
  48. {lfss-0.11.4 → lfss-0.11.6}/lfss/sql/pragma.sql +0 -0
  49. {lfss-0.11.4 → lfss-0.11.6}/lfss/svc/app.py +0 -0
  50. {lfss-0.11.4 → lfss-0.11.6}/lfss/svc/app_base.py +0 -0
  51. {lfss-0.11.4 → 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.4
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
@@ -0,0 +1,40 @@
1
+
2
+ # Client-side CLI tools
3
+
4
+ To install python CLI tools without dependencies (to avoid conflicts with your existing packages):
5
+ ```sh
6
+ pip install requests
7
+ pip install lfss --no-deps
8
+ ```
9
+
10
+ Then set the `LFSS_ENDPOINT`, `LFSS_TOKEN` environment variables,
11
+ then you can use the following commands:
12
+ ```sh
13
+ # Check current user information
14
+ lfss whoami
15
+
16
+ # Query a path
17
+ lfss query remote/file[/or_dir/]
18
+
19
+ # List directories of a specified path
20
+ lfss list-dirs remote/dir/
21
+
22
+ # List files of a specified path,
23
+ # with pagination and sorting
24
+ lfss list-files --offset 0 --limit 100 --order access_time remote/dir/
25
+
26
+ # Upload a file
27
+ lfss upload local/file.txt remote/file.txt
28
+
29
+ # Upload a directory, note the ending slashes
30
+ lfss upload local/dir/ remote/dir/
31
+
32
+ # Download a file
33
+ lfss download remote/file.txt local/file.txt
34
+
35
+ # Download a directory, with verbose output and 8 concurrent jobs
36
+ # Overwrite existing files
37
+ lfss download -v -j 8 --conflict overwrite remote/dir/ local/dir/
38
+ ```
39
+
40
+ More commands can be found using `lfss-cli --help`.
@@ -1,5 +1,26 @@
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
+
8
+ ### 0.11.5
9
+ - Script entry default to client CLI.
10
+ - Fix single file download name deduce with decoding.
11
+ - Fix code misspell (minor).
12
+
13
+ ### 0.11.4
14
+ - Fix SQL query for LIKE clause to escape special characters in path.
15
+
16
+ ### 0.11.3
17
+ - Add method to get multiple files, maybe with content, at once.
18
+ - Allow copy directory files that the user is not the owner of.
19
+ - Environment variables to set origin and disable file logging.
20
+ - Fix error handling for some endpoints.
21
+ - Redirect CLI error output to stderr.
22
+ - Increase thumb image size to 64x64.
23
+
3
24
  ### 0.11.2
4
25
  - Improve frontend directory upload feedback.
5
26
  - Set default large file threashold to 1M.
@@ -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) => {
@@ -2,7 +2,7 @@ import os, time, pathlib
2
2
  from threading import Lock
3
3
  from .connector import Connector
4
4
  from ..eng.datatype import FileRecord
5
- from ..eng.utils import decode_uri_compnents
5
+ from ..eng.utils import decode_uri_components
6
6
  from ..eng.bounded_pool import BoundedThreadPoolExecutor
7
7
 
8
8
  def upload_file(
@@ -105,7 +105,7 @@ def download_file(
105
105
  assert not src_url.endswith('/'), "Source URL must not end with a slash."
106
106
  while this_try <= n_retries:
107
107
  if os.path.isdir(file_path):
108
- fname = src_url.split('/')[-1]
108
+ fname = decode_uri_components(src_url.split('/')[-1])
109
109
  file_path = os.path.join(file_path, fname)
110
110
 
111
111
  if not overwrite and os.path.exists(file_path):
@@ -176,7 +176,7 @@ def download_directory(
176
176
  with _counter_lock:
177
177
  _counter += 1
178
178
  this_count = _counter
179
- dst_path = f"{directory}{os.path.relpath(decode_uri_compnents(src_url), decode_uri_compnents(src_path))}"
179
+ dst_path = f"{directory}{os.path.relpath(decode_uri_components(src_url), decode_uri_components(src_path))}"
180
180
  if verbose:
181
181
  print(f"[{this_count}/{file_count}] Downloading {src_url} to {dst_path}")
182
182
 
@@ -8,10 +8,10 @@ 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
- from lfss.eng.utils import ensure_uri_compnents
14
+ from lfss.eng.utils import ensure_uri_components
15
15
 
16
16
  _default_endpoint = os.environ.get('LFSS_ENDPOINT', 'http://localhost:8000')
17
17
  _default_token = os.environ.get('LFSS_TOKEN', '')
@@ -74,7 +74,7 @@ class Connector:
74
74
  ):
75
75
  if path.startswith('/'):
76
76
  path = path[1:]
77
- path = ensure_uri_compnents(path)
77
+ path = ensure_uri_components(path)
78
78
  def f(**kwargs):
79
79
  search_params_t = [
80
80
  (k, str(v).lower() if isinstance(v, bool) else v)
@@ -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_compnents
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(
@@ -143,7 +177,7 @@ def main():
143
177
  order_desc=args.reverse,
144
178
  )
145
179
  for i, f in enumerate(line_sep(res)):
146
- f.url = decode_uri_compnents(f.url)
180
+ f.url = decode_uri_components(f.url)
147
181
  print(f"[{i+1}] {f if args.long else f.url}")
148
182
 
149
183
  if len(res) == args.limit:
@@ -160,7 +194,7 @@ def main():
160
194
  order_desc=args.reverse,
161
195
  )
162
196
  for i, d in enumerate(line_sep(res)):
163
- d.url = decode_uri_compnents(d.url)
197
+ d.url = decode_uri_components(d.url)
164
198
  print(f"[{i+1}] {d if args.long else d.url}")
165
199
 
166
200
  if len(res) == args.limit:
@@ -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
@@ -21,7 +21,7 @@ from .datatype import (
21
21
  )
22
22
  from .config import LARGE_BLOB_DIR, CHUNK_SIZE, LARGE_FILE_BYTES, MAX_MEM_FILE_BYTES
23
23
  from .log import get_logger
24
- from .utils import decode_uri_compnents, hash_credential, concurrent_wrap, debounce_async, static_vars
24
+ from .utils import decode_uri_components, hash_credential, concurrent_wrap, debounce_async, static_vars
25
25
  from .error import *
26
26
 
27
27
  class DBObjectBase(ABC):
@@ -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()
@@ -1108,7 +1114,7 @@ class Database:
1108
1114
  async def data_iter():
1109
1115
  async for (r, blob) in self.iter_dir(top_url, None):
1110
1116
  rel_path = r.url[len(top_url):]
1111
- rel_path = decode_uri_compnents(rel_path)
1117
+ rel_path = decode_uri_components(rel_path)
1112
1118
  b_iter: AsyncIterable[bytes]
1113
1119
  if isinstance(blob, bytes):
1114
1120
  async def blob_iter(): yield blob
@@ -1138,7 +1144,7 @@ class Database:
1138
1144
  with zipfile.ZipFile(buffer, 'w') as zf:
1139
1145
  async for (r, blob) in self.iter_dir(top_url, None):
1140
1146
  rel_path = r.url[len(top_url):]
1141
- rel_path = decode_uri_compnents(rel_path)
1147
+ rel_path = decode_uri_components(rel_path)
1142
1148
  if r.external:
1143
1149
  assert isinstance(blob, AsyncIterable)
1144
1150
  zf.writestr(rel_path, b''.join([chunk async for chunk in blob]))
@@ -21,19 +21,19 @@ async def copy_file(source: str|pathlib.Path, destination: str|pathlib.Path):
21
21
  def hash_credential(username: str, password: str):
22
22
  return hashlib.sha256(f"{username}:{password}".encode()).hexdigest()
23
23
 
24
- def encode_uri_compnents(path: str):
24
+ def encode_uri_components(path: str):
25
25
  path_sp = path.split("/")
26
26
  mapped = map(lambda x: urllib.parse.quote(x), path_sp)
27
27
  return "/".join(mapped)
28
28
 
29
- def decode_uri_compnents(path: str):
29
+ def decode_uri_components(path: str):
30
30
  path_sp = path.split("/")
31
31
  mapped = map(lambda x: urllib.parse.unquote(x), path_sp)
32
32
  return "/".join(mapped)
33
33
 
34
- def ensure_uri_compnents(path: str):
34
+ def ensure_uri_components(path: str):
35
35
  """ Ensure the path components are safe to use """
36
- return encode_uri_compnents(decode_uri_compnents(path))
36
+ return encode_uri_components(decode_uri_components(path))
37
37
 
38
38
  class TaskManager:
39
39
  def __init__(self):
@@ -11,7 +11,7 @@ from ..eng.error import *
11
11
  from ..eng.config import DATA_HOME, DEBUG_MODE
12
12
  from ..eng.datatype import UserRecord, FileRecord, DirectoryRecord, AccessLevel
13
13
  from ..eng.database import FileConn, UserConn, check_path_permission
14
- from ..eng.utils import ensure_uri_compnents, decode_uri_compnents, format_last_modified, static_vars
14
+ from ..eng.utils import ensure_uri_components, decode_uri_components, format_last_modified, static_vars
15
15
  from .app_base import *
16
16
  from .common_impl import copy_impl
17
17
 
@@ -36,7 +36,7 @@ async def eval_path(path: str) -> tuple[ptype, str, Optional[FileRecord | Direct
36
36
  and should end with / if it is a directory, otherwise it is a file
37
37
  record is the FileRecord or DirectoryRecord object, it is None if the path does not exist
38
38
  """
39
- path = decode_uri_compnents(path)
39
+ path = decode_uri_components(path)
40
40
  if "://" in path:
41
41
  if not path.startswith("http://") and not path.startswith("https://"):
42
42
  raise HTTPException(status_code=400, detail="Bad Request, unsupported protocol")
@@ -47,7 +47,7 @@ async def eval_path(path: str) -> tuple[ptype, str, Optional[FileRecord | Direct
47
47
  assert path.startswith(route_prefix), "Path should start with the route prefix, got: " + path
48
48
  path = path[len(route_prefix):]
49
49
 
50
- path = ensure_uri_compnents(path)
50
+ path = ensure_uri_components(path)
51
51
  if path.startswith("/"): path = path[1:]
52
52
 
53
53
  # path now is url-safe and without leading slash
@@ -160,7 +160,7 @@ async def create_file_xml_element(frecord: FileRecord) -> ET.Element:
160
160
  href.text = f"/{frecord.url}"
161
161
  propstat = ET.SubElement(file_el, f"{{{DAV_NS}}}propstat")
162
162
  prop = ET.SubElement(propstat, f"{{{DAV_NS}}}prop")
163
- ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = decode_uri_compnents(frecord.url.split("/")[-1])
163
+ ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = decode_uri_components(frecord.url.split("/")[-1])
164
164
  ET.SubElement(prop, f"{{{DAV_NS}}}resourcetype")
165
165
  ET.SubElement(prop, f"{{{DAV_NS}}}getcontentlength").text = str(frecord.file_size)
166
166
  ET.SubElement(prop, f"{{{DAV_NS}}}getlastmodified").text = format_last_modified(frecord.create_time)
@@ -178,7 +178,7 @@ async def create_dir_xml_element(drecord: DirectoryRecord) -> ET.Element:
178
178
  href.text = f"/{drecord.url}"
179
179
  propstat = ET.SubElement(dir_el, f"{{{DAV_NS}}}propstat")
180
180
  prop = ET.SubElement(propstat, f"{{{DAV_NS}}}prop")
181
- ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = decode_uri_compnents(drecord.url.split("/")[-2])
181
+ ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = decode_uri_components(drecord.url.split("/")[-2])
182
182
  ET.SubElement(prop, f"{{{DAV_NS}}}resourcetype").append(ET.Element(f"{{{DAV_NS}}}collection"))
183
183
  if drecord.size >= 0:
184
184
  ET.SubElement(prop, f"{{{DAV_NS}}}getlastmodified").text = format_last_modified(drecord.create_time)
@@ -211,7 +211,7 @@ async def dav_options(request: Request, path: str):
211
211
  @handle_exception
212
212
  async def dav_propfind(request: Request, path: str, user: UserRecord = Depends(registered_user), body: Optional[ET.Element] = Depends(xml_request_body)):
213
213
  if path.startswith("/"): path = path[1:]
214
- path = ensure_uri_compnents(path)
214
+ path = ensure_uri_components(path)
215
215
 
216
216
  if body and DEBUG_MODE:
217
217
  print("Propfind-body:", ET.tostring(body, encoding="utf-8", method="xml"))
@@ -5,10 +5,10 @@ from fastapi import Depends, Request, Response, UploadFile, Query
5
5
  from fastapi.responses import StreamingResponse, JSONResponse
6
6
  from fastapi.exceptions import HTTPException
7
7
 
8
- from ..eng.utils import ensure_uri_compnents
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
@@ -83,7 +83,7 @@ async def delete_file(path: str, user: UserRecord = Depends(registered_user)):
83
83
  @handle_exception
84
84
  async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
85
85
  logger.info(f"GET bundle({path}), user: {user.username}")
86
- path = ensure_uri_compnents(path)
86
+ path = ensure_uri_components(path)
87
87
  if not path.endswith("/"):
88
88
  raise HTTPException(status_code=400, detail="Path must end with /")
89
89
  if path[0] == "/": # adapt to both /path and path
@@ -123,7 +123,7 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
123
123
  @handle_exception
124
124
  async def get_file_meta(path: str, user: UserRecord = Depends(registered_user)):
125
125
  logger.info(f"GET meta({path}), user: {user.username}")
126
- path = ensure_uri_compnents(path)
126
+ path = ensure_uri_components(path)
127
127
  is_file = not path.endswith("/")
128
128
  async with unique_cursor() as cur:
129
129
  fconn = FileConn(cur)
@@ -147,7 +147,7 @@ async def update_file_meta(
147
147
  new_path: Optional[str] = None,
148
148
  user: UserRecord = Depends(registered_user)
149
149
  ):
150
- path = ensure_uri_compnents(path)
150
+ path = ensure_uri_components(path)
151
151
  if path.startswith("/"):
152
152
  path = path[1:]
153
153
 
@@ -162,7 +162,7 @@ async def update_file_meta(
162
162
  )
163
163
 
164
164
  if new_path is not None:
165
- new_path = ensure_uri_compnents(new_path)
165
+ new_path = ensure_uri_components(new_path)
166
166
  logger.info(f"Update path of {path} to {new_path}")
167
167
  await db.move_file(path, new_path, user)
168
168
 
@@ -170,7 +170,7 @@ async def update_file_meta(
170
170
  else:
171
171
  assert perm is None, "Permission is not supported for directory"
172
172
  if new_path is not None:
173
- new_path = ensure_uri_compnents(new_path)
173
+ new_path = ensure_uri_components(new_path)
174
174
  logger.info(f"Update path of {path} to {new_path}")
175
175
  # will raise duplicate path error if same name path exists in the new path
176
176
  await db.move_dir(path, new_path, user)
@@ -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 /")
@@ -194,7 +204,7 @@ async def validate_path_read_permission(path: str, user: UserRecord):
194
204
  @handle_exception
195
205
  async def count_files(path: str, flat: bool = False, user: UserRecord = Depends(registered_user)):
196
206
  await validate_path_read_permission(path, user)
197
- path = ensure_uri_compnents(path)
207
+ path = ensure_uri_components(path)
198
208
  async with unique_cursor() as conn:
199
209
  fconn = FileConn(conn)
200
210
  return { "count": await fconn.count_dir_files(url = path, flat = flat) }
@@ -206,7 +216,7 @@ async def list_files(
206
216
  flat: bool = False, user: UserRecord = Depends(registered_user)
207
217
  ):
208
218
  await validate_path_read_permission(path, user)
209
- path = ensure_uri_compnents(path)
219
+ path = ensure_uri_components(path)
210
220
  async with unique_cursor() as conn:
211
221
  fconn = FileConn(conn)
212
222
  return await fconn.list_dir_files(
@@ -219,7 +229,7 @@ async def list_files(
219
229
  @handle_exception
220
230
  async def count_dirs(path: str, user: UserRecord = Depends(registered_user)):
221
231
  await validate_path_read_permission(path, user)
222
- path = ensure_uri_compnents(path)
232
+ path = ensure_uri_components(path)
223
233
  async with unique_cursor() as conn:
224
234
  fconn = FileConn(conn)
225
235
  return { "count": await fconn.count_path_dirs(url = path) }
@@ -231,7 +241,7 @@ async def list_dirs(
231
241
  skim: bool = True, user: UserRecord = Depends(registered_user)
232
242
  ):
233
243
  await validate_path_read_permission(path, user)
234
- path = ensure_uri_compnents(path)
244
+ path = ensure_uri_components(path)
235
245
  async with unique_cursor() as conn:
236
246
  fconn = FileConn(conn)
237
247
  return await fconn.list_path_dirs(
@@ -263,7 +273,7 @@ async def get_multiple_files(
263
273
  upath2path = OrderedDict[str, str]()
264
274
  for p in path:
265
275
  p_ = p if not p.startswith("/") else p[1:]
266
- upath2path[ensure_uri_compnents(p_)] = p
276
+ upath2path[ensure_uri_components(p_)] = p
267
277
  upaths = list(upath2path.keys())
268
278
 
269
279
  # get files
@@ -6,7 +6,7 @@ from ..eng.connection_pool import unique_cursor
6
6
  from ..eng.datatype import UserRecord, FileRecord, PathContents, AccessLevel, FileReadPermission
7
7
  from ..eng.database import FileConn, UserConn, delayed_log_access, check_file_read_permission, check_path_permission
8
8
  from ..eng.thumb import get_thumb
9
- from ..eng.utils import format_last_modified, ensure_uri_compnents
9
+ from ..eng.utils import format_last_modified, ensure_uri_components
10
10
  from ..eng.config import CHUNK_SIZE, DEBUG_MODE
11
11
 
12
12
  from .app_base import skip_request_log, db, logger
@@ -100,7 +100,7 @@ async def get_impl(
100
100
  thumb: bool = False,
101
101
  is_head = False,
102
102
  ):
103
- path = ensure_uri_compnents(path)
103
+ path = ensure_uri_components(path)
104
104
  if path.startswith("/"): path = path[1:]
105
105
 
106
106
  # handle directory query
@@ -194,7 +194,7 @@ async def put_file_impl(
194
194
  conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
195
195
  permission: int = 0,
196
196
  ):
197
- path = ensure_uri_compnents(path)
197
+ path = ensure_uri_components(path)
198
198
  assert not path.endswith("/"), "Path must not end with /"
199
199
 
200
200
  access_level = await check_path_permission(path, user)
@@ -249,7 +249,7 @@ async def post_file_impl(
249
249
  conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
250
250
  permission: int = 0,
251
251
  ):
252
- path = ensure_uri_compnents(path)
252
+ path = ensure_uri_components(path)
253
253
  assert not path.endswith("/"), "Path must not end with /"
254
254
 
255
255
  access_level = await check_path_permission(path, user)
@@ -288,7 +288,7 @@ async def post_file_impl(
288
288
  }, content=json.dumps({"url": path}))
289
289
 
290
290
  async def delete_impl(path: str, user: UserRecord):
291
- path = ensure_uri_compnents(path)
291
+ path = ensure_uri_components(path)
292
292
  if await check_path_permission(path, user) < AccessLevel.WRITE:
293
293
  raise HTTPException(status_code=403, detail="Permission denied")
294
294
 
@@ -307,8 +307,8 @@ async def delete_impl(path: str, user: UserRecord):
307
307
  async def copy_impl(
308
308
  op_user: UserRecord, src_path: str, dst_path: str,
309
309
  ):
310
- src_path = ensure_uri_compnents(src_path)
311
- dst_path = ensure_uri_compnents(dst_path)
310
+ src_path = ensure_uri_components(src_path)
311
+ dst_path = ensure_uri_components(dst_path)
312
312
  copy_type = "file" if not src_path[-1] == "/" else "directory"
313
313
  if (src_path[-1] == "/") != (dst_path[-1] == "/"):
314
314
  raise HTTPException(status_code=400, detail="Source and destination must be same type")
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "lfss"
3
- version = "0.11.4"
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"
@@ -30,10 +30,11 @@ webdavclient3 = "*"
30
30
  lfss-serve = "lfss.cli.serve:main"
31
31
  lfss-user = "lfss.cli.user:main"
32
32
  lfss-panel = "lfss.cli.panel:main"
33
- lfss-cli = "lfss.cli.cli:main"
34
33
  lfss-vacuum = "lfss.cli.vacuum:main"
35
34
  lfss-balance = "lfss.cli.balance:main"
36
35
  lfss-log = "lfss.cli.log:main"
36
+ lfss-cli = "lfss.cli.cli:main"
37
+ lfss = "lfss.cli.cli:main"
37
38
 
38
39
  [build-system]
39
40
  requires = ["poetry-core>=1.0.0"]
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