lfss 0.12.0__tar.gz → 0.12.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {lfss-0.12.0 → lfss-0.12.1}/PKG-INFO +1 -1
  2. {lfss-0.12.0 → lfss-0.12.1}/docs/changelog.md +4 -0
  3. {lfss-0.12.0 → lfss-0.12.1}/lfss/api/connector.py +2 -2
  4. {lfss-0.12.0 → lfss-0.12.1}/lfss/cli/__init__.py +4 -7
  5. {lfss-0.12.0 → lfss-0.12.1}/lfss/cli/cli.py +63 -19
  6. lfss-0.12.1/lfss/cli/cli_lib.py +64 -0
  7. {lfss-0.12.0 → lfss-0.12.1}/pyproject.toml +1 -1
  8. {lfss-0.12.0 → lfss-0.12.1}/Readme.md +0 -0
  9. {lfss-0.12.0 → lfss-0.12.1}/docs/Client.md +0 -0
  10. {lfss-0.12.0 → lfss-0.12.1}/docs/Enviroment_variables.md +0 -0
  11. {lfss-0.12.0 → lfss-0.12.1}/docs/Known_issues.md +0 -0
  12. {lfss-0.12.0 → lfss-0.12.1}/docs/Permission.md +0 -0
  13. {lfss-0.12.0 → lfss-0.12.1}/docs/Webdav.md +0 -0
  14. {lfss-0.12.0 → lfss-0.12.1}/frontend/api.js +0 -0
  15. {lfss-0.12.0 → lfss-0.12.1}/frontend/index.html +0 -0
  16. {lfss-0.12.0 → lfss-0.12.1}/frontend/info.css +0 -0
  17. {lfss-0.12.0 → lfss-0.12.1}/frontend/info.js +0 -0
  18. {lfss-0.12.0 → lfss-0.12.1}/frontend/login.css +0 -0
  19. {lfss-0.12.0 → lfss-0.12.1}/frontend/login.js +0 -0
  20. {lfss-0.12.0 → lfss-0.12.1}/frontend/popup.css +0 -0
  21. {lfss-0.12.0 → lfss-0.12.1}/frontend/popup.js +0 -0
  22. {lfss-0.12.0 → lfss-0.12.1}/frontend/scripts.js +0 -0
  23. {lfss-0.12.0 → lfss-0.12.1}/frontend/state.js +0 -0
  24. {lfss-0.12.0 → lfss-0.12.1}/frontend/styles.css +0 -0
  25. {lfss-0.12.0 → lfss-0.12.1}/frontend/thumb.css +0 -0
  26. {lfss-0.12.0 → lfss-0.12.1}/frontend/thumb.js +0 -0
  27. {lfss-0.12.0 → lfss-0.12.1}/frontend/utils.js +0 -0
  28. {lfss-0.12.0 → lfss-0.12.1}/lfss/api/__init__.py +0 -0
  29. {lfss-0.12.0 → lfss-0.12.1}/lfss/cli/balance.py +0 -0
  30. {lfss-0.12.0 → lfss-0.12.1}/lfss/cli/log.py +0 -0
  31. {lfss-0.12.0 → lfss-0.12.1}/lfss/cli/panel.py +0 -0
  32. {lfss-0.12.0 → lfss-0.12.1}/lfss/cli/serve.py +0 -0
  33. {lfss-0.12.0 → lfss-0.12.1}/lfss/cli/user.py +0 -0
  34. {lfss-0.12.0 → lfss-0.12.1}/lfss/cli/vacuum.py +0 -0
  35. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/__init__.py +0 -0
  36. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/bounded_pool.py +0 -0
  37. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/config.py +0 -0
  38. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/connection_pool.py +0 -0
  39. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/database.py +0 -0
  40. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/datatype.py +0 -0
  41. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/error.py +0 -0
  42. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/log.py +0 -0
  43. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/thumb.py +0 -0
  44. {lfss-0.12.0 → lfss-0.12.1}/lfss/eng/utils.py +0 -0
  45. {lfss-0.12.0 → lfss-0.12.1}/lfss/sql/init.sql +0 -0
  46. {lfss-0.12.0 → lfss-0.12.1}/lfss/sql/pragma.sql +0 -0
  47. {lfss-0.12.0 → lfss-0.12.1}/lfss/svc/app.py +0 -0
  48. {lfss-0.12.0 → lfss-0.12.1}/lfss/svc/app_base.py +0 -0
  49. {lfss-0.12.0 → lfss-0.12.1}/lfss/svc/app_dav.py +0 -0
  50. {lfss-0.12.0 → lfss-0.12.1}/lfss/svc/app_native.py +0 -0
  51. {lfss-0.12.0 → lfss-0.12.1}/lfss/svc/common_impl.py +0 -0
  52. {lfss-0.12.0 → lfss-0.12.1}/lfss/svc/request_log.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.12.0
