lfss 0.7.3__tar.gz → 0.7.4__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 (33) hide show
  1. {lfss-0.7.3 → lfss-0.7.4}/PKG-INFO +1 -1
  2. {lfss-0.7.3 → lfss-0.7.4}/docs/Permission.md +7 -0
  3. lfss-0.7.4/lfss/cli/cli.py +95 -0
  4. lfss-0.7.4/lfss/client/__init__.py +155 -0
  5. {lfss-0.7.3 → lfss-0.7.4}/lfss/client/api.py +2 -0
  6. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/config.py +0 -3
  7. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/database.py +2 -2
  8. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/server.py +24 -5
  9. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/utils.py +4 -0
  10. {lfss-0.7.3 → lfss-0.7.4}/pyproject.toml +1 -1
  11. lfss-0.7.3/lfss/cli/cli.py +0 -52
  12. lfss-0.7.3/lfss/client/__init__.py +0 -60
  13. {lfss-0.7.3 → lfss-0.7.4}/Readme.md +0 -0
  14. {lfss-0.7.3 → lfss-0.7.4}/docs/Known_issues.md +0 -0
  15. {lfss-0.7.3 → lfss-0.7.4}/frontend/api.js +0 -0
  16. {lfss-0.7.3 → lfss-0.7.4}/frontend/index.html +0 -0
  17. {lfss-0.7.3 → lfss-0.7.4}/frontend/popup.css +0 -0
  18. {lfss-0.7.3 → lfss-0.7.4}/frontend/popup.js +0 -0
  19. {lfss-0.7.3 → lfss-0.7.4}/frontend/scripts.js +0 -0
  20. {lfss-0.7.3 → lfss-0.7.4}/frontend/styles.css +0 -0
  21. {lfss-0.7.3 → lfss-0.7.4}/frontend/utils.js +0 -0
  22. {lfss-0.7.3 → lfss-0.7.4}/lfss/cli/balance.py +0 -0
  23. {lfss-0.7.3 → lfss-0.7.4}/lfss/cli/panel.py +0 -0
  24. {lfss-0.7.3 → lfss-0.7.4}/lfss/cli/serve.py +0 -0
  25. {lfss-0.7.3 → lfss-0.7.4}/lfss/cli/user.py +0 -0
  26. {lfss-0.7.3 → lfss-0.7.4}/lfss/sql/init.sql +0 -0
  27. {lfss-0.7.3 → lfss-0.7.4}/lfss/sql/pragma.sql +0 -0
  28. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/__init__.py +0 -0
  29. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/connection_pool.py +0 -0
  30. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/datatype.py +0 -0
  31. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/error.py +0 -0
  32. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/log.py +0 -0
  33. {lfss-0.7.3 → lfss-0.7.4}/lfss/src/stat.py +0 -0
@@ -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
@@ -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
@@ -0,0 +1,95 @@
1
+ from lfss.client import Connector, upload_directory, upload_file, download_file, download_directory
2
+ from lfss.src.datatype import FileReadPermission
3
+ from pathlib import Path
4
+ import argparse
5
+
6
+ def parse_arguments():
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")
9
+
10
+ sp = parser.add_subparsers(dest="command", required=True)
11
+
12
+ # upload
13
+ sp_upload = sp.add_parser("upload", help="Upload files")
14
+ sp_upload.add_argument("src", help="Source file or directory", type=str)
15
+ sp_upload.add_argument("dst", help="Destination url path", type=str)
16
+ sp_upload.add_argument("-j", "--jobs", type=int, default=1, help="Number of concurrent uploads")
17
+ sp_upload.add_argument("--interval", type=float, default=0, help="Interval between files, only works with directory upload")
18
+ sp_upload.add_argument("--conflict", choices=["overwrite", "abort", "skip", "skip-ahead"], default="abort", help="Conflict resolution")
19
+ sp_upload.add_argument("--permission", type=FileReadPermission, default=FileReadPermission.UNSET, help="File permission")
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")
30
+
31
+ return parser.parse_args()
32
+
33
+ def main():
34
+ args = parse_arguments()
35
+ connector = Connector()
36
+ if args.command == "upload":
37
+ src_path = Path(args.src)
38
+ if src_path.is_dir():
39
+ failed_upload = upload_directory(
40
+ connector, args.src, args.dst,
41
+ verbose=args.verbose,
42
+ n_concurrent=args.jobs,
43
+ n_retries=args.retries,
44
+ interval=args.interval,
45
+ conflict=args.conflict,
46
+ permission=args.permission
47
+ )
48
+ if failed_upload:
49
+ print("Failed to upload:")
50
+ for path in failed_upload:
51
+ print(f" {path}")
52
+ else:
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.")
93
+ else:
94
+ raise NotImplementedError(f"Command {args.command} not implemented.")
95
+
@@ -0,0 +1,155 @@
1
+ import os, time, pathlib
2
+ from threading import Lock
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ from .api import Connector
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
+
37
+ def upload_directory(
38
+ connector: Connector,
39
+ directory: str,
40
+ path: str,
41
+ n_concurrent: int = 1,
42
+ n_retries: int = 0,
43
+ interval: float = 0,
44
+ verbose: bool = False,
45
+ **put_kwargs
46
+ ) -> list[str]:
47
+ assert path.endswith('/'), "Path must end with a slash."
48
+ if path.startswith('/'):
49
+ path = path[1:]
50
+ directory = str(directory)
51
+
52
+ _counter = 0
53
+ _counter_lock = Lock()
54
+
55
+ faild_files = []
56
+ def put_file(file_path):
57
+ with _counter_lock:
58
+ nonlocal _counter
59
+ _counter += 1
60
+ this_count = _counter
61
+ dst_path = f"{path}{os.path.relpath(file_path, directory)}"
62
+ if verbose:
63
+ print(f"[{this_count}] Uploading {file_path} to {dst_path}")
64
+
65
+ if not upload_file(
66
+ connector, file_path, dst_path,
67
+ n_retries=n_retries, interval=interval, verbose=verbose, **put_kwargs
68
+ ):
69
+ faild_files.append(file_path)
70
+
71
+ with ThreadPoolExecutor(n_concurrent) as executor:
72
+ for root, dirs, files in os.walk(directory):
73
+ for file in files:
74
+ executor.submit(put_file, os.path.join(root, file))
75
+
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
@@ -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', {})
@@ -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()
@@ -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):
@@ -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")
@@ -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
  [tool.poetry]
