lfss 0.4.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -54,10 +54,16 @@ export default class Connector {
54
54
  * @param {File} file - the file to upload
55
55
  * @returns {Promise<string>} - the promise of the request, the url of the file
56
56
  */
57
- async put(path, file){
57
+ async put(path, file, {
58
+ overwrite = false,
59
+ permission = 0
60
+ } = {}){
58
61
  if (path.startsWith('/')){ path = path.slice(1); }
59
62
  const fileBytes = await file.arrayBuffer();
60
- const res = await fetch(this.config.endpoint + '/' + path, {
63
+ const dst = new URL(this.config.endpoint + '/' + path);
64
+ dst.searchParams.append('overwrite', overwrite);
65
+ dst.searchParams.append('permission', permission);
66
+ const res = await fetch(dst.toString(), {
61
67
  method: 'PUT',
62
68
  headers: {
63
69
  'Authorization': 'Bearer ' + this.config.token,
@@ -172,7 +172,7 @@ Are you sure you want to proceed?
172
172
  async function uploadFile(...args){
173
173
  const [file, path] = args;
174
174
  try{
175
- await conn.put(path, file);
175
+ await conn.put(path, file, {overwrite: true});
176
176
  }
177
177
  catch (err){
178
178
  showPopup('Failed to upload file [' + file.name + ']: ' + err, {level: 'error', timeout: 5000});
@@ -0,0 +1,48 @@
1
+ from lfss.client import Connector, upload_directory
2
+ from lfss.src.database 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 retries, only works with directory upload")
17
+ sp_upload.add_argument("--overwrite", action="store_true", help="Overwrite existing files")
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
+ 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
+ overwrite=args.overwrite,
36
+ permission=args.permission
37
+ )
38
+ else:
39
+ with open(args.src, 'rb') as f:
40
+ connector.put(
41
+ args.dst,
42
+ f.read(),
43
+ overwrite=args.overwrite,
44
+ permission=args.permission
45
+ )
46
+ else:
47
+ raise NotImplementedError(f"Command {args.command} not implemented.")
48
+
@@ -0,0 +1,58 @@
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 verbose:
43
+ print(f"[{this_count}] Error uploading {file_path}: {e}, retrying...")
44
+ this_try += 1
45
+ finally:
46
+ time.sleep(interval)
47
+
48
+ if this_try > n_reties:
49
+ faild_files.append(file_path)
50
+ if verbose:
51
+ print(f"[{this_count}] Failed to upload {file_path} after {n_reties} retries.")
52
+
53
+ with ThreadPoolExecutor(n_concurrent) as executor:
54
+ for root, dirs, files in os.walk(directory):
55
+ for file in files:
56
+ executor.submit(put_file, os.path.join(root, file))
57
+
58
+ return faild_files
@@ -34,9 +34,14 @@ class Connector:
34
34
  return response
35
35
  return f
36
36
 
37
- def put(self, path: str, file_data: bytes):
37
+ def put(self, path: str, file_data: bytes, permission: int | FileReadPermission = 0, overwrite: bool = False):
38
38
  """Uploads a file to the specified path."""
39
- response = self._fetch('PUT', path)(
39
+ if path.startswith('/'):
40
+ path = path[1:]
41
+ response = self._fetch('PUT', path, search_params={
42
+ 'permission': int(permission),
43
+ 'overwrite': overwrite
44
+ })(
40
45
  data=file_data,
41
46
  headers={'Content-Type': 'application/octet-stream'}
42
47
  )
@@ -543,7 +543,7 @@ class Database:
543
543
  if _g_conn is not None:
544
544
  await _g_conn.rollback()
545
545
 
546
- async def save_file(self, u: int | str, url: str, blob: bytes):
546
+ async def save_file(self, u: int | str, url: str, blob: bytes, permission: FileReadPermission = FileReadPermission.UNSET):
547
547
  validate_url(url)
548
548
  assert isinstance(blob, bytes), "blob must be bytes"
549
549
 
@@ -571,7 +571,7 @@ class Database:
571
571
  f_id = uuid.uuid4().hex
572
572
  async with transaction(self):
573
573
  await self.file.set_file_blob(f_id, blob)
574
- await self.file.set_file_record(url, owner_id=user.id, file_id=f_id, file_size=file_size)
574
+ await self.file.set_file_record(url, owner_id=user.id, file_id=f_id, file_size=file_size, permission=permission)
575
575
  await self.user.set_active(user.username)
576
576
 
577
577
  # async def read_file_stream(self, url: str): ...
@@ -162,7 +162,12 @@ async def get_file(path: str, download = False, user: UserRecord = Depends(get_c
162
162
 
163
163
  @router_fs.put("/{path:path}")
164
164
  @handle_exception
165
- async def put_file(request: Request, path: str, user: UserRecord = Depends(get_current_user)):
165
+ async def put_file(
166
+ request: Request,
167
+ path: str,
168
+ overwrite: Optional[bool] = False,
169
+ permission: int = 0,
170
+ user: UserRecord = Depends(get_current_user)):
166
171
  path = ensure_uri_compnents(path)
167
172
  if user.id == 0:
168
173
  logger.debug("Reject put request from DECOY_USER")
@@ -182,8 +187,10 @@ async def put_file(request: Request, path: str, user: UserRecord = Depends(get_c
182
187
  exists_flag = False
183
188
  file_record = await conn.file.get_file_record(path)
184
189
  if file_record:
185
- exists_flag = True
190
+ if not overwrite:
191
+ raise HTTPException(status_code=409, detail="File exists")
186
192
  # remove the old file
193
+ exists_flag = True
187
194
  await conn.delete_file(path)
188
195
 
189
196
  # check content-type
@@ -191,20 +198,20 @@ async def put_file(request: Request, path: str, user: UserRecord = Depends(get_c
191
198
  logger.debug(f"Content-Type: {content_type}")
192
199
  if content_type == "application/json":
193
200
  body = await request.json()
194
- await conn.save_file(user.id, path, json.dumps(body).encode('utf-8'))
201
+ await conn.save_file(user.id, path, json.dumps(body).encode('utf-8'), permission = FileReadPermission(permission))
195
202
  elif content_type == "application/x-www-form-urlencoded":
196
203
  # may not work...
197
204
  body = await request.form()
198
205
  file = body.get("file")
199
206
  if isinstance(file, str) or file is None:
200
207
  raise HTTPException(status_code=400, detail="Invalid form data, file required")
201
- await conn.save_file(user.id, path, await file.read())
208
+ await conn.save_file(user.id, path, await file.read(), permission = FileReadPermission(permission))
202
209
  elif content_type == "application/octet-stream":
203
210
  body = await request.body()
204
- await conn.save_file(user.id, path, body)
211
+ await conn.save_file(user.id, path, body, permission = FileReadPermission(permission))
205
212
  else:
206
213
  body = await request.body()
207
- await conn.save_file(user.id, path, body)
214
+ await conn.save_file(user.id, path, body, permission = FileReadPermission(permission))
208
215
 
209
216
  # https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/PUT
210
217
  if exists_flag:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "lfss"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "Lightweight file storage service"
5
5
  authors = ["li, mengxun <limengxun45@outlook.com>"]
6
6
  readme = "Readme.md"
@@ -18,6 +18,7 @@ mimesniff = "1.*"
18
18
  lfss-serve = "lfss.cli.serve:main"
19
19
  lfss-user = "lfss.cli.user:main"
20
20
  lfss-panel = "lfss.cli.panel:main"
21
+ lfss-cli = "lfss.cli.cli:main"
21
22
 
22
23
  [build-system]
23
24
  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