lfss 0.11.3__tar.gz → 0.11.5__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.3 → lfss-0.11.5}/PKG-INFO +1 -1
  2. lfss-0.11.5/docs/Client.md +37 -0
  3. {lfss-0.11.3 → lfss-0.11.5}/docs/changelog.md +16 -0
  4. {lfss-0.11.3 → lfss-0.11.5}/lfss/api/__init__.py +3 -3
  5. {lfss-0.11.3 → lfss-0.11.5}/lfss/api/connector.py +2 -2
  6. {lfss-0.11.3 → lfss-0.11.5}/lfss/cli/cli.py +3 -3
  7. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/database.py +57 -25
  8. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/utils.py +4 -4
  9. {lfss-0.11.3 → lfss-0.11.5}/lfss/svc/app_dav.py +6 -6
  10. {lfss-0.11.3 → lfss-0.11.5}/lfss/svc/app_native.py +11 -11
  11. {lfss-0.11.3 → lfss-0.11.5}/lfss/svc/common_impl.py +7 -7
  12. {lfss-0.11.3 → lfss-0.11.5}/pyproject.toml +3 -2
  13. {lfss-0.11.3 → lfss-0.11.5}/Readme.md +0 -0
  14. {lfss-0.11.3 → lfss-0.11.5}/docs/Enviroment_variables.md +0 -0
  15. {lfss-0.11.3 → lfss-0.11.5}/docs/Known_issues.md +0 -0
  16. {lfss-0.11.3 → lfss-0.11.5}/docs/Permission.md +0 -0
  17. {lfss-0.11.3 → lfss-0.11.5}/docs/Webdav.md +0 -0
  18. {lfss-0.11.3 → lfss-0.11.5}/frontend/api.js +0 -0
  19. {lfss-0.11.3 → lfss-0.11.5}/frontend/index.html +0 -0
  20. {lfss-0.11.3 → lfss-0.11.5}/frontend/info.css +0 -0
  21. {lfss-0.11.3 → lfss-0.11.5}/frontend/info.js +0 -0
  22. {lfss-0.11.3 → lfss-0.11.5}/frontend/login.css +0 -0
  23. {lfss-0.11.3 → lfss-0.11.5}/frontend/login.js +0 -0
  24. {lfss-0.11.3 → lfss-0.11.5}/frontend/popup.css +0 -0
  25. {lfss-0.11.3 → lfss-0.11.5}/frontend/popup.js +0 -0
  26. {lfss-0.11.3 → lfss-0.11.5}/frontend/scripts.js +0 -0
  27. {lfss-0.11.3 → lfss-0.11.5}/frontend/state.js +0 -0
  28. {lfss-0.11.3 → lfss-0.11.5}/frontend/styles.css +0 -0
  29. {lfss-0.11.3 → lfss-0.11.5}/frontend/thumb.css +0 -0
  30. {lfss-0.11.3 → lfss-0.11.5}/frontend/thumb.js +0 -0
  31. {lfss-0.11.3 → lfss-0.11.5}/frontend/utils.js +0 -0
  32. {lfss-0.11.3 → lfss-0.11.5}/lfss/cli/__init__.py +0 -0
  33. {lfss-0.11.3 → lfss-0.11.5}/lfss/cli/balance.py +0 -0
  34. {lfss-0.11.3 → lfss-0.11.5}/lfss/cli/log.py +0 -0
  35. {lfss-0.11.3 → lfss-0.11.5}/lfss/cli/panel.py +0 -0
  36. {lfss-0.11.3 → lfss-0.11.5}/lfss/cli/serve.py +0 -0
  37. {lfss-0.11.3 → lfss-0.11.5}/lfss/cli/user.py +0 -0
  38. {lfss-0.11.3 → lfss-0.11.5}/lfss/cli/vacuum.py +0 -0
  39. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/__init__.py +0 -0
  40. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/bounded_pool.py +0 -0
  41. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/config.py +0 -0
  42. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/connection_pool.py +0 -0
  43. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/datatype.py +0 -0
  44. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/error.py +0 -0
  45. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/log.py +0 -0
  46. {lfss-0.11.3 → lfss-0.11.5}/lfss/eng/thumb.py +0 -0
  47. {lfss-0.11.3 → lfss-0.11.5}/lfss/sql/init.sql +0 -0
  48. {lfss-0.11.3 → lfss-0.11.5}/lfss/sql/pragma.sql +0 -0
  49. {lfss-0.11.3 → lfss-0.11.5}/lfss/svc/app.py +0 -0
  50. {lfss-0.11.3 → lfss-0.11.5}/lfss/svc/app_base.py +0 -0
  51. {lfss-0.11.3 → lfss-0.11.5}/lfss/svc/request_log.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.11.3
