lfss 0.7.3__py3-none-any.whl → 0.7.4__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/Permission.md CHANGED
@@ -18,6 +18,13 @@ Non-admin users can access files based on:
18
18
  - If the file is `unset`, then the file's permission is inherited from the owner's permission.
19
19
  - If both the owner and the file have `unset` permission, then the file is `public`.
20
20
 
21
+ ### Meta-data access
22
+ - Non-login users can't access any file-meta.
23
+ - All users can access the file-meta of files under their own path.
24
+ - For files under other users' path, the file-meta is determined in a way same as file access.
25
+ - Admins can access the path-meta of all users.
26
+ - All users can access the path-meta of their own path.
27
+
21
28
  ### Path-listing
22
29
  - Non-login users cannot list any files.
23
30
  - All users can list the files under their own path
lfss/cli/cli.py CHANGED
@@ -1,22 +1,32 @@
1
- from lfss.client import Connector, upload_directory
1
+ from lfss.client import Connector, upload_directory, upload_file, download_file, download_directory
2
2
  from lfss.src.datatype import FileReadPermission
3
3
  from pathlib import Path
4
4
  import argparse
5
5
 
6
6
  def parse_arguments():
7
7
  parser = argparse.ArgumentParser(description="Command line interface, please set LFSS_ENDPOINT and LFSS_TOKEN environment variables.")
8
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
8
9
 
9
10
  sp = parser.add_subparsers(dest="command", required=True)
10
11
 
11
12
  # upload
12
13
  sp_upload = sp.add_parser("upload", help="Upload files")
13
14
  sp_upload.add_argument("src", help="Source file or directory", type=str)
14
- sp_upload.add_argument("dst", help="Destination path", type=str)
15
+ sp_upload.add_argument("dst", help="Destination url path", type=str)
15
16
  sp_upload.add_argument("-j", "--jobs", type=int, default=1, help="Number of concurrent uploads")
16
17
  sp_upload.add_argument("--interval", type=float, default=0, help="Interval between files, only works with directory upload")
17
18
  sp_upload.add_argument("--conflict", choices=["overwrite", "abort", "skip", "skip-ahead"], default="abort", help="Conflict resolution")
18
19
  sp_upload.add_argument("--permission", type=FileReadPermission, default=FileReadPermission.UNSET, help="File permission")
19
- sp_upload.add_argument("--retries", type=int, default=0, help="Number of retries, only works with directory upload")
20
+ sp_upload.add_argument("--retries", type=int, default=0, help="Number of retries")
21
+
22
+ # download
23
+ sp_download = sp.add_parser("download", help="Download files")
24
+ sp_download.add_argument("src", help="Source url path", type=str)
25
+ sp_download.add_argument("dst", help="Destination file or directory", type=str)
26
+ sp_download.add_argument("-j", "--jobs", type=int, default=1, help="Number of concurrent downloads")
27
+ sp_download.add_argument("--interval", type=float, default=0, help="Interval between files, only works with directory download")
28
+ sp_download.add_argument("--overwrite", action="store_true", help="Overwrite existing files")
29
+ sp_download.add_argument("--retries", type=int, default=0, help="Number of retries")
20
30
 
21
31
  return parser.parse_args()
22
32
 
@@ -28,9 +38,9 @@ def main():
28
38
  if src_path.is_dir():