3
+ Version: 0.12.1
4
4
  Summary: Lite file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: Li, Mengxun
@@ -1,5 +1,9 @@
1
1
  ## 0.12
2
2
 
3
+ ### 0.12.1
4
+ - Add `cat` command
5
+ - Use unicode icons for CLI list command
6
+
3
7
  ### 0.12.0
4
8
  - Change default script to client CLI
5
9
  - Client CLI default to verbose output
@@ -212,12 +212,12 @@ class Connector:
212
212
  if response is None: return None
213
213
  return response.content
214
214
 
215
- def get_stream(self, path: str) -> Iterator[bytes]:
215
+ def get_stream(self, path: str, chunk_size = 1024) -> Iterator[bytes]:
216
216
  """Downloads a file from the specified path, will raise PathNotFoundError if path not found."""
217
217
  path = _p(path)
218
218
  response = self._get(path, stream=True)
219
219
  if response is None: raise PathNotFoundError("Path not found: " + path)
220
- return response.iter_content(chunk_size=1024)
220
+ return response.iter_content(chunk_size)
221
221
 
222
222
  def get_json(self, path: str) -> Optional[dict]:
223
223
  path = _p(path)
@@ -16,16 +16,13 @@ def catch_request_error(error_code_handler: Optional[ dict[int, Callable[[reques
16
16
  print(f"\033[91m[Error message]: {e.response.text}\033[0m")
17
17
 
18
18
  T = TypeVar('T')
19
- def line_sep(iter: Iterable[T], enable=True, start=True, end=True, color="\033[90m") -> Generator[T, None, None]:
19
+ def line_sep(iter: Iterable[T], enable=True, start=True, end=True, middle=False, color="\033[90m") -> Generator[T, None, None]:
20
20
  screen_width = os.get_terminal_size().columns
21
21
  def print_ln():
22
22
  if enable: print(color + "-" * screen_width + "\033[0m")
23
23
 
24
- if start:
25
- print_ln()
24
+ if start: print_ln()
26
25
  for i, line in enumerate(iter):
27
- if i > 0:
28
- print_ln()
26
+ if i > 0 and middle: print_ln()
29
27
  yield line
30
- if end:
31
- print_ln()
28
+ if end: print_ln()
@@ -1,12 +1,15 @@
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, AccessLevel
4
+ from lfss.eng.datatype import (
5
+ FileReadPermission, AccessLevel,
6
+ FileSortKey, DirSortKey,
7
+ FileRecord, DirectoryRecord, PathContents
8
+ )
5
9
  from lfss.eng.utils import decode_uri_components, fmt_storage_size
6
- from . import catch_request_error, line_sep as _line_sep
7
10
 
8
- # monkey patch to avoid printing line separators...may remove line_sep in the future
9
- line_sep = lambda *args, **kwargs: _line_sep(*args, enable=False, **kwargs)
11
+ from . import catch_request_error, line_sep
12
+ from .cli_lib import mimetype_unicode, stream_text
10
13
 
11
14
  def parse_permission(s: str) -> FileReadPermission:
12
15
  for p in FileReadPermission:
@@ -25,6 +28,46 @@ def default_error_handler_dict(path: str):
25
28
  404: lambda _: print(f"\033[31mNot found\033[0m ({path})", file=sys.stderr),
26
29
  409: lambda _: print(f"\033[31mConflict\033[0m ({path})", file=sys.stderr),
27
30
  }
31
+ def print_path_list(
32
+ path_list: list[FileRecord] | list[DirectoryRecord] | PathContents,
33
+ detailed: bool = False
34
+ ):
35
+ dirs: list[DirectoryRecord]
36
+ files: list[FileRecord]
37
+ if isinstance(path_list, PathContents):
38
+ dirs = path_list.dirs
39
+ files = path_list.files
40
+ else:
41
+ dirs = [p for p in path_list if isinstance(p, DirectoryRecord)]
42
+ files = [p for p in path_list if isinstance(p, FileRecord)]
43
+ # check if terminal supports unicode
44
+ supports_unicode = sys.stdout.encoding.lower().startswith("utf")
45
+ def print_ln(r: DirectoryRecord | FileRecord):
46
+ nonlocal detailed, supports_unicode
47
+ match (r, supports_unicode):
48
+ case (DirectoryRecord(), True):
49
+ print(mimetype_unicode(r), end=" ")
50
+ case (DirectoryRecord(), False):
51
+ print("[D]", end=" ")
52
+ case (FileRecord(), True):
53
+ print(mimetype_unicode(r), end=" ")
54
+ case (FileRecord(), False):
55
+ print("[F]", end=" ")
56
+ case _:
57
+ print("[?]", end=" ")
58
+ print(decode_uri_components(r.url), end="")
59
+ if detailed:
60
+ if isinstance(r, FileRecord):
61
+ print(f" | {fmt_storage_size(r.file_size)}, permission={r.permission.name}, created={r.create_time}, accessed={r.access_time}")
62
+ else:
63
+ print()
64
+ else:
65
+ print()
66
+
67
+ for d in line_sep(dirs, end=False):
68
+ print_ln(d)
69
+ for f in line_sep(files, start=False):
70
+ print_ln(f)
28
71
 
29
72
  def parse_arguments():
30
73
  parser = argparse.ArgumentParser(description="Client-side command line interface, set LFSS_ENDPOINT and LFSS_TOKEN environment variables for authentication.")
@@ -97,6 +140,10 @@ def parse_arguments():
97
140
  sp_list_f.add_argument("--order", "--order-by", type=str, help="Order of the list", default="", choices=typing.get_args(FileSortKey))
98
141
  sp_list_f.add_argument("--reverse", "--order-desc", action="store_true", help="Reverse the list order")
99
142
 
143
+ # show content
144
+ sp_show = sp.add_parser("concatenate", help="Concatenate and print files", aliases=["cat"])
145
+ sp_show.add_argument("path", help="Path to the text files", type=str, nargs="+")
146
+ sp_show.add_argument("-e", "--encoding", type=str, default="utf-8", help="Text file encoding, default utf-8")
100
147
  return parser.parse_args()
101
148
 
102
149
  def main():
@@ -213,13 +260,7 @@ def main():
213
260
  order_by=args.order,
214
261
  order_desc=args.reverse,
215
262
  )
216
- for i, d in enumerate(line_sep(res.dirs, end=False)):
217
- d.url = decode_uri_components(d.url)
218
- print(f"[d{i+1}] {d if args.long else d.url}")
219
- for i, f in enumerate(line_sep(res.files)):
220
- f.url = decode_uri_components(f.url)
221
- print(f"[f{i+1}] {f if args.long else f.url}")
222
-
263
+ print_path_list(res, detailed=args.long)
223
264
  if len(res.dirs) + len(res.files) == args.limit:
224
265
  print(f"\033[33m[Warning] List limit reached, use --offset and --limit to list more items.\033[0m")
225
266
 
@@ -233,10 +274,7 @@ def main():
233
274
  order_by=args.order,
234
275
  order_desc=args.reverse,
235
276
  )
236
- for i, f in enumerate(line_sep(res)):
237
- f.url = decode_uri_components(f.url)
238
- print(f"[{i+1}] {f if args.long else f.url}")
239
-
277
+ print_path_list(res, detailed=args.long)
240
278
  if len(res) == args.limit:
241
279
  print(f"\033[33m[Warning] List limit reached, use --offset and --limit to list more files.\033[0m")
242
280
 
@@ -250,13 +288,19 @@ def main():
250
288
  order_by=args.order,
251
289
  order_desc=args.reverse,
252
290
  )
253
- for i, d in enumerate(line_sep(res)):
254
- d.url = decode_uri_components(d.url)
255
- print(f"[{i+1}] {d if args.long else d.url}")
256
-
291
+ print_path_list(res, detailed=args.long)
257
292
  if len(res) == args.limit:
258
293
  print(f"\033[33m[Warning] List limit reached, use --offset and --limit to list more directories.\033[0m")
259
294
 
295
+ elif args.command in ["cat", "concatenate"]:
296
+ for _p in args.path:
297
+ with catch_request_error(default_error_handler_dict(_p)):
298
+ try:
299
+ for chunk in stream_text(connector, _p, encoding=args.encoding):
300
+ print(chunk, end="")
301
+ except (FileNotFoundError, ValueError) as e:
302
+ print(f"\033[31m{e}\033[0m", file=sys.stderr)
303
+
260
304
  else:
261
305
  raise NotImplementedError(f"Command {args.command} not implemented.")
262
306
 
@@ -0,0 +1,64 @@
1
+
2
+ from ..api.connector import Connector
3
+ from ..eng.datatype import DirectoryRecord, FileRecord
4
+
5
+ def mimetype_unicode(r: DirectoryRecord | FileRecord):
6
+ if isinstance(r, DirectoryRecord):
7
+ return "📁"
8
+ if r.mime_type in ["application/pdf", "application/x-pdf"]:
9
+ return "📕"
10
+ elif r.mime_type.startswith("image/"):
11
+ return "🖼️"
12
+ elif r.mime_type.startswith("video/"):
13
+ return "🎞️"
14
+ elif r.mime_type.startswith("audio/"):
15
+ return "🎵"
16
+ elif r.mime_type in ["application/zip", "application/x-tar", "application/gzip", "application/x-7z-compressed"]:
17
+ return "📦"
18
+ elif r.mime_type in ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]:
19
+ return "📊"
20
+ elif r.mime_type in ["application/x-msdownload", "application/x-executable", "application/x-mach-binary", "application/x-elf"]:
21
+ return "💻"
22
+ elif r.mime_type in ["application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation"]:
23
+ return "📈"
24
+ elif r.mime_type in set([
25
+ "text/html", "application/xhtml+xml", "application/xml", "text/css", "text/x-scss", "application/javascript", "text/javascript",
26
+ "application/json", "text/x-yaml", "text/x-markdown", "application/wasm",
27
+ "text/x-ruby", "application/x-ruby", "text/x-perl", "application/x-lisp",
28
+ "text/x-haskell", "text/x-lua", "application/x-tcl",
29
+ "text/x-python", "text/x-java-source", "text/x-go", "application/x-rust", "text/x-asm",
30
+ "application/sql", "text/x-c", "text/x-c++", "text/x-csharp",
31
+ "application/x-httpd-php", "application/x-sh", "application/x-shellscript",
32
+ "application/x-latex", "application/x-tex",
33
+ ]):
34
+ return "👨‍💻"
35
+ elif r.mime_type.startswith("text/"):
36
+ return "📃"
37
+ return "📄"
38
+
39
+ def stream_text(
40
+ conn: Connector,
41
+ path: str,
42
+ encoding="utf-8",
43
+ chunk_size=1024 * 8,
44
+ ):
45
+ """
46
+ Stream text content of a file from the server.
47
+ Raise FileNotFoundError if the file does not exist.
48
+ Raise ValueError if the file size exceeds MAX_TEXT_SIZE.
49
+
50
+ Yields str chunks.
51
+ """
52
+ MAX_TEXT_SIZE = 100 * 1024 * 1024 # 100 MB
53
+ r = conn.get_fmeta(path)
54
+ if r is None:
55
+ raise FileNotFoundError(f"File not found: {path}")
56
+ if r.file_size > MAX_TEXT_SIZE:
57
+ raise ValueError(f"File size {r.file_size} exceeds maximum text size {MAX_TEXT_SIZE}")
58
+ ss = conn.get_stream(r.url, chunk_size=chunk_size)
59
+ total_read = 0
60
+ for chunk in ss:
61
+ total_read += len(chunk)
62
+ if total_read > MAX_TEXT_SIZE:
63
+ raise ValueError(f"File size exceeds maximum text size {MAX_TEXT_SIZE}")
64
+ yield chunk.decode(encoding, errors='replace') # decode bytes to str, replace errors
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "lfss"
3
- version = "0.12.0"
3
+ version = "0.12.1"
4
4
  description = "Lite file storage service"
5
5
  authors = ["Li, Mengxun <mengxunli@whu.edu.cn>"]
6
6
  readme = "Readme.md"
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
File without changes
File without changes
File without changes
File without changes
File without changes