3
+ Version: 0.11.5
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: Li, Mengxun
@@ -0,0 +1,37 @@
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
+ # Query a path
14
+ lfss query remote/file[/or_dir/]
15
+
16
+ # List directories of a specified path
17
+ lfss list-dirs remote/dir/
18
+
19
+ # List files of a specified path,
20
+ # with pagination and sorting
21
+ lfss list-files --offset 0 --limit 100 --order access_time remote/dir/
22
+
23
+ # Upload a file
24
+ lfss upload local/file.txt remote/file.txt
25
+
26
+ # Upload a directory, note the ending slashes
27
+ lfss upload local/dir/ remote/dir/
28
+
29
+ # Download a file
30
+ lfss download remote/file.txt local/file.txt
31
+
32
+ # Download a directory, with verbose output and 8 concurrent jobs
33
+ # Overwrite existing files
34
+ lfss download -v -j 8 --conflict overwrite remote/dir/ local/dir/
35
+ ```
36
+
37
+ More commands can be found using `lfss-cli --help`.
@@ -1,5 +1,21 @@
1
1
  ## 0.11
2
2
 
3
+ ### 0.11.5
4
+ - Script entry default to client CLI.
5
+ - Fix single file download name deduce with decoding.
6
+ - Fix code misspell (minor).
7
+
8
+ ### 0.11.4
9
+ - Fix SQL query for LIKE clause to escape special characters in path.
10
+
11
+ ### 0.11.3
12
+ - Add method to get multiple files, maybe with content, at once.
13
+ - Allow copy directory files that the user is not the owner of.
14
+ - Environment variables to set origin and disable file logging.
15
+ - Fix error handling for some endpoints.
16
+ - Redirect CLI error output to stderr.
17
+ - Increase thumb image size to 64x64.
18
+
3
19
  ### 0.11.2
4
20
  - Improve frontend directory upload feedback.
5
21
  - Set default large file threashold to 1M.
@@ -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
 
@@ -11,7 +11,7 @@ from lfss.eng.datatype import (
11
11
  FileReadPermission, FileRecord, DirectoryRecord, UserRecord, PathContents,
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)
@@ -2,7 +2,7 @@ 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
4
  from lfss.eng.datatype import FileReadPermission, FileSortKey, DirSortKey
5
- from lfss.eng.utils import decode_uri_compnents
5
+ from lfss.eng.utils import decode_uri_components
6
6
  from . import catch_request_error, line_sep
7
7
 
8
8
  def parse_permission(s: str) -> FileReadPermission:
@@ -143,7 +143,7 @@ def main():
143
143
  order_desc=args.reverse,
144
144
  )
145
145
  for i, f in enumerate(line_sep(res)):
146
- f.url = decode_uri_compnents(f.url)
146
+ f.url = decode_uri_components(f.url)
147
147
  print(f"[{i+1}] {f if args.long else f.url}")
148
148
 
149
149
  if len(res) == args.limit:
@@ -160,7 +160,7 @@ def main():
160
160
  order_desc=args.reverse,
161
161
  )
162
162
  for i, d in enumerate(line_sep(res)):
163
- d.url = decode_uri_compnents(d.url)
163
+ d.url = decode_uri_components(d.url)
164
164
  print(f"[{i+1}] {d if args.long else d.url}")
165
165
 
166
166
  if len(res) == args.limit:
@@ -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):
@@ -197,6 +197,11 @@ class FileConn(DBObjectBase):
197
197
  def parse_record(record) -> FileRecord:
198
198
  return FileRecord(*record)
199
199
 
200
+ @staticmethod
201
+ def escape_sqlike(url: str) -> str:
202
+ """ Escape a url for use in SQL LIKE clause (The % and _ characters) """
203
+ return url.replace('%', r'\%').replace('_', r'\_')
204
+
200
205
  @overload
201
206
  async def get_file_record(self, url: str, throw: Literal[True]) -> FileRecord: ...
202
207
  @overload
@@ -246,9 +251,9 @@ class FileConn(DBObjectBase):
246
251
  url, LENGTH(?) + 1,
247
252
  INSTR(SUBSTR(url, LENGTH(?) + 1), '/')
248
253
  ) AS dirname
249
- FROM fmeta WHERE url LIKE ? AND dirname != ''
254
+ FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND dirname != ''
250
255
  )
251
- """, (url, url, url + '%'))
256
+ """, (url, url, self.escape_sqlike(url) + '%'))
252
257
  res = await cursor.fetchone()
253
258
  assert res is not None, "Error: count_path_dirs"
254
259
  return res[0]
@@ -271,11 +276,11 @@ class FileConn(DBObjectBase):
271
276
  1 + LENGTH(?),
272
277
  INSTR(SUBSTR(url, 1 + LENGTH(?)), '/')
273
278
  ) AS dirname
274
- FROM fmeta WHERE url LIKE ? AND dirname != ''
279
+ FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND dirname != ''
275
280
  """ \
276
281
  + (f"ORDER BY {order_by} {'DESC' if order_desc else 'ASC'}" if order_by else '') \
277
282
  + " LIMIT ? OFFSET ?"
278
- cursor = await self.cur.execute(sql_qury, (url, url, url + '%', limit, offset))
283
+ cursor = await self.cur.execute(sql_qury, (url, url, self.escape_sqlike(url) + '%', limit, offset))
279
284
  res = await cursor.fetchall()
280
285
  dirs_str = [r[0] for r in res]
281
286
  async def get_dir(dir_url):
@@ -290,9 +295,15 @@ class FileConn(DBObjectBase):
290
295
  if not url.endswith('/'): url += '/'
291
296
  if url == '/': url = ''
292
297
  if flat:
293
- cursor = await self.cur.execute("SELECT COUNT(*) FROM fmeta WHERE url LIKE ?", (url + '%', ))
298
+ cursor = await self.cur.execute(
299
+ "SELECT COUNT(*) FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
300
+ (self.escape_sqlike(url) + '%', )
301
+ )
294
302
  else:
295
- cursor = await self.cur.execute("SELECT COUNT(*) FROM fmeta WHERE url LIKE ? AND url NOT LIKE ?", (url + '%', url + '%/%'))
303
+ cursor = await self.cur.execute(
304
+ "SELECT COUNT(*) FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND url NOT LIKE ? ESCAPE '\\'",
305
+ (self.escape_sqlike(url) + '%', self.escape_sqlike(url) + '%/%')
306
+ )
296
307
  res = await cursor.fetchone()
297
308
  assert res is not None, "Error: count_path_files"
298
309
  return res[0]
@@ -309,14 +320,14 @@ class FileConn(DBObjectBase):
309
320
  if not url.endswith('/'): url += '/'
310
321
  if url == '/': url = ''
311
322
 
312
- sql_query = "SELECT * FROM fmeta WHERE url LIKE ?"
313
- if not flat: sql_query += " AND url NOT LIKE ?"
323
+ sql_query = "SELECT * FROM fmeta WHERE url LIKE ? ESCAPE '\\'"
324
+ if not flat: sql_query += " AND url NOT LIKE ? ESCAPE '\\'"
314
325
  if order_by: sql_query += f" ORDER BY {order_by} {'DESC' if order_desc else 'ASC'}"
315
326
  sql_query += " LIMIT ? OFFSET ?"
316
327
  if flat:
317
- cursor = await self.cur.execute(sql_query, (url + '%', limit, offset))
328
+ cursor = await self.cur.execute(sql_query, (self.escape_sqlike(url) + '%', limit, offset))
318
329
  else:
319
- cursor = await self.cur.execute(sql_query, (url + '%', url + '%/%', limit, offset))
330
+ cursor = await self.cur.execute(sql_query, (self.escape_sqlike(url) + '%', self.escape_sqlike(url) + '%/%', limit, offset))
320
331
  res = await cursor.fetchall()
321
332
  files = [self.parse_record(r) for r in res]
322
333
  return files
@@ -351,8 +362,8 @@ class FileConn(DBObjectBase):
351
362
  MAX(access_time) as access_time,
352
363
  COUNT(*) as n_files
353
364
  FROM fmeta
354
- WHERE url LIKE ?
355
- """, (url + '%', ))
365
+ WHERE url LIKE ? ESCAPE '\\'
366
+ """, (self.escape_sqlike(url) + '%', ))
356
367
  result = await cursor.fetchone()
357
368
  if result is None or any(val is None for val in result):
358
369
  raise PathNotFoundError(f"Path {url} not found")
@@ -376,10 +387,16 @@ class FileConn(DBObjectBase):
376
387
  if not url.endswith('/'):
377
388
  url += '/'
378
389
  if not include_subpath:
379
- cursor = await self.cur.execute("SELECT SUM(file_size) FROM fmeta WHERE url LIKE ? AND url NOT LIKE ?", (url + '%', url + '%/%'))
390
+ cursor = await self.cur.execute(
391
+ "SELECT SUM(file_size) FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND url NOT LIKE ? ESCAPE '\\'",
392
+ (self.escape_sqlike(url) + '%', self.escape_sqlike(url) + '%/%')
393
+ )
380
394
  res = await cursor.fetchone()
381
395
  else:
382
- cursor = await self.cur.execute("SELECT SUM(file_size) FROM fmeta WHERE url LIKE ?", (url + '%', ))
396
+ cursor = await self.cur.execute(
397
+ "SELECT SUM(file_size) FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
398
+ (self.escape_sqlike(url) + '%', )
399
+ )
383
400
  res = await cursor.fetchone()
384
401
  assert res is not None
385
402
  return res[0] or 0
@@ -442,7 +459,10 @@ class FileConn(DBObjectBase):
442
459
  """
443
460
  assert old_url.endswith('/'), "Old path must end with /"
444
461
  assert new_url.endswith('/'), "New path must end with /"
445
- cursor = await self.cur.execute("SELECT * FROM fmeta WHERE url LIKE ?", (old_url + '%', ))
462
+ cursor = await self.cur.execute(
463
+ "SELECT * FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
464
+ (self.escape_sqlike(old_url) + '%', )
465
+ )
446
466
  res = await cursor.fetchall()
447
467
  for r in res:
448
468
  old_record = FileRecord(*r)
@@ -472,10 +492,16 @@ class FileConn(DBObjectBase):
472
492
  assert old_url.endswith('/'), "Old path must end with /"
473
493
  assert new_url.endswith('/'), "New path must end with /"
474
494
  if user_id is None:
475
- cursor = await self.cur.execute("SELECT * FROM fmeta WHERE url LIKE ?", (old_url + '%', ))
495
+ cursor = await self.cur.execute(
496
+ "SELECT * FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
497
+ (self.escape_sqlike(old_url) + '%', )
498
+ )
476
499
  res = await cursor.fetchall()
477
500
  else:
478
- cursor = await self.cur.execute("SELECT * FROM fmeta WHERE url LIKE ? AND owner_id = ?", (old_url + '%', user_id))
501
+ cursor = await self.cur.execute(
502
+ "SELECT * FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND owner_id = ?",
503
+ (self.escape_sqlike(old_url) + '%', user_id)
504
+ )
479
505
  res = await cursor.fetchall()
480
506
  for r in res:
481
507
  new_r = new_url + r[0][len(old_url):]
@@ -510,10 +536,16 @@ class FileConn(DBObjectBase):
510
536
  async def delete_records_by_prefix(self, path: str, under_owner_id: Optional[int] = None) -> list[FileRecord]:
511
537
  """Delete all records with url starting with path"""
512
538
  # update user size
513
- cursor = await self.cur.execute("SELECT DISTINCT owner_id FROM fmeta WHERE url LIKE ?", (path + '%', ))
539
+ cursor = await self.cur.execute(
540
+ "SELECT DISTINCT owner_id FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
541
+ (self.escape_sqlike(path) + '%', )
542
+ )
514
543
  res = await cursor.fetchall()
515
544
  for r in res:
516
- cursor = await self.cur.execute("SELECT SUM(file_size) FROM fmeta WHERE owner_id = ? AND url LIKE ?", (r[0], path + '%'))
545
+ cursor = await self.cur.execute(
546
+ "SELECT SUM(file_size) FROM fmeta WHERE owner_id = ? AND url LIKE ? ESCAPE '\\'",
547
+ (r[0], self.escape_sqlike(path) + '%')
548
+ )
517
549
  size = await cursor.fetchone()
518
550
  if size is not None:
519
551
  await self._user_size_dec(r[0], size[0])
@@ -522,9 +554,9 @@ class FileConn(DBObjectBase):
522
554
  # but it's not a big deal... we should have only one writer
523
555
 
524
556
  if under_owner_id is None:
525
- res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? RETURNING *", (path + '%', ))
557
+ res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? ESCAPE '\\' RETURNING *", (self.escape_sqlike(path) + '%', ))
526
558
  else:
527
- res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? AND owner_id = ? RETURNING *", (path + '%', under_owner_id))
559
+ res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND owner_id = ? RETURNING *", (self.escape_sqlike(path) + '%', under_owner_id))
528
560
  all_f_rec = await res.fetchall()
529
561
  self.logger.info(f"Deleted {len(all_f_rec)} file(s) for path {path}") # type: ignore
530
562
  return [self.parse_record(r) for r in all_f_rec]
@@ -692,7 +724,7 @@ async def delayed_log_access(url: str):
692
724
  prohibited_part_regex = re.compile(
693
725
  "|".join([
694
726
  r"^\s*\.+\s*$", # dot path
695
- "[{}]".format("".join(re.escape(c) for c in ('/', "\\", "'", '"', "*", "%"))), # prohibited characters
727
+ "[{}]".format("".join(re.escape(c) for c in ('/', "\\", "'", '"', "*"))), # prohibited characters
696
728
  ])
697
729
  ),
698
730
  )
@@ -1076,7 +1108,7 @@ class Database:
1076
1108
  async def data_iter():
1077
1109
  async for (r, blob) in self.iter_dir(top_url, None):
1078
1110
  rel_path = r.url[len(top_url):]
1079
- rel_path = decode_uri_compnents(rel_path)
1111
+ rel_path = decode_uri_components(rel_path)
1080
1112
  b_iter: AsyncIterable[bytes]
1081
1113
  if isinstance(blob, bytes):
1082
1114
  async def blob_iter(): yield blob
@@ -1106,7 +1138,7 @@ class Database:
1106
1138
  with zipfile.ZipFile(buffer, 'w') as zf:
1107
1139
  async for (r, blob) in self.iter_dir(top_url, None):
1108
1140
  rel_path = r.url[len(top_url):]
1109
- rel_path = decode_uri_compnents(rel_path)
1141
+ rel_path = decode_uri_components(rel_path)
1110
1142
  if r.external:
1111
1143
  assert isinstance(blob, AsyncIterable)
1112
1144
  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,7 +5,7 @@ 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
11
  from ..eng.database import check_file_read_permission, check_path_permission, FileConn, delayed_log_access
@@ -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)
@@ -194,7 +194,7 @@ async def validate_path_read_permission(path: str, user: UserRecord):
194
194
  @handle_exception
195
195
  async def count_files(path: str, flat: bool = False, user: UserRecord = Depends(registered_user)):
196
196
  await validate_path_read_permission(path, user)
197
- path = ensure_uri_compnents(path)
197
+ path = ensure_uri_components(path)
198
198
  async with unique_cursor() as conn:
199
199
  fconn = FileConn(conn)
200
200
  return { "count": await fconn.count_dir_files(url = path, flat = flat) }
@@ -206,7 +206,7 @@ async def list_files(
206
206
  flat: bool = False, user: UserRecord = Depends(registered_user)
207
207
  ):
208
208
  await validate_path_read_permission(path, user)
209
- path = ensure_uri_compnents(path)
209
+ path = ensure_uri_components(path)
210
210
  async with unique_cursor() as conn:
211
211
  fconn = FileConn(conn)
212
212
  return await fconn.list_dir_files(
@@ -219,7 +219,7 @@ async def list_files(
219
219
  @handle_exception
220
220
  async def count_dirs(path: str, user: UserRecord = Depends(registered_user)):
221
221
  await validate_path_read_permission(path, user)
222
- path = ensure_uri_compnents(path)
222
+ path = ensure_uri_components(path)
223
223
  async with unique_cursor() as conn:
224
224
  fconn = FileConn(conn)
225
225
  return { "count": await fconn.count_path_dirs(url = path) }
@@ -231,7 +231,7 @@ async def list_dirs(
231
231
  skim: bool = True, user: UserRecord = Depends(registered_user)
232
232
  ):
233
233
  await validate_path_read_permission(path, user)
234
- path = ensure_uri_compnents(path)
234
+ path = ensure_uri_components(path)
235
235
  async with unique_cursor() as conn:
236
236
  fconn = FileConn(conn)
237
237
  return await fconn.list_path_dirs(
@@ -263,7 +263,7 @@ async def get_multiple_files(
263
263
  upath2path = OrderedDict[str, str]()
264
264
  for p in path:
265
265
  p_ = p if not p.startswith("/") else p[1:]
266
- upath2path[ensure_uri_compnents(p_)] = p
266
+ upath2path[ensure_uri_components(p_)] = p
267
267
  upaths = list(upath2path.keys())
268
268
 
269
269
  # 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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "lfss"
3
- version = "0.11.3"
3
+ version = "0.11.5"
4
4
  description = "Lightweight file storage service"
5
5
  authors = ["Li, Mengxun <mengxunli@whu.edu.cn>"]
6
6
  readme = "Readme.md"
@@ -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
File without changes
File without changes
File without changes