29
39
  failed_upload = upload_directory(
30
40
  connector, args.src, args.dst,
31
- verbose=True,
41
+ verbose=args.verbose,
32
42
  n_concurrent=args.jobs,
33
- n_reties=args.retries,
43
+ n_retries=args.retries,
34
44
  interval=args.interval,
35
45
  conflict=args.conflict,
36
46
  permission=args.permission
@@ -40,13 +50,46 @@ def main():
40
50
  for path in failed_upload:
41
51
  print(f" {path}")
42
52
  else:
43
- with open(args.src, 'rb') as f:
44
- connector.put(
45
- args.dst,
46
- f.read(),
47
- conflict=args.conflict,
48
- permission=args.permission
49
- )
53
+ success = upload_file(
54
+ connector,
55
+ file_path = args.src,
56
+ dst_url = args.dst,
57
+ verbose=args.verbose,
58
+ n_retries=args.retries,
59
+ interval=args.interval,
60
+ conflict=args.conflict,
61
+ permission=args.permission
62
+ )
63
+ if not success:
64
+ print("Failed to upload.")
65
+
66
+ elif args.command == "download":
67
+ is_dir = args.src.endswith("/")
68
+ if is_dir:
69
+ failed_download = download_directory(
70
+ connector, args.src, args.dst,
71
+ verbose=args.verbose,
72
+ n_concurrent=args.jobs,
73
+ n_retries=args.retries,
74
+ interval=args.interval,
75
+ overwrite=args.overwrite
76
+ )
77
+ if failed_download:
78
+ print("Failed to download:")
79
+ for path in failed_download:
80
+ print(f" {path}")
81
+ else:
82
+ success = download_file(
83
+ connector,
84
+ src_url = args.src,
85
+ file_path = args.dst,
86
+ verbose=args.verbose,
87
+ n_retries=args.retries,
88
+ interval=args.interval,
89
+ overwrite=args.overwrite
90
+ )
91
+ if not success:
92
+ print("Failed to download.")
50
93
  else:
51
94
  raise NotImplementedError(f"Command {args.command} not implemented.")
52
95
 
lfss/client/__init__.py CHANGED
@@ -1,14 +1,45 @@
1
- import os, time
1
+ import os, time, pathlib
2
2
  from threading import Lock
3
3
  from concurrent.futures import ThreadPoolExecutor
4
4
  from .api import Connector
5
5
 
6
+ def upload_file(
7
+ connector: Connector,
8
+ file_path: str,
9
+ dst_url: str,
10
+ n_retries: int = 0,
11
+ interval: float = 0,
12
+ verbose: bool = False,
13
+ **put_kwargs
14
+ ):
15
+ this_try = 0
16
+ while this_try <= n_retries:
17
+ try:
18
+ with open(file_path, 'rb') as f:
19
+ blob = f.read()
20
+ connector.put(dst_url, blob, **put_kwargs)
21
+ break
22
+ except Exception as e:
23
+ if isinstance(e, KeyboardInterrupt):
24
+ raise e
25
+ if verbose:
26
+ print(f"Error uploading {file_path}: {e}, retrying...")
27
+ this_try += 1
28
+ finally:
29
+ time.sleep(interval)
30
+
31
+ if this_try > n_retries:
32
+ if verbose:
33
+ print(f"Failed to upload {file_path} after {n_retries} retries.")
34
+ return False
35
+ return True
36
+
6
37
  def upload_directory(
7
38
  connector: Connector,
8
39
  directory: str,
9
40
  path: str,
10
41
  n_concurrent: int = 1,
11
- n_reties: int = 0,
42
+ n_retries: int = 0,
12
43
  interval: float = 0,
13
44
  verbose: bool = False,
14
45
  **put_kwargs
@@ -16,6 +47,7 @@ def upload_directory(
16
47
  assert path.endswith('/'), "Path must end with a slash."
17
48
  if path.startswith('/'):
18
49
  path = path[1:]
50
+ directory = str(directory)
19
51
 
20
52
  _counter = 0
21
53
  _counter_lock = Lock()
@@ -30,31 +62,94 @@ def upload_directory(
30
62
  if verbose:
31
63
  print(f"[{this_count}] Uploading {file_path} to {dst_path}")
32
64
 
33
- this_try = 0
34
- with open(file_path, 'rb') as f:
35
- blob = f.read()
36
-
37
- while this_try <= n_reties:
38
- try:
39
- connector.put(dst_path, blob, **put_kwargs)
40
- break
41
- except Exception as e:
42
- if isinstance(e, KeyboardInterrupt):
43
- raise e
44
- if verbose:
45
- print(f"[{this_count}] Error uploading {file_path}: {e}, retrying...")
46
- this_try += 1
47
- finally:
48
- time.sleep(interval)
49
-
50
- if this_try > n_reties:
65
+ if not upload_file(
66
+ connector, file_path, dst_path,
67
+ n_retries=n_retries, interval=interval, verbose=verbose, **put_kwargs
68
+ ):
51
69
  faild_files.append(file_path)
52
- if verbose:
53
- print(f"[{this_count}] Failed to upload {file_path} after {n_reties} retries.")
54
70
 
55
71
  with ThreadPoolExecutor(n_concurrent) as executor:
56
72
  for root, dirs, files in os.walk(directory):
57
73
  for file in files:
58
74
  executor.submit(put_file, os.path.join(root, file))
59
75
 
60
- return faild_files
76
+ return faild_files
77
+
78
+ def download_file(
79
+ connector: Connector,
80
+ src_url: str,
81
+ file_path: str,
82
+ n_retries: int = 0,
83
+ interval: float = 0,
84
+ verbose: bool = False,
85
+ overwrite: bool = False
86
+ ):
87
+ this_try = 0
88
+ while this_try <= n_retries:
89
+ if not overwrite and os.path.exists(file_path):
90
+ if verbose:
91
+ print(f"File {file_path} already exists, skipping download.")
92
+ return True
93
+ try:
94
+ blob = connector.get(src_url)
95
+ if not blob:
96
+ return False
97
+ pathlib.Path(file_path).parent.mkdir(parents=True, exist_ok=True)
98
+ with open(file_path, 'wb') as f:
99
+ f.write(blob)
100
+ break
101
+ except Exception as e:
102
+ if isinstance(e, KeyboardInterrupt):
103
+ raise e
104
+ if verbose:
105
+ print(f"Error downloading {src_url}: {e}, retrying...")
106
+ this_try += 1
107
+ finally:
108
+ time.sleep(interval)
109
+
110
+ if this_try > n_retries:
111
+ if verbose:
112
+ print(f"Failed to download {src_url} after {n_retries} retries.")
113
+ return False
114
+ return True
115
+
116
+ def download_directory(
117
+ connector: Connector,
118
+ src_path: str,
119
+ directory: str,
120
+ n_concurrent: int = 1,
121
+ n_retries: int = 0,
122
+ interval: float = 0,
123
+ verbose: bool = False,
124
+ overwrite: bool = False
125
+ ) -> list[str]:
126
+
127
+ directory = str(directory)
128
+
129
+ if not src_path.endswith('/'):
130
+ src_path += '/'
131
+ if not directory.endswith(os.sep):
132
+ directory += os.sep
133
+
134
+ _counter = 0
135
+ _counter_lock = Lock()
136
+ failed_files = []
137
+ def get_file(src_url):
138
+ nonlocal _counter, failed_files
139
+ with _counter_lock:
140
+ _counter += 1
141
+ this_count = _counter
142
+ dst_path = f"{directory}{os.path.relpath(src_url, src_path)}"
143
+ if verbose:
144
+ print(f"[{this_count}] Downloading {src_url} to {dst_path}")
145
+
146
+ if not download_file(
147
+ connector, src_url, dst_path,
148
+ n_retries=n_retries, interval=interval, verbose=verbose, overwrite=overwrite
149
+ ):
150
+ failed_files.append(src_url)
151
+
152
+ with ThreadPoolExecutor(n_concurrent) as executor:
153
+ for file in connector.list_path(src_path).files:
154
+ executor.submit(get_file, file.url)
155
+ return failed_files
lfss/client/api.py CHANGED
@@ -5,6 +5,7 @@ import urllib.parse
5
5
  from lfss.src.datatype import (
6
6
  FileReadPermission, FileRecord, DirectoryRecord, UserRecord, PathContents
7
7
  )
8
+ from lfss.src.utils import ensure_uri_compnents
8
9
 
9
10
  _default_endpoint = os.environ.get('LFSS_ENDPOINT', 'http://localhost:8000')
10
11
  _default_token = os.environ.get('LFSS_TOKEN', '')
@@ -23,6 +24,7 @@ class Connector:
23
24
  ):
24
25
  if path.startswith('/'):
25
26
  path = path[1:]
27
+ path = ensure_uri_compnents(path)
26
28
  def f(**kwargs):
27
29
  url = f"{self.config['endpoint']}/{path}" + "?" + urllib.parse.urlencode(search_params)
28
30
  headers: dict = kwargs.pop('headers', {})
lfss/src/config.py CHANGED
@@ -15,6 +15,3 @@ LARGE_BLOB_DIR.mkdir(exist_ok=True)
15
15
  LARGE_FILE_BYTES = 8 * 1024 * 1024 # 8MB
16
16
  MAX_FILE_BYTES = 512 * 1024 * 1024 # 512MB
17
17
  MAX_BUNDLE_BYTES = 512 * 1024 * 1024 # 512MB
18
-
19
- def hash_credential(username, password):
20
- return hashlib.sha256((username + password).encode()).hexdigest()
lfss/src/database.py CHANGED
@@ -11,9 +11,9 @@ import aiofiles.os
11
11
 
12
12
  from .connection_pool import execute_sql, unique_cursor, transaction
13
13
  from .datatype import UserRecord, FileReadPermission, FileRecord, DirectoryRecord, PathContents
14
- from .config import LARGE_BLOB_DIR, hash_credential
14
+ from .config import LARGE_BLOB_DIR
15
15
  from .log import get_logger
16
- from .utils import decode_uri_compnents
16
+ from .utils import decode_uri_compnents, hash_credential
17
17
  from .error import *
18
18
 
19
19
  class DBObjectBase(ABC):
lfss/src/server.py CHANGED
@@ -339,15 +339,34 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
339
339
  @router_api.get("/meta")
340
340
  @handle_exception
341
341
  async def get_file_meta(path: str, user: UserRecord = Depends(registered_user)):
342
+ """
343
+ Permission:
344
+ for file:
345
+ if file is under user's path, return the meta,
346
+ else, determine by the permission same as get_file
347
+ for path:
348
+ if path is under user's path, return the meta, else return 403
349
+ """
342
350
  logger.info(f"GET meta({path}), user: {user.username}")
343
351
  path = ensure_uri_compnents(path)
352
+ is_file = not path.endswith("/")
344
353
  async with unique_cursor() as conn:
345
354
  fconn = FileConn(conn)
346
- get_fn = fconn.get_file_record if not path.endswith("/") else fconn.get_path_record
347
- record = await get_fn(path)
348
-
349
- if not record:
350
- raise HTTPException(status_code=404, detail="Path not found")
355
+ if is_file:
356
+ record = await fconn.get_file_record(path)
357
+ if not record:
358
+ raise HTTPException(status_code=404, detail="File not found")
359
+ if not path.startswith(f"{user.username}/") and not user.is_admin:
360
+ uconn = UserConn(conn)
361
+ owner = await uconn.get_user_by_id(record.owner_id)
362
+ assert owner is not None, "Owner not found"
363
+ is_allowed, reason = check_user_permission(user, owner, record)
364
+ if not is_allowed:
365
+ raise HTTPException(status_code=403, detail=reason)
366
+ else:
367
+ record = await fconn.get_path_record(path)
368
+ if not path.startswith(f"{user.username}/") and not user.is_admin:
369
+ raise HTTPException(status_code=403, detail="Permission denied")
351
370
  return record
352
371
 
353
372
  @router_api.post("/meta")
lfss/src/utils.py CHANGED
@@ -2,6 +2,10 @@ import datetime
2
2
  import urllib.parse
3
3
  import asyncio
4
4
  import functools
5
+ import hashlib
6
+
7
+ def hash_credential(username: str, password: str):
8
+ return hashlib.sha256((username + password).encode()).hexdigest()
5
9
 
6
10
  def encode_uri_compnents(path: str):
7
11
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.7.3
3
+ Version: 0.7.4
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -1,6 +1,6 @@
1
1
  Readme.md,sha256=vsPotlwPAaHI5plh4aaszpi3rr7ZGDn7-wLdEYTWQ0k,1275
2
2
  docs/Known_issues.md,sha256=rfdG3j1OJF-59S9E06VPyn0nZKbW-ybPxkoZ7MEZWp8,81
3
- docs/Permission.md,sha256=e0ZcogBpTxHsKKOUVmNB460-P6AALQToU2ePq-RHHSQ,1930
3
+ docs/Permission.md,sha256=X0VNfBKU52f93QYqcVyiBFJ3yURiSkhIo9S_5fdSgzM,2265
4
4
  frontend/api.js,sha256=-ouhsmucEunAK3m1H__MqffQkXAjoeVEfM15BvqfIZs,7677
5
5
  frontend/index.html,sha256=VPJDs2LG8ep9kjlsKzjWzpN9vc1VGgdvOUlNTZWyQoQ,2088
6
6
  frontend/popup.css,sha256=VzkjG1ZTLxhHMtTyobnlvqYmVsTmdbJJed2Pu1cc06c,1007
@@ -9,25 +9,25 @@ frontend/scripts.js,sha256=hQ8m3L7P-LplLqrPUWD6pBo4C_tCUl2XZKRNtkWBy8I,21155
9
9
  frontend/styles.css,sha256=wly8O-zF4EUgV12Tv1bATSfmJsLITv2u3_SiyXVaxv4,4096
10
10
  frontend/utils.js,sha256=Ts4nlef8pkrEgpwX-uQwAhWvwxlIzex8ijDLNCa22ps,2372
11
11
  lfss/cli/balance.py,sha256=heOgwH6oNnfYsKJfA4VxWKdEXPstdVbbRXWxcDqLIS0,4176
12
- lfss/cli/cli.py,sha256=ehdsWJrIuIKEawAN9yVGUZjcVoIwwwZUqxLudQ7S9Ig,2259
12
+ lfss/cli/cli.py,sha256=Yup3xIVEQPu10uM8dq1bvre1fK5ngweQHxXZsgQq4Hc,4187
13
13
  lfss/cli/panel.py,sha256=iGdVmdWYjA_7a78ZzWEB_3ggIOBeUKTzg6F5zLaB25c,1401
14
14
  lfss/cli/serve.py,sha256=bO3GT0kuylMGN-7bZWP4e71MlugGZ_lEMkYaYld_Ntg,985
15
15
  lfss/cli/user.py,sha256=h-USWF6lB0Ztm9vwQznqsghKJ5INq5mBmaQeX2D5F-w,3490
16
- lfss/client/__init__.py,sha256=YttaGTBup7mxnW0CtxdZPF0HWga2cGM4twz9MXJIrts,1827
17
- lfss/client/api.py,sha256=x8-bb2K_dOvQPfNa7IsQE4H755iNlvkwzOPY7o9Bu-g,5638
16
+ lfss/client/__init__.py,sha256=R9erioMInKIPZ0rXu1J-4mezbwyGMjig18532ATFP5s,4545
17
+ lfss/client/api.py,sha256=aun5HWVNPBeJK6x0_iaM8gVcE3wX6yaqX0TsfsfifSw,5728
18
18
  lfss/sql/init.sql,sha256=C-JtQAlaOjESI8uoF1Y_9dKukEVSw5Ll-7yA3gG-XHU,1210
19
19
  lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
20
20
  lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- lfss/src/config.py,sha256=Dn_94Wt8RHnJTSTawyh0UIhnpBmka7ajPkeRN1pCWTQ,651
21
+ lfss/src/config.py,sha256=z0aVOW8yGgKSryhQCWTf2RY3iHmKMcKIZ-HiosTnPRs,539
22
22
  lfss/src/connection_pool.py,sha256=69QMJ4gRQ62qi39t0JKdvIaWRBrbU9S7slutIpCc30A,4959
23
- lfss/src/database.py,sha256=kDPkfA-h7n8c-8M0JMBhRGC7nH8s518W5NbNab5a7Wk,32147
23
+ lfss/src/database.py,sha256=VKTLJSUFImF3pzMUaqGWEh1H06Yk3VgJa78bw5rAvG8,32147
24
24
  lfss/src/datatype.py,sha256=BLS7vuuKnFZQg0nrKeP9SymqUhcN6HwPgejU0yBd_Ak,1622
25
25
  lfss/src/error.py,sha256=imbhwnbhnI3HLhkbfICROe3F0gleKrOk4XnqHJDOtuI,285
26
26
  lfss/src/log.py,sha256=qNE04sHoZ2rMbQ5dR2zT7Xaz1KVAfYp5hKpWJX42S9g,5244
27
- lfss/src/server.py,sha256=J1pYq65thHC9ecFqeszHFoQuoFLEsgg6hWmW3XA9U-s,15447
27
+ lfss/src/server.py,sha256=4GYDAa9Fx4H4fYh_He1u1zlIro3zaQmaEBNKpPO2Q2E,16374
28
28
  lfss/src/stat.py,sha256=hTMtQyM_Ukmhc33Bb9FGCfBMIX02KrGHQg8nL7sC8sU,2082
29
- lfss/src/utils.py,sha256=8VkrtpSmurbMiX7GyK-n7Grvzy3uwSJXHdONEsuLCDI,2272
30
- lfss-0.7.3.dist-info/METADATA,sha256=Ize9PqeFnq-Ki18BgTteSs9HSzaICY20mRUE35s9kV8,1967
31
- lfss-0.7.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
- lfss-0.7.3.dist-info/entry_points.txt,sha256=d_Ri3GXxUW-S0E6q953A8od0YMmUAnZGlJSKS46OiW8,172
33
- lfss-0.7.3.dist-info/RECORD,,
29
+ lfss/src/utils.py,sha256=ZE3isOS3gafEYw1z8s2ucY08eWQHIN4YUbKYH8F1hEQ,2409
30
+ lfss-0.7.4.dist-info/METADATA,sha256=XeJ4ft1B9KDdfcxyFrFdwUMTYc1QsUAkgqI8v5rwbq4,1967
31
+ lfss-0.7.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
+ lfss-0.7.4.dist-info/entry_points.txt,sha256=d_Ri3GXxUW-S0E6q953A8od0YMmUAnZGlJSKS46OiW8,172
33
+ lfss-0.7.4.dist-info/RECORD,,
File without changes