lfss 0.8.0__py3-none-any.whl → 0.8.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.
frontend/api.js CHANGED
@@ -73,7 +73,8 @@ export default class Connector {
73
73
  method: 'PUT',
74
74
  headers: {
75
75
  'Authorization': 'Bearer ' + this.config.token,
76
- 'Content-Type': 'application/octet-stream'
76
+ 'Content-Type': 'application/octet-stream',
77
+ 'Content-Length': fileBytes.byteLength
77
78
  },
78
79
  body: fileBytes
79
80
  });
@@ -83,6 +84,38 @@ export default class Connector {
83
84
  return (await res.json()).url;
84
85
  }
85
86
 
87
+ /**
88
+ * @param {string} path - the path to the file (url)
89
+ * @param {File} file - the file to upload
90
+ * @returns {Promise<string>} - the promise of the request, the url of the file
91
+ */
92
+ async post(path, file, {
93
+ conflict = 'abort',
94
+ permission = 0
95
+ } = {}){
96
+ if (path.startsWith('/')){ path = path.slice(1); }
97
+ const dst = new URL(this.config.endpoint + '/' + path);
98
+ dst.searchParams.append('conflict', conflict);
99
+ dst.searchParams.append('permission', permission);
100
+ // post as multipart form data
101
+ const formData = new FormData();
102
+ formData.append('file', file);
103
+ const res = await fetch(dst.toString(), {
104
+ method: 'POST',
105
+ // don't include the content type, let the browser handle it
106
+ // https://muffinman.io/blog/uploading-files-using-fetch-multipart-form-data/
107
+ headers: {
108
+ 'Authorization': 'Bearer ' + this.config.token,
109
+ },
110
+ body: formData
111
+ });
112
+
113
+ if (res.status != 200 && res.status != 201){
114
+ throw new Error(`Failed to upload file, status code: ${res.status}, message: ${await res.json()}`);
115
+ }
116
+ return (await res.json()).url;
117
+ }
118
+
86
119
  /**
87
120
  * @param {string} path - the path to the file (url), should end with .json
88
121
  * @param {Objec} data - the data to upload
@@ -422,4 +455,28 @@ export async function listPath(conn, path, {
422
455
  dirs: dirCount,
423
456
  files: fileCount
424
457
  }];
425
- };
458
+ };
459
+
460
+ /**
461
+ * a function to wrap the upload function into one
462
+ * it will return the url of the file
463
+ *
464
+ * @typedef {Object} UploadOptions
465
+ * @property {string} conflict - the conflict resolution strategy, can be 'abort', 'replace', 'rename'
466
+ * @property {number} permission - the permission of the file, can be 0, 1, 2, 3
467
+ *
468
+ * @param {Connector} conn - the connector to the API
469
+ * @param {string} path - the path to the file (url)
470
+ * @param {File} file - the file to upload
471
+ * @param {UploadOptions} options - the options for the request
472
+ * @returns {Promise<string>} - the promise of the request, the url of the file
473
+ */
474
+ export async function uploadFile(conn, path, file, {
475
+ conflict = 'abort',
476
+ permission = 0
477
+ } = {}){
478
+ if (file.size < 1024 * 1024 * 10){
479
+ return await conn.put(path, file, {conflict, permission});
480
+ }
481
+ return await conn.post(path, file, {conflict, permission});
482
+ }
frontend/scripts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { permMap, listPath } from './api.js';
1
+ import { permMap, listPath, uploadFile } from './api.js';
2
2
  import { showFloatingWindowLineInput, showPopup } from './popup.js';
3
3
  import { formatSize, decodePathURI, ensurePathURI, getRandomString, cvtGMT2Local, debounce, encodePathURI, asHtmlText } from './utils.js';
4
4
  import { showInfoPanel, showDirInfoPanel } from './info.js';
