lfss 0.12.0__py3-none-any.whl → 0.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/changelog.md +4 -0
- lfss/api/connector.py +2 -2
- lfss/cli/__init__.py +4 -7
- lfss/cli/cli.py +63 -19
- lfss/cli/cli_lib.py +64 -0
- {lfss-0.12.0.dist-info → lfss-0.12.1.dist-info}/METADATA +1 -1
- {lfss-0.12.0.dist-info → lfss-0.12.1.dist-info}/RECORD +9 -8
- {lfss-0.12.0.dist-info → lfss-0.12.1.dist-info}/WHEEL +0 -0
- {lfss-0.12.0.dist-info → lfss-0.12.1.dist-info}/entry_points.txt +0 -0
docs/changelog.md
CHANGED
lfss/api/connector.py
CHANGED
@@ -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
|
220
|
+
return response.iter_content(chunk_size)
|
221
221
|
|
222
222
|
def get_json(self, path: str) -> Optional[dict]:
|
223
223
|
path = _p(path)
|
lfss/cli/__init__.py
CHANGED
@@ -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()
|
lfss/cli/cli.py
CHANGED
@@ -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
|
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
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|
lfss/cli/cli_lib.py
ADDED
@@ -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
|
@@ -4,7 +4,7 @@ docs/Enviroment_variables.md,sha256=CZ5DrrXSLU5RLBEVQ-gLMaOIuFthd7dEiTzO7ODrPRQ,
|
|
4
4
|
docs/Known_issues.md,sha256=ZqETcWP8lzTOel9b2mxEgCnADFF8IxOrEtiVO1NoMAk,251
|
5
5
|
docs/Permission.md,sha256=thUJx7YRoU63Pb-eqo5l5450DrZN3QYZ36GCn8r66no,3152
|
6
6
|
docs/Webdav.md,sha256=-Ja-BTWSY1BEMAyZycvEMNnkNTPZ49gSPzmf3Lbib70,1547
|
7
|
-
docs/changelog.md,sha256=
|
7
|
+
docs/changelog.md,sha256=yusNvAGnSZCBGSYsWKLyYENw6rgqCtN83FBgSeY0Ax8,2536
|
8
8
|
frontend/api.js,sha256=RqvwRWhYZx7_cDlyYgWzgJAr83RxH47WzY-5KL6aRX0,22670
|
9
9
|
frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
|
10
10
|
frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
|
@@ -20,10 +20,11 @@ frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
|
|
20
20
|
frontend/thumb.js,sha256=46ViD2TlTTWy0fx6wjoAs_5CQ4ajYB90vVzM7UO2IHw,6182
|
21
21
|
frontend/utils.js,sha256=XP5hM_mROYaxK5dqn9qZVwv7GdQuiDzByilFskbrnxA,6068
|
22
22
|
lfss/api/__init__.py,sha256=qHlQAnvw2y0FNZKhes6ikzItEEQvyJWFhVMs1GAqZiM,6822
|
23
|
-
lfss/api/connector.py,sha256=
|
24
|
-
lfss/cli/__init__.py,sha256=
|
23
|
+
lfss/api/connector.py,sha256=jMgYhSYQN0yDD9B1e35F7x6ZFswR3zBKQ19TkdP2KK0,17018
|
24
|
+
lfss/cli/__init__.py,sha256=OPJLYHvqsyNUoPRzW4ITKQ3hEuotx7u-OsN4Uoz1XvA,1132
|
25
25
|
lfss/cli/balance.py,sha256=fUbKKAUyaDn74f7mmxMfBL4Q4voyBLHu6Lg_g8GfMOQ,4121
|
26
|
-
lfss/cli/cli.py,sha256=
|
26
|
+
lfss/cli/cli.py,sha256=FK8XZZnHxvw8AhnPkQXWc-hwnj4Kxt4N5CQZUrVF5Fw,15019
|
27
|
+
lfss/cli/cli_lib.py,sha256=QtXB8WsThz4R5n8ZpxKF_20L1BPBpvbC1QJBeuq0-NA,2866
|
27
28
|
lfss/cli/log.py,sha256=TBlt8mhHMouv8ZBUMHYfGZiV6-0yPdajJQ5mkGHEojI,3016
|
28
29
|
lfss/cli/panel.py,sha256=Xq3I_n-ctveym-Gh9LaUpzHiLlvt3a_nuDiwUS-MGrg,1597
|
29
30
|
lfss/cli/serve.py,sha256=vTo6_BiD7Dn3VLvHsC5RKRBC3lMu45JVr_0SqpgHdj0,1086
|
@@ -47,7 +48,7 @@ lfss/svc/app_dav.py,sha256=DRMgByUAQ3gD6wL9xmikV5kvVmATN7QkxGSttFTYxFU,18245
|
|
47
48
|
lfss/svc/app_native.py,sha256=imqnuAoseTS2CmztUI0yQ0Jjq_jqbjxYG-_FFnYp6u0,11040
|
48
49
|
lfss/svc/common_impl.py,sha256=wlTQm8zEGAfyw9FJvK9zqgLQw47MzNq6IT3OgwdUaCw,13736
|
49
50
|
lfss/svc/request_log.py,sha256=v8yXEIzPjaksu76Oh5vgdbUEUrw8Kt4etLAXBWSGie8,3207
|
50
|
-
lfss-0.12.
|
51
|
-
lfss-0.12.
|
52
|
-
lfss-0.12.
|
53
|
-
lfss-0.12.
|
51
|
+
lfss-0.12.1.dist-info/METADATA,sha256=AMMYCFIaaEcI51q1QuFyL3DGJ6TjnbnBmdpWYd_bgNI,2725
|
52
|
+
lfss-0.12.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
53
|
+
lfss-0.12.1.dist-info/entry_points.txt,sha256=M4ubn9oLYcTc9wxlLKWwljnluStPWpCDlCGuTVU8twg,255
|
54
|
+
lfss-0.12.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|