2
2
  name = "lfss"
3
- version = "0.7.3"
3
+ version = "0.7.4"
4
4
  description = "Lightweight file storage service"
5
5
  authors = ["li, mengxun <limengxun45@outlook.com>"]
6
6
  readme = "Readme.md"
@@ -1,52 +0,0 @@
1
- from lfss.client import Connector, upload_directory
2
- from lfss.src.datatype import FileReadPermission
3
- from pathlib import Path
4
- import argparse
5
-
6
- def parse_arguments():
7
- parser = argparse.ArgumentParser(description="Command line interface, please set LFSS_ENDPOINT and LFSS_TOKEN environment variables.")
8
-
9
- sp = parser.add_subparsers(dest="command", required=True)
10
-
11
- # upload
12
- sp_upload = sp.add_parser("upload", help="Upload files")
13
- 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("-j", "--jobs", type=int, default=1, help="Number of concurrent uploads")
16
- sp_upload.add_argument("--interval", type=float, default=0, help="Interval between files, only works with directory upload")
17
- sp_upload.add_argument("--conflict", choices=["overwrite", "abort", "skip", "skip-ahead"], default="abort", help="Conflict resolution")
18
- 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
-
21
- return parser.parse_args()
22
-
23
- def main():
24
- args = parse_arguments()
25
- connector = Connector()
26
- if args.command == "upload":
27
- src_path = Path(args.src)
28
- if src_path.is_dir():
29
- failed_upload = upload_directory(
30
- connector, args.src, args.dst,
31
- verbose=True,
32
- n_concurrent=args.jobs,
33
- n_reties=args.retries,
34
- interval=args.interval,
35
- conflict=args.conflict,
36
- permission=args.permission
37
- )
38
- if failed_upload:
39
- print("Failed to upload:")
40
- for path in failed_upload:
41
- print(f" {path}")
42
- 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
- )
50
- else:
51
- raise NotImplementedError(f"Command {args.command} not implemented.")
52
-
@@ -1,60 +0,0 @@
1
- import os, time
2
- from threading import Lock
3
- from concurrent.futures import ThreadPoolExecutor
4
- from .api import Connector
5
-
6
- def upload_directory(
7
- connector: Connector,
8
- directory: str,
9
- path: str,
10
- n_concurrent: int = 1,
11
- n_reties: int = 0,
12
- interval: float = 0,
13
- verbose: bool = False,
14
- **put_kwargs
15
- ) -> list[str]:
16
- assert path.endswith('/'), "Path must end with a slash."
17
- if path.startswith('/'):
18
- path = path[1:]
19
-
20
- _counter = 0
21
- _counter_lock = Lock()
22
-
23
- faild_files = []
24
- def put_file(file_path):
25
- with _counter_lock:
26
- nonlocal _counter
27
- _counter += 1
28
- this_count = _counter
29
- dst_path = f"{path}{os.path.relpath(file_path, directory)}"
30
- if verbose:
31
- print(f"[{this_count}] Uploading {file_path} to {dst_path}")
32
-
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:
51
- faild_files.append(file_path)
52
- if verbose:
53
- print(f"[{this_count}] Failed to upload {file_path} after {n_reties} retries.")
54
-
55
- with ThreadPoolExecutor(n_concurrent) as executor:
56
- for root, dirs, files in os.walk(directory):
57
- for file in files:
58
- executor.submit(put_file, os.path.join(root, file))
59
-
60
- return faild_files
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