@@ -132,7 +132,7 @@ uploadButton.addEventListener('click', () => {
132
132
  }
133
133
  path = path + fileName;
134
134
  showPopup('Uploading...', {level: 'info', timeout: 3000});
135
- conn.put(path, file, {'conflict': 'overwrite'})
135
+ uploadFile(conn, path, file, {'conflict': 'overwrite'})
136
136
  .then(() => {
137
137
  refreshFileList();
138
138
  uploadFileNameInput.value = '';
@@ -178,10 +178,10 @@ Are you sure you want to proceed?
178
178
  `)){ return; }
179
179
 
180
180
  let counter = 0;
181
- async function uploadFile(...args){
181
+ async function uploadFileFn(...args){
182
182
  const [file, path] = args;
183
183
  try{
184
- await conn.put(path, file, {conflict: 'overwrite'});
184
+ await uploadFile(conn, path, file, {conflict: 'overwrite'});
185
185
  }
186
186
  catch (err){
187
187
  showPopup('Failed to upload file [' + file.name + ']: ' + err, {level: 'error', timeout: 5000});
@@ -194,7 +194,7 @@ Are you sure you want to proceed?
194
194
  for (let i = 0; i < files.length; i++){
195
195
  const file = files[i];
196
196
  const path = dstPath + file.name;
197
- promises.push(uploadFile(file, path));
197
+ promises.push(uploadFileFn(file, path));
198
198
  }
199
199
  showPopup('Uploading multiple files...', {level: 'info', timeout: 3000});
200
200
  Promise.all(promises).then(
lfss/api/__init__.py CHANGED
@@ -18,9 +18,13 @@ def upload_file(
18
18
  error_msg = ""
19
19
  while this_try <= n_retries:
20
20
  try:
21
- with open(file_path, 'rb') as f:
22
- blob = f.read()
23
- connector.put(dst_url, blob, **put_kwargs)
21
+ fsize = os.path.getsize(file_path)
22
+ if fsize < 32 * 1024 * 1024: # 32MB
23
+ with open(file_path, 'rb') as f:
24
+ blob = f.read()
25
+ connector.put(dst_url, blob, **put_kwargs)
26
+ else:
27
+ connector.post(dst_url, file_path, **put_kwargs)
24
28
  break
25
29
  except Exception as e:
26
30
  if isinstance(e, KeyboardInterrupt):
@@ -97,14 +101,24 @@ def download_file(
97
101
  print(f"File {file_path} already exists, skipping download.")
98
102
  return True, error_msg
99
103
  try:
100
- blob = connector.get(src_url)
101
- if blob is None:
104
+ fmeta = connector.get_metadata(src_url)
105
+ if fmeta is None:
102
106
  error_msg = "File not found."
103
107
  return False, error_msg
108
+
104
109
  pathlib.Path(file_path).parent.mkdir(parents=True, exist_ok=True)
105
- with open(file_path, 'wb') as f:
106
- f.write(blob)
110
+ fsize = fmeta.file_size # type: ignore
111
+ if fsize < 32 * 1024 * 1024: # 32MB
112
+ blob = connector.get(src_url)
113
+ assert blob is not None
114
+ with open(file_path, 'wb') as f:
115
+ f.write(blob)
116
+ else:
117
+ with open(file_path, 'wb') as f:
118
+ for chunk in connector.get_stream(src_url):
119
+ f.write(chunk)
107
120
  break
121
+
108
122
  except Exception as e:
109
123
  if isinstance(e, KeyboardInterrupt):
110
124
  raise e
lfss/api/connector.py CHANGED
@@ -4,6 +4,7 @@ import os
4
4
  import requests
5
5
  import requests.adapters
6
6
  import urllib.parse
7
+ from tempfile import SpooledTemporaryFile
7
8
  from lfss.src.error import PathNotFoundError
8
9
  from lfss.src.datatype import (
9
10
  FileReadPermission, FileRecord, DirectoryRecord, UserRecord, PathContents,
@@ -95,6 +96,42 @@ class Connector:
95
96
  )
96
97
  return response.json()
97
98
 
99
+ def post(self, path, file: str | bytes, permission: int | FileReadPermission = 0, conflict: Literal['overwrite', 'abort', 'skip', 'skip-ahead'] = 'abort'):
100
+ """
101
+ Uploads a file to the specified path,
102
+ using the POST method, with form-data/multipart.
103
+ file can be a path to a file on disk, or bytes.
104
+ """
105
+
106
+ # Skip ahead by checking if the file already exists
107
+ if conflict == 'skip-ahead':
108
+ exists = self.get_metadata(path)
109
+ if exists is None:
110
+ conflict = 'skip'
111
+ else:
112
+ return {'status': 'skipped', 'path': path}
113
+
114
+ if isinstance(file, str):
115
+ assert os.path.exists(file), "File does not exist on disk"
116
+ fsize = os.path.getsize(file)
117
+
118
+ with open(file, 'rb') if isinstance(file, str) else SpooledTemporaryFile(max_size=1024*1024*32) as fp:
119
+
120
+ if isinstance(file, bytes):
121
+ fsize = len(file)
122
+ fp.write(file)
123
+ fp.seek(0)
124
+
125
+ # https://stackoverflow.com/questions/12385179/
126
+ print(f"Uploading {fsize} bytes")
127
+ response = self._fetch_factory('POST', path, search_params={
128
+ 'permission': int(permission),
129
+ 'conflict': conflict
130
+ })(
131
+ files={'file': fp},
132
+ )
133
+ return response.json()
134
+
98
135
  def put_json(self, path: str, data: dict, permission: int | FileReadPermission = 0, conflict: Literal['overwrite', 'abort', 'skip', 'skip-ahead'] = 'abort'):
99
136
  """Uploads a JSON file to the specified path."""
100
137
  assert path.endswith('.json'), "Path must end with .json"
lfss/src/config.py CHANGED
@@ -18,7 +18,7 @@ if __env_large_file is not None:
18
18
  LARGE_FILE_BYTES = parse_storage_size(__env_large_file)
19
19
  else:
20
20
  LARGE_FILE_BYTES = 8 * 1024 * 1024 # 8MB
21
- MAX_FILE_BYTES = 512 * 1024 * 1024 # 512MB
21
+ MAX_MEM_FILE_BYTES = 128 * 1024 * 1024 # 128MB
22
22
  MAX_BUNDLE_BYTES = 512 * 1024 * 1024 # 512MB
23
23
  CHUNK_SIZE = 1024 * 1024 # 1MB chunks for streaming (on large files)
24
24
 
lfss/src/database.py CHANGED
@@ -8,13 +8,14 @@ import zipfile, io, asyncio
8
8
 
9
9
  import aiosqlite, aiofiles
10
10
  import aiofiles.os
11
+ import mimetypes, mimesniff
11
12
 
12
13
  from .connection_pool import execute_sql, unique_cursor, transaction
13
14
  from .datatype import (
14
15
  UserRecord, FileReadPermission, FileRecord, DirectoryRecord, PathContents,
15
16
  FileSortKey, DirSortKey, isValidFileSortKey, isValidDirSortKey
16
17
  )
17
- from .config import LARGE_BLOB_DIR, CHUNK_SIZE
18
+ from .config import LARGE_BLOB_DIR, CHUNK_SIZE, LARGE_FILE_BYTES, MAX_MEM_FILE_BYTES
18
19
  from .log import get_logger
19
20
  from .utils import decode_uri_compnents, hash_credential, concurrent_wrap, debounce_async
20
21
  from .error import *
@@ -285,8 +286,7 @@ class FileConn(DBObjectBase):
285
286
  async def user_size(self, user_id: int) -> int:
286
287
  cursor = await self.cur.execute("SELECT size FROM usize WHERE user_id = ?", (user_id, ))
287
288
  res = await cursor.fetchone()
288
- if res is None:
289
- return -1
289
+ if res is None: return 0
290
290
  return res[0]
291
291
  async def _user_size_inc(self, user_id: int, inc: int):
292
292
  self.logger.debug(f"Increasing user {user_id} size by {inc}")
@@ -534,59 +534,66 @@ class Database:
534
534
 
535
535
  async def save_file(
536
536
  self, u: int | str, url: str,
537
- blob: bytes | AsyncIterable[bytes],
537
+ blob_stream: AsyncIterable[bytes],
538
538
  permission: FileReadPermission = FileReadPermission.UNSET,
539
- mime_type: str = 'application/octet-stream'
540
- ):
539
+ mime_type: Optional[str] = None
540
+ ) -> int:
541
541
  """
542
- if file_size is not provided, the blob must be bytes
542
+ Save a file to the database.
543
+ Will check file size and user storage limit,
544
+ should check permission before calling this method.
543
545
  """
544
546
  validate_url(url)
545
547
  async with unique_cursor() as cur:
546
548
  user = await get_user(cur, u)
547
- if user is None:
548
- return
549
-
550
- # check if the user is the owner of the path, or is admin
551
- if url.startswith('/'):
552
- url = url[1:]
553
- first_component = url.split('/')[0]
554
- if first_component != user.username:
555
- if not user.is_admin:
556
- raise PermissionDeniedError(f"Permission denied: {user.username} cannot write to {url}")
557
- else:
558
- if await get_user(cur, first_component) is None:
559
- raise PermissionDeniedError(f"Invalid path: {first_component} is not a valid username")
549
+ assert user is not None, f"User {u} not found"
560
550
 
561
551
  fconn_r = FileConn(cur)
562
552
  user_size_used = await fconn_r.user_size(user.id)
563
553
 
564
- if isinstance(blob, bytes):
565
- file_size = len(blob)
566
- if user_size_used + file_size > user.max_storage:
567
- raise StorageExceededError(f"Unable to save file, user {user.username} has storage limit of {user.max_storage}, used {user_size_used}, requested {file_size}")
568
- f_id = uuid.uuid4().hex
569
-
570
- async with transaction() as w_cur:
571
- fconn_w = FileConn(w_cur)
572
- await fconn_w.set_file_blob(f_id, blob)
573
- await fconn_w.set_file_record(
574
- url, owner_id=user.id, file_id=f_id, file_size=file_size,
575
- permission=permission, external=False, mime_type=mime_type)
576
- else:
577
- assert isinstance(blob, AsyncIterable)
578
- f_id = uuid.uuid4().hex
579
- file_size = await FileConn.set_file_blob_external(f_id, blob)
554
+ f_id = uuid.uuid4().hex
555
+ async with aiofiles.tempfile.SpooledTemporaryFile(max_size=MAX_MEM_FILE_BYTES) as f:
556
+ async for chunk in blob_stream:
557
+ await f.write(chunk)
558
+ file_size = await f.tell()
580
559
  if user_size_used + file_size > user.max_storage:
581
- await FileConn.delete_file_blob_external(f_id)
582
560
  raise StorageExceededError(f"Unable to save file, user {user.username} has storage limit of {user.max_storage}, used {user_size_used}, requested {file_size}")
583
561
 
584
- async with transaction() as w_cur:
585
- await FileConn(w_cur).set_file_record(
586
- url, owner_id=user.id, file_id=f_id, file_size=file_size,
587
- permission=permission, external=True, mime_type=mime_type)
588
-
562
+ # check mime type
563
+ if mime_type is None:
564
+ fname = url.split('/')[-1]
565
+ mime_type, _ = mimetypes.guess_type(fname)
566
+ if mime_type is None:
567
+ await f.seek(0)
568
+ mime_type = mimesniff.what(await f.read(1024))
569
+ if mime_type is None:
570
+ mime_type = 'application/octet-stream'
571
+ await f.seek(0)
572
+
573
+ if file_size < LARGE_FILE_BYTES:
574
+ blob = await f.read()
575
+ async with transaction() as w_cur:
576
+ fconn_w = FileConn(w_cur)
577
+ await fconn_w.set_file_blob(f_id, blob)
578
+ await fconn_w.set_file_record(
579
+ url, owner_id=user.id, file_id=f_id, file_size=file_size,
580
+ permission=permission, external=False, mime_type=mime_type)
581
+
582
+ else:
583
+ async def blob_stream_tempfile():
584
+ nonlocal f
585
+ while True:
586
+ chunk = await f.read(CHUNK_SIZE)
587
+ if not chunk: break
588
+ yield chunk
589
+ await FileConn.set_file_blob_external(f_id, blob_stream_tempfile())
590
+ async with transaction() as w_cur:
591
+ await FileConn(w_cur).set_file_record(
592
+ url, owner_id=user.id, file_id=f_id, file_size=file_size,
593
+ permission=permission, external=True, mime_type=mime_type)
594
+
589
595
  await delayed_log_activity(user.username)
596
+ return file_size
590
597
 
591
598
  async def read_file_stream(self, url: str) -> AsyncIterable[bytes]:
592
599
  validate_url(url)
lfss/src/server.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from typing import Optional, Literal
2
2
  from functools import wraps
3
3
 
4
- from fastapi import FastAPI, APIRouter, Depends, Request, Response
4
+ from fastapi import FastAPI, APIRouter, Depends, Request, Response, UploadFile
5
5
  from fastapi.responses import StreamingResponse
6
6
  from fastapi.exceptions import HTTPException
7
7
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
@@ -9,16 +9,15 @@ from fastapi.middleware.cors import CORSMiddleware
9
9
  import mimesniff
10
10
 
11
11
  import asyncio, json, time
12
- import mimetypes
13
12
  from contextlib import asynccontextmanager
14
13
 
15
14
  from .error import *
16
15
  from .log import get_logger
17
16
  from .stat import RequestDB
18
- from .config import MAX_BUNDLE_BYTES, MAX_FILE_BYTES, LARGE_FILE_BYTES, CHUNK_SIZE
17
+ from .config import MAX_BUNDLE_BYTES, MAX_MEM_FILE_BYTES, LARGE_FILE_BYTES, CHUNK_SIZE
19
18
  from .utils import ensure_uri_compnents, format_last_modified, now_stamp, wait_for_debounce_tasks
20
19
  from .connection_pool import global_connection_init, global_connection_close, unique_cursor
21
- from .database import Database, DECOY_USER, check_user_permission, UserConn, FileConn, delayed_log_activity
20
+ from .database import Database, DECOY_USER, check_user_permission, UserConn, FileConn, delayed_log_activity, get_user
22
21
  from .datatype import (
23
22
  FileReadPermission, FileRecord, UserRecord, PathContents,
24
23
  FileSortKey, DirSortKey
@@ -246,18 +245,21 @@ async def put_file(
246
245
  path: str,
247
246
  conflict: Literal["overwrite", "skip", "abort"] = "abort",
248
247
  permission: int = 0,
249
- user: UserRecord = Depends(registered_user)):
248
+ user: UserRecord = Depends(registered_user)
249
+ ):
250
250
  path = ensure_uri_compnents(path)
251
- if not path.startswith(f"{user.username}/") and not user.is_admin:
252
- logger.debug(f"Reject put request from {user.username} to {path}")
253
- raise HTTPException(status_code=403, detail="Permission denied")
254
-
255
- content_length = request.headers.get("Content-Length")
256
- if content_length is not None:
257
- content_length = int(content_length)
258
- if content_length > MAX_FILE_BYTES:
259
- logger.debug(f"Reject put request from {user.username} to {path}, file too large")
260
- raise HTTPException(status_code=413, detail="File too large")
251
+ assert not path.endswith("/"), "Path must not end with /"
252
+ if not path.startswith(f"{user.username}/"):
253
+ if not user.is_admin:
254
+ logger.debug(f"Reject put request from {user.username} to {path}")
255
+ raise HTTPException(status_code=403, detail="Permission denied")
256
+ else:
257
+ first_comp = path.split("/")[0]
258
+ async with unique_cursor() as c:
259
+ uconn = UserConn(c)
260
+ owner = await uconn.get_user(first_comp)
261
+ if not owner:
262
+ raise HTTPException(status_code=404, detail="Owner not found")
261
263
 
262
264
  logger.info(f"PUT {path}, user: {user.username}")
263
265
  exists_flag = False
@@ -280,47 +282,73 @@ async def put_file(
280
282
  # check content-type
281
283
  content_type = request.headers.get("Content-Type")
282
284
  logger.debug(f"Content-Type: {content_type}")
283
- if content_type == "application/json":
284
- body = await request.json()
285
- blobs = json.dumps(body).encode('utf-8')
286
- elif content_type == "application/x-www-form-urlencoded":
287
- # may not work...
288
- body = await request.form()
289
- file = body.get("file")
290
- if isinstance(file, str) or file is None:
291
- raise HTTPException(status_code=400, detail="Invalid form data, file required")
292
- blobs = await file.read()
293
- elif content_type == "application/octet-stream":
294
- blobs = await request.body()
295
- else:
296
- blobs = await request.body()
285
+ if not (content_type == "application/octet-stream" or content_type == "application/json"):
286
+ raise HTTPException(status_code=415, detail="Unsupported content type, put request must be application/json or application/octet-stream")
297
287
 
298
- # check file type
299
- assert not path.endswith("/"), "Path must be a file"
300
- fname = path.split("/")[-1]
301
- mime_t, _ = mimetypes.guess_type(fname)
302
- if mime_t is None:
303
- mime_t = mimesniff.what(blobs)
304
- if mime_t is None:
305
- mime_t = "application/octet-stream"
306
-
307
- if len(blobs) > LARGE_FILE_BYTES:
308
- async def blob_reader():
309
- for b in range(0, len(blobs), CHUNK_SIZE):
310
- yield blobs[b:b+CHUNK_SIZE]
311
- await db.save_file(user.id, path, blob_reader(), permission = FileReadPermission(permission), mime_type = mime_t)
312
- else:
313
- await db.save_file(user.id, path, blobs, permission = FileReadPermission(permission), mime_type=mime_t)
288
+ async def blob_reader():
289
+ nonlocal request
290
+ async for chunk in request.stream():
291
+ yield chunk
292
+
293
+ await db.save_file(user.id, path, blob_reader(), permission = FileReadPermission(permission))
314
294
 
315
295
  # https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/PUT
316
- if exists_flag:
317
- return Response(status_code=201, headers={
318
- "Content-Type": "application/json",
319
- }, content=json.dumps({"url": path}))
320
- else:
321
- return Response(status_code=200, headers={
322
- "Content-Type": "application/json",
323
- }, content=json.dumps({"url": path}))
296
+ return Response(status_code=200 if exists_flag else 201, headers={
297
+ "Content-Type": "application/json",
298
+ }, content=json.dumps({"url": path}))
299
+
300
+ # using form-data instead of raw body
301
+ @router_fs.post("/{path:path}")
302
+ @handle_exception
303
+ async def post_file(
304
+ path: str,
305
+ file: UploadFile,
306
+ conflict: Literal["overwrite", "skip", "abort"] = "abort",
307
+ permission: int = 0,
308
+ user: UserRecord = Depends(registered_user)
309
+ ):
310
+ path = ensure_uri_compnents(path)
311
+ assert not path.endswith("/"), "Path must not end with /"
312
+ if not path.startswith(f"{user.username}/"):
313
+ if not user.is_admin:
314
+ logger.debug(f"Reject put request from {user.username} to {path}")
315
+ raise HTTPException(status_code=403, detail="Permission denied")
316
+ else:
317
+ first_comp = path.split("/")[0]
318
+ async with unique_cursor() as conn:
319
+ uconn = UserConn(conn)
320
+ owner = await uconn.get_user(first_comp)
321
+ if not owner:
322
+ raise HTTPException(status_code=404, detail="Owner not found")
323
+
324
+ logger.info(f"POST {path}, user: {user.username}")
325
+ exists_flag = False
326
+ async with unique_cursor() as conn:
327
+ fconn = FileConn(conn)
328
+ file_record = await fconn.get_file_record(path)
329
+
330
+ if file_record:
331
+ if conflict == "abort":
332
+ raise HTTPException(status_code=409, detail="File exists")
333
+ if conflict == "skip":
334
+ return Response(status_code=200, headers={
335
+ "Content-Type": "application/json",
336
+ }, content=json.dumps({"url": path}))
337
+ exists_flag = True
338
+ if not user.is_admin and not file_record.owner_id == user.id:
339
+ raise HTTPException(status_code=403, detail="Permission denied, cannot overwrite other's file")
340
+ await db.delete_file(path)
341
+
342
+ async def blob_reader():
343
+ nonlocal file
344
+ while (chunk := await file.read(CHUNK_SIZE)):
345
+ yield chunk
346
+
347
+ await db.save_file(user.id, path, blob_reader(), permission = FileReadPermission(permission))
348
+ return Response(status_code=200 if exists_flag else 201, headers={
349
+ "Content-Type": "application/json",
350
+ }, content=json.dumps({"url": path}))
351
+
324
352
 
325
353
  @router_fs.delete("/{path:path}")
326
354
  @handle_exception
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.8.0
3
+ Version: 0.8.1
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -16,6 +16,7 @@ Requires-Dist: aiosqlite (==0.*)
16
16
  Requires-Dist: fastapi (==0.*)
17
17
  Requires-Dist: mimesniff (==1.*)
18
18
  Requires-Dist: pillow
19
+ Requires-Dist: python-multipart
19
20
  Requires-Dist: requests (==2.*)
20
21
  Requires-Dist: uvicorn (==0.*)
21
22
  Project-URL: Repository, https://github.com/MenxLi/lfss
@@ -1,7 +1,7 @@
1
1
  Readme.md,sha256=LpbTvUWjCOv4keMNDrZvEnNAmCQnvaxvlq2srWixXn0,1299
2
2
  docs/Known_issues.md,sha256=rfdG3j1OJF-59S9E06VPyn0nZKbW-ybPxkoZ7MEZWp8,81
3
3
  docs/Permission.md,sha256=9r9nEmhqfz18RTS8FI0fZ9F0a31r86OoAyx3EQxxpk0,2317
4
- frontend/api.js,sha256=9cR8ddaoF-ulauQ1tcISV2nmfakkplC7uS8-lWFqU58,15820
4
+ frontend/api.js,sha256=wUJNAkL8QigAiwR_jaMPUhCQEsL-lp0wZ6XeueYgunE,18049
5
5
  frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
6
6
  frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
7
7
  frontend/info.js,sha256=WhOGaeqMoezEAfg4nIpK26hvejC7AZ-ZDLiJmRj0kDk,5758
@@ -9,14 +9,14 @@ frontend/login.css,sha256=VMM0QfbDFYerxKWKSGhMI1yg5IRBXg0TTdLJEEhQZNk,355
9
9
  frontend/login.js,sha256=QoO8yKmBHDVP-ZomCMOaV7xVUVIhpl7esJrb6T5aHQE,2466
10
10
  frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
11
11
  frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
12
- frontend/scripts.js,sha256=PFgAZC-frV-qs6sYCb1ZzH_CAa7in17qbfqYiaIxNmA,21697
12
+ frontend/scripts.js,sha256=2YoMhrcAkI1bHihD_2EK6uCHZ1s0DiIR3FzZsh79x9A,21729
13
13
  frontend/state.js,sha256=vbNL5DProRKmSEY7xu9mZH6IY0PBenF8WGxPtGgDnLI,1680
14
14
  frontend/styles.css,sha256=xcNLqI3KBsY5TLnku8UIP0Jfr7QLajr1_KNlZj9eheM,4935
15
15
  frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
16
16
  frontend/thumb.js,sha256=RQ_whXNwmkdG4SEbNQGeh488YYzqwoNYDc210hPeuhQ,5703
17
17
  frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
18
- lfss/api/__init__.py,sha256=YI1_9nyW0E5lyXn_PmmIzIff1ccBC-KCA0twpsKDRIY,5453
19
- lfss/api/connector.py,sha256=q9CJBOmN83tfpwI1IclSzq_lzI4Kq1SOKC3S5H-vuWo,9301
18
+ lfss/api/__init__.py,sha256=MRzwISePOdq3of9IWGryVWX6coGkxeJ3OEh42Se4IYc,6029
19
+ lfss/api/connector.py,sha256=tmcgKswE0sktBhanEDEc6mpuJA0de7C-DS2YqlpxHX4,10743
20
20
  lfss/cli/balance.py,sha256=R2rbO2tg9TVnnQIVeU0GJVeMS-5LDhEdk4mbOE9qGq0,4121
21
21
  lfss/cli/cli.py,sha256=8VKe41m_LhVSFxGlvgBxdz55sjscLNbbkNX1fOnmES4,4618
22
22
  lfss/cli/panel.py,sha256=iGdVmdWYjA_7a78ZzWEB_3ggIOBeUKTzg6F5zLaB25c,1401
@@ -27,17 +27,17 @@ lfss/sql/init.sql,sha256=C-JtQAlaOjESI8uoF1Y_9dKukEVSw5Ll-7yA3gG-XHU,1210
27
27
  lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
28
28
  lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  lfss/src/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
30
- lfss/src/config.py,sha256=K1b5clNRO4EzxDG-p6U5aRLDaq-Up0tDHfn_D79D0ns,857
30
+ lfss/src/config.py,sha256=7k_6L_aG7vD-lVcmgpwMIg-ggpMMDNU8IbaB4y1effE,861
31
31
  lfss/src/connection_pool.py,sha256=4YULPcmndy33FyBSo9w1fZjAlBX2q-xPP27xJOTA06I,5228
32
- lfss/src/database.py,sha256=yFMoxHJ-ZRwi9qvSVdIl9m_gpycmL2eencdv7P0vzwQ,35678
32
+ lfss/src/database.py,sha256=Cexv6r9sZl29hWzFyL_J_kWz9roUbut6A246Zc4ORs0,35885
33
33
  lfss/src/datatype.py,sha256=q2lc8BJaB2sS7gtqPMqxM625mckgI2SmvuqbadiObLY,2158
34
34
  lfss/src/error.py,sha256=Bh_GUtuNsMSMIKbFreU4CfZzL96TZdNAIcgmDxvGbQ0,333
35
35
  lfss/src/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
36
- lfss/src/server.py,sha256=OcyPuVyvBCxpgF8ESxzDYNZnUJIbkOGDv-eghUSVM9E,20063
36
+ lfss/src/server.py,sha256=mUu0WbiGdM08Jos7a4r_e9ND5sK_tNDEv1VGRPIHvLk,21206
37
37
  lfss/src/stat.py,sha256=WlRiiAl0rHX9oPi3thi6K4GKn70WHpClSDCt7iZUow4,3197
38
38
  lfss/src/thumb.py,sha256=qjCNMpnCozMuzkhm-2uAYy1eAuYTeWG6xqs-13HX-7k,3266
39
39
  lfss/src/utils.py,sha256=nal2rpr00jq1PeFhGQXkvU0FIbtRhXTj8VmbeIyRyLI,5184
40
- lfss-0.8.0.dist-info/METADATA,sha256=uNZzNHCActK1ok2aPmZZqMXE3JxyWHStAVAH5mzpvkc,2076
41
- lfss-0.8.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
42
- lfss-0.8.0.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
43
- lfss-0.8.0.dist-info/RECORD,,
40
+ lfss-0.8.1.dist-info/METADATA,sha256=PMNu6iNXnpYU6Lyxvlr9-e-ypSj71dygYyqH3mTHzE0,2108
41
+ lfss-0.8.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
42
+ lfss-0.8.1.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
43
+ lfss-0.8.1.dist-info/RECORD,,
File without changes