lfss 0.7.6__py3-none-any.whl → 0.7.8__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
@@ -20,6 +20,7 @@
20
20
  * @property {number} file_size - the size of the file, in bytes
21
21
  * @property {string} create_time - the time the file was created
22
22
  * @property {string} access_time - the time the file was last accessed
23
+ * @property {string} mime_type - the mime type of the file
23
24
  *
24
25
  * Partially complete...
25
26
  * @typedef {Object} DirectoryRecord
frontend/index.html CHANGED
@@ -31,10 +31,9 @@
31
31
  <table id="files">
32
32
  <thead>
33
33
  <tr>
34
- <th>File</th>
34
+ <th>Name</th>
35
35
  <th>Size</th>
36
36
  <th>Accessed</th>
37
- <th>Created</th>
38
37
  <th>Read access</th>
39
38
  <th>Actions</th>
40
39
  </tr>
frontend/info.css ADDED
@@ -0,0 +1,36 @@
1
+
2
+ div.info-container {
3
+ display: flex;
4
+ flex-direction: column;
5
+ margin: 1rem;
6
+ gap: 1rem;
7
+ }
8
+ div.info-container-left {
9
+ margin: 0 auto;
10
+ padding: 10px;
11
+ border: 1px solid #ccc;
12
+ border-radius: 5px;
13
+ background-color: #f9f9f9;
14
+ }
15
+
16
+ td {
17
+ text-align: left;
18
+ text-wrap: nowrap;
19
+ width: fit-content;
20
+ }
21
+ td.info-table-key {
22
+ font-weight: bold;
23
+ padding-right: 1rem;
24
+ }
25
+
26
+ div.info-container-right {
27
+ display: flex;
28
+ flex-direction: column;
29
+ gap: 0.5rem;
30
+ width: 100%;
31
+ }
32
+ div.info-path-copy {
33
+ display: flex;
34
+ gap: 0.5rem;
35
+ align-items: center;
36
+ }
frontend/info.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @import { UserRecord, FileRecord, DirectoryRecord } from "./api.js";
3
+ */
4
+ import Connector from "./api.js";
5
+ import { createFloatingWindow, showPopup } from "./popup.js";
6
+ import { cvtGMT2Local, formatSize, decodePathURI, copyToClipboard } from "./utils.js";
7
+
8
+ const ensureSlashEnd = (path) => {
9
+ return path.endsWith('/') ? path : path + '/';
10
+ }
11
+
12
+ /**
13
+ * @param {FileRecord} r
14
+ * @param {UserRecord} u
15
+ */
16
+ export function showInfoPanel(r, u){
17
+ const innerHTML = `
18
+ <div class="info-container">
19
+ <div class="info-container-left">
20
+ <table class="info-table">
21
+ <tr>
22
+ <td class="info-table-key">Name</td>
23
+ <td class="info-table-value">${decodePathURI(r.url).split('/').pop()}</td>
24
+ </tr>
25
+ <tr>
26
+ <td class="info-table-key">Size</td>
27
+ <td class="info-table-value">${formatSize(r.file_size)}</td>
28
+ </tr>
29
+ <tr>
30
+ <td class="info-table-key">File-Type</td>
31
+ <td class="info-table-value">${r.mime_type}</td>
32
+ </tr>
33
+ <tr>
34
+ <td class="info-table-key">Owner-ID</td>
35
+ <td class="info-table-value">${r.owner_id}</td>
36
+ </tr>
37
+ <tr>
38
+ <td class="info-table-key">Access-Time</td>
39
+ <td class="info-table-value">${cvtGMT2Local(r.access_time)}</td>
40
+ </tr>
41
+ <tr>
42
+ <td class="info-table-key">Create-Time</td>
43
+ <td class="info-table-value">${cvtGMT2Local(r.create_time)}</td>
44
+ </tr>
45
+ </table>
46
+ </div>
47
+ <div class="info-container-right">
48
+ <div class="info-path-copy">
49
+ <input type="text" value="${window.location.origin}/${r.url}" readonly>
50
+ <button class="copy-button" id='copy-btn-full-path'>📋</button>
51
+ </div>
52
+ <div class="info-path-copy">
53
+ <input type="text" value="${r.url}" readonly>
54
+ <button class="copy-button" id='copy-btn-rel-path'>📋</button>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ `
59
+ const [win, closeWin] = createFloatingWindow(innerHTML, {title: 'File Info'});
60
+ document.getElementById('copy-btn-full-path').onclick = () => {
61
+ copyToClipboard(window.location.origin + '/' + r.url);
62
+ showPopup('Path copied to clipboard', {timeout: 2000, level: 'success'});
63
+ }
64
+ document.getElementById('copy-btn-rel-path').onclick = () => {
65
+ copyToClipboard(r.url);
66
+ showPopup('Path copied to clipboard', {timeout: 2000, level: 'success'});
67
+ }
68
+ }
69
+
70
+ /**
71
+ * @param {DirectoryRecord} r
72
+ * @param {UserRecord} u
73
+ * @param {Connector} c
74
+ */
75
+ export function showDirInfoPanel(r, u, c){
76
+ let fmtPath = decodePathURI(r.url);
77
+ if (fmtPath.endsWith('/')) {
78
+ fmtPath = fmtPath.slice(0, -1);
79
+ }
80
+ const innerHTML = `
81
+ <div class="info-container">
82
+ <div class="info-container-left">
83
+ <table class="info-table">
84
+ <tr>
85
+ <td class="info-table-key">Name</td>
86
+ <td class="info-table-value" id="info-table-pathname">${fmtPath.split('/').pop()}</td>
87
+ </tr>
88
+ <tr>
89
+ <td class="info-table-key">Size</td>
90
+ <td class="info-table-value" id="info-table-pathsize">N/A</td>
91
+ </tr>
92
+ <tr>
93
+ <td class="info-table-key">Access-Time</td>
94
+ <td class="info-table-value" id="info-table-accesstime">1970-01-01 00:00:00</td>
95
+ </tr>
96
+ <tr>
97
+ <td class="info-table-key">Create-Time</td>
98
+ <td class="info-table-value" id="info-table-createtime">1970-01-01 00:00:00</td>
99
+ </td>
100
+ </table>
101
+ </div>
102
+ <div class="info-container-right">
103
+ <div class="info-path-copy">
104
+ <input type="text" value="${window.location.origin}/${ensureSlashEnd(r.url)}" readonly>
105
+ <button class="copy-button" id='copy-btn-full-path'>📋</button>
106
+ </div>
107
+ <div class="info-path-copy">
108
+ <input type="text" value="${ensureSlashEnd(r.url)}" readonly>
109
+ <button class="copy-button" id='copy-btn-rel-path'>📋</button>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ `
114
+ const [win, closeWin] = createFloatingWindow(innerHTML, {title: 'File Info'});
115
+ document.getElementById('copy-btn-full-path').onclick = () => {
116
+ copyToClipboard(window.location.origin + '/' + ensureSlashEnd(r.url));
117
+ showPopup('Path copied to clipboard', {timeout: 2000, level: 'success'});
118
+ }
119
+ document.getElementById('copy-btn-rel-path').onclick = () => {
120
+ copyToClipboard(ensureSlashEnd(r.url));
121
+ showPopup('Path copied to clipboard', {timeout: 2000, level: 'success'});
122
+ }
123
+
124
+ const sizeValTd = document.querySelector('.info-table-value#info-table-pathsize');
125
+ const createTimeValTd = document.querySelector('.info-table-value#info-table-createtime');
126
+ const accessTimeValTd = document.querySelector('.info-table-value#info-table-accesstime');
127
+ // console.log(sizeValTd, createTimeValTd, accessTimeValTd)
128
+ c.getMetadata(ensureSlashEnd(r.url)).then((meta) => {
129
+ if (!meta) {
130
+ console.error('Failed to fetch metadata for: ' + r.url);
131
+ return;
132
+ }
133
+ sizeValTd.textContent = formatSize(meta.size);
134
+ createTimeValTd.textContent = cvtGMT2Local(meta.create_time);
135
+ accessTimeValTd.textContent = cvtGMT2Local(meta.access_time);
136
+ });
137
+ }
frontend/popup.css CHANGED
@@ -9,6 +9,19 @@ div.floating-window.blocker{
9
9
  z-index: 100;
10
10
  }
11
11
 
12
+ @keyframes fade-in{
13
+ from{
14
+ opacity: 0;
15
+ transform: translate(-50%, calc(-50% + 0.25rem));
16
+ }
17
+ to{
18
+ opacity: 1;
19
+ transform: translate(-50%, -50%);
20
+ }
21
+ }
22
+ div.floating-window.window{
23
+ animation: fade-in 0.1s ease-in;
24
+ }
12
25
  div.floating-window.window{
13
26
  position: fixed;
14
27
  top: 50%;
@@ -39,6 +52,7 @@ div.popup-window{
39
52
  display: block;
40
53
  text-align: left;
41
54
  animation: popup-appear 0.5s ease;
55
+ z-index: 102;
42
56
  }
43
57
 
44
58
  @keyframes popup-appear{
frontend/scripts.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import Connector from './api.js';
2
2
  import { permMap } from './api.js';
3
3
  import { showFloatingWindowLineInput, showPopup } from './popup.js';
4
- import { formatSize, decodePathURI, ensurePathURI, copyToClipboard, getRandomString, cvtGMT2Local, debounce, encodePathURI } from './utils.js';
4
+ import { formatSize, decodePathURI, ensurePathURI, getRandomString, cvtGMT2Local, debounce, encodePathURI } from './utils.js';
5
+ import { showInfoPanel, showDirInfoPanel } from './info.js';
5
6
 
6
7
  const conn = new Connector();
7
8
  let userRecord = null;
@@ -235,7 +236,6 @@ function refreshFileList(){
235
236
  const tr = document.createElement('tr');
236
237
  const sizeTd = document.createElement('td');
237
238
  const accessTimeTd = document.createElement('td');
238
- const createTimeTd = document.createElement('td');
239
239
  {
240
240
  const nameTd = document.createElement('td');
241
241
  if (dir.url.endsWith('/')){
@@ -262,8 +262,6 @@ function refreshFileList(){
262
262
  tr.appendChild(sizeTd);
263
263
  accessTimeTd.textContent = cvtGMT2Local(dir.access_time);
264
264
  tr.appendChild(accessTimeTd);
265
- createTimeTd.textContent = cvtGMT2Local(dir.create_time);
266
- tr.appendChild(createTimeTd);
267
265
  }
268
266
  {
269
267
  const accessTd = document.createElement('td');
@@ -275,21 +273,16 @@ function refreshFileList(){
275
273
  const actContainer = document.createElement('div');
276
274
  actContainer.classList.add('action-container');
277
275
 
278
- const showMetaButton = document.createElement('a');
279
- showMetaButton.textContent = 'Reveal';
280
- showMetaButton.style.cursor = 'pointer';
281
- showMetaButton.addEventListener('click', () => {
282
- const dirUrlEncap = dirurl;
283
- conn.getMetadata(dirUrlEncap).then(
284
- (meta) => {
285
- sizeTd.textContent = formatSize(meta.size);
286
- accessTimeTd.textContent = cvtGMT2Local(meta.access_time);
287
- createTimeTd.textContent = cvtGMT2Local(meta.create_time);
288
- }
289
- );
290
- showPopup('Fetching metadata...', {level: 'info', timeout: 3000});
276
+ const infoButton = document.createElement('a');
277
+ infoButton.style.cursor = 'pointer';
278
+ infoButton.textContent = 'Details';
279
+ infoButton.style.width = '100%';
280
+ infoButton.style.display = 'block';
281
+ infoButton.style.textAlign = 'center';
282
+ infoButton.addEventListener('click', () => {
283
+ showDirInfoPanel(dir, userRecord, conn);
291
284
  });
292
- actContainer.appendChild(showMetaButton);
285
+ actContainer.appendChild(infoButton);
293
286
 
294
287
  const moveButton = document.createElement('a');
295
288
  moveButton.textContent = 'Move';
@@ -369,13 +362,6 @@ function refreshFileList(){
369
362
  tr.appendChild(dateTd);
370
363
  }
371
364
 
372
- {
373
- const dateTd = document.createElement('td');
374
- const createTime = file.create_time;
375
- dateTd.textContent = cvtGMT2Local(createTime);
376
- tr.appendChild(dateTd);
377
- }
378
-
379
365
  {
380
366
  const accessTd = document.createElement('td');
381
367
  if (file.owner_id === userRecord.id || userRecord.is_admin){
@@ -416,14 +402,13 @@ function refreshFileList(){
416
402
  const actContainer = document.createElement('div');
417
403
  actContainer.classList.add('action-container');
418
404
 
419
- const copyButton = document.createElement('a');
420
- copyButton.style.cursor = 'pointer';
421
- copyButton.textContent = 'Share';
422
- copyButton.addEventListener('click', () => {
423
- copyToClipboard(conn.config.endpoint + '/' + file.url);
424
- showPopup('Link copied to clipboard', {level: "success"});
405
+ const infoButton = document.createElement('a');
406
+ infoButton.style.cursor = 'pointer';
407
+ infoButton.textContent = 'Details';
408
+ infoButton.addEventListener('click', () => {
409
+ showInfoPanel(file, userRecord);
425
410
  });
426
- actContainer.appendChild(copyButton);
411
+ actContainer.appendChild(infoButton);
427
412
 
428
413
  const viewButton = document.createElement('a');
429
414
  viewButton.textContent = 'View';
frontend/styles.css CHANGED
@@ -1,4 +1,5 @@
1
1
  @import "./popup.css";
2
+ @import "./info.css";
2
3
 
3
4
  body{
4
5
  font-family: Arial, sans-serif;
@@ -173,10 +174,10 @@ table#files tr:hover {
173
174
  background-color: #eaeaea;
174
175
  transition: all 0.2s;
175
176
  }
176
- table#files tr td:nth-child(2), table#files tr td:nth-child(5){
177
+ table#files tr td:nth-child(2), table#files tr td:nth-child(4){
177
178
  width: 1%;
178
179
  }
179
- table#files tr td:nth-child(3), table#files tr td:nth-child(4), table#files tr td:nth-child(6){
180
+ table#files tr td:nth-child(3), table#files tr td:nth-child(5){
180
181
  width: 12%;
181
182
  }
182
183
 
@@ -218,4 +219,14 @@ a{
218
219
  .delete-btn:hover{
219
220
  color: white !important;
220
221
  background-color: #990511c7 !important;
222
+ }
223
+
224
+ button{
225
+ transition: all 0.2s;
226
+ }
227
+ button:hover {
228
+ transform: scale(1.05);
229
+ }
230
+ button:active {
231
+ transform: scale(0.95);
221
232
  }
lfss/cli/balance.py CHANGED
@@ -3,10 +3,11 @@ Balance the storage by ensuring that large file thresholds are met.
3
3
  """
4
4
 
5
5
  from lfss.src.config import LARGE_BLOB_DIR, LARGE_FILE_BYTES
6
- import argparse, time
6
+ import argparse, time, itertools
7
7
  from functools import wraps
8
8
  from asyncio import Semaphore
9
9
  import aiofiles, asyncio
10
+ from contextlib import contextmanager
10
11
  from lfss.src.database import transaction, unique_cursor
11
12
  from lfss.src.connection_pool import global_entrance
12
13
 
@@ -30,7 +31,6 @@ async def move_to_external(f_id: str, flag: str = ''):
30
31
  if blob_row is None:
31
32
  print(f"{flag}File {f_id} not found in blobs.fdata")
32
33
  return
33
- await c.execute("BEGIN")
34
34
  blob: bytes = blob_row[0]
35
35
  async with aiofiles.open(LARGE_BLOB_DIR / f_id, 'wb') as f:
36
36
  await f.write(blob)
@@ -59,8 +59,7 @@ async def _main(batch_size: int = 10000):
59
59
  start_time = time.time()
60
60
 
61
61
  e_cout = 0
62
- batch_count = 0
63
- while True:
62
+ for batch_count in itertools.count(start=0):
64
63
  async with unique_cursor() as conn:
65
64
  exceeded_rows = list(await (await conn.execute(
66
65
  "SELECT file_id FROM fmeta WHERE file_size > ? AND external = 0 LIMIT ? OFFSET ?",
@@ -76,8 +75,7 @@ async def _main(batch_size: int = 10000):
76
75
  await asyncio.gather(*tasks)
77
76
 
78
77
  i_count = 0
79
- batch_count = 0
80
- while True:
78
+ for batch_count in itertools.count(start=0):
81
79
  async with unique_cursor() as conn:
82
80
  under_rows = list(await (await conn.execute(
83
81
  "SELECT file_id, file_size, external FROM fmeta WHERE file_size <= ? AND external = 1 LIMIT ? OFFSET ?",
@@ -95,15 +93,35 @@ async def _main(batch_size: int = 10000):
95
93
  end_time = time.time()
96
94
  print(f"Balancing complete, took {end_time - start_time:.2f} seconds. "
97
95
  f"{e_cout} files moved to external storage, {i_count} files moved to internal storage.")
96
+
97
+ @global_entrance()
98
+ async def vacuum(index: bool = False, blobs: bool = False):
99
+ @contextmanager
100
+ def indicator(name: str):
101
+ print(f"\033[1;33mRunning {name}... \033[0m")
102
+ s = time.time()
103
+ yield
104
+ print(f"{name} took {time.time() - s:.2f} seconds")
105
+
106
+ async with unique_cursor(is_write=True) as c:
107
+ if index:
108
+ with indicator("VACUUM-index"):
109
+ await c.execute("VACUUM main")
110
+ if blobs:
111
+ with indicator("VACUUM-blobs"):
112
+ await c.execute("VACUUM blobs")
98
113
 
99
114
  def main():
100
115
  global sem
101
116
  parser = argparse.ArgumentParser(description="Balance the storage by ensuring that large file thresholds are met.")
102
117
  parser.add_argument("-j", "--jobs", type=int, default=2, help="Number of concurrent jobs")
103
118
  parser.add_argument("-b", "--batch-size", type=int, default=10000, help="Batch size for processing files")
119
+ parser.add_argument("--vacuum", action="store_true", help="Run VACUUM only on index.db after balancing")
120
+ parser.add_argument("--vacuum-all", action="store_true", help="Run VACUUM on both index.db and blobs.db after balancing")
104
121
  args = parser.parse_args()
105
122
  sem = Semaphore(args.jobs)
106
123
  asyncio.run(_main(args.batch_size))
124
+ asyncio.run(vacuum(index=args.vacuum or args.vacuum_all, blobs=args.vacuum_all))
107
125
 
108
126
  if __name__ == '__main__':
109
127
  main()
lfss/cli/user.py CHANGED
@@ -1,20 +1,10 @@
1
1
  import argparse, asyncio
2
2
  from contextlib import asynccontextmanager
3
3
  from .cli import parse_permission, FileReadPermission
4
+ from ..src.utils import parse_storage_size
4
5
  from ..src.database import Database, FileReadPermission, transaction, UserConn
5
6
  from ..src.connection_pool import global_entrance
6
7
 
7
- def parse_storage_size(s: str) -> int:
8
- if s[-1] in 'Kk':
9
- return int(s[:-1]) * 1024
10
- if s[-1] in 'Mm':
11
- return int(s[:-1]) * 1024 * 1024
12
- if s[-1] in 'Gg':
13
- return int(s[:-1]) * 1024 * 1024 * 1024
14
- if s[-1] in 'Tt':
15
- return int(s[:-1]) * 1024 * 1024 * 1024 * 1024
16
- return int(s)
17
-
18
8
  @global_entrance(1)
19
9
  async def _main():
20
10
  parser = argparse.ArgumentParser()
lfss/src/config.py CHANGED
@@ -1,5 +1,6 @@
1
- from pathlib import Path
2
1
  import os
2
+ from pathlib import Path
3
+ from .utils import parse_storage_size
3
4
 
4
5
  __default_dir = '.storage_data'
5
6
 
@@ -12,6 +13,10 @@ LARGE_BLOB_DIR = DATA_HOME / 'large_blobs'
12
13
  LARGE_BLOB_DIR.mkdir(exist_ok=True)
13
14
 
14
15
  # https://sqlite.org/fasterthanfs.html
15
- LARGE_FILE_BYTES = 8 * 1024 * 1024 # 8MB
16
+ __env_large_file = os.environ.get('LFSS_LARGE_FILE', None)
17
+ if __env_large_file is not None:
18
+ LARGE_FILE_BYTES = parse_storage_size(__env_large_file)
19
+ else:
20
+ LARGE_FILE_BYTES = 8 * 1024 * 1024 # 8MB
16
21
  MAX_FILE_BYTES = 512 * 1024 * 1024 # 512MB
17
22
  MAX_BUNDLE_BYTES = 512 * 1024 * 1024 # 512MB
@@ -5,6 +5,7 @@ from contextlib import asynccontextmanager
5
5
  from dataclasses import dataclass
6
6
  from asyncio import Semaphore, Lock
7
7
  from functools import wraps
8
+ from typing import Callable, Awaitable
8
9
 
9
10
  from .log import get_logger
10
11
  from .config import DATA_HOME
@@ -125,7 +126,7 @@ async def global_connection(n_read: int = 1):
125
126
  await global_connection_close()
126
127
 
127
128
  def global_entrance(n_read: int = 1):
128
- def decorator(func):
129
+ def decorator(func: Callable[..., Awaitable]):
129
130
  @wraps(func)
130
131
  async def wrapper(*args, **kwargs):
131
132
  async with global_connection(n_read):
lfss/src/database.py CHANGED
@@ -13,7 +13,7 @@ from .connection_pool import execute_sql, unique_cursor, transaction
13
13
  from .datatype import UserRecord, FileReadPermission, FileRecord, DirectoryRecord, PathContents
14
14
  from .config import LARGE_BLOB_DIR
15
15
  from .log import get_logger
16
- from .utils import decode_uri_compnents, hash_credential
16
+ from .utils import decode_uri_compnents, hash_credential, concurrent_wrap
17
17
  from .error import *
18
18
 
19
19
  class DBObjectBase(ABC):
@@ -661,6 +661,7 @@ class Database:
661
661
  continue
662
662
  yield r, blob
663
663
 
664
+ @concurrent_wrap()
664
665
  async def zip_path(self, top_url: str, urls: Optional[list[str]]) -> io.BytesIO:
665
666
  if top_url.startswith('/'):
666
667
  top_url = top_url[1:]
lfss/src/utils.py CHANGED
@@ -3,6 +3,10 @@ import urllib.parse
3
3
  import asyncio
4
4
  import functools
5
5
  import hashlib
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from typing import TypeVar, Callable, Awaitable
8
+ from functools import wraps, partial
9
+ import os
6
10
 
7
11
  def hash_credential(username: str, password: str):
8
12
  return hashlib.sha256((username + password).encode()).hexdigest()
@@ -56,4 +60,45 @@ def now_stamp() -> float:
56
60
  return datetime.datetime.now().timestamp()
57
61
 
58
62
  def stamp_to_str(stamp: float) -> str:
59
- return datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d %H:%M:%S')
63
+ return datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d %H:%M:%S')
64
+
65
+ def parse_storage_size(s: str) -> int:
66
+ """ Parse the file size string to bytes """
67
+ if s[-1].isdigit():
68
+ return int(s)
69
+ unit = s[-1].lower()
70
+ match unit:
71
+ case 'b': return int(s[:-1])
72
+ case 'k': return int(s[:-1]) * 1024
73
+ case 'm': return int(s[:-1]) * 1024**2
74
+ case 'g': return int(s[:-1]) * 1024**3
75
+ case 't': return int(s[:-1]) * 1024**4
76
+ case _: raise ValueError(f"Invalid file size string: {s}")
77
+
78
+ _FnReturnT = TypeVar('_FnReturnT')
79
+ _AsyncReturnT = Awaitable[_FnReturnT]
80
+ _g_executor = None
81
+ def get_global_executor():
82
+ global _g_executor
83
+ if _g_executor is None:
84
+ _g_executor = ThreadPoolExecutor(max_workers=4 if (cpu_count:=os.cpu_count()) and cpu_count > 4 else cpu_count)
85
+ return _g_executor
86
+ def async_wrap(executor=None):
87
+ if executor is None:
88
+ executor = get_global_executor()
89
+ def _async_wrap(func: Callable[..., _FnReturnT]) -> Callable[..., Awaitable[_FnReturnT]]:
90
+ @wraps(func)
91
+ async def run(*args, **kwargs):
92
+ loop = asyncio.get_event_loop()
93
+ pfunc = partial(func, *args, **kwargs)
94
+ return await loop.run_in_executor(executor, pfunc)
95
+ return run
96
+ return _async_wrap
97
+ def concurrent_wrap(executor=None):
98
+ def _concurrent_wrap(func: Callable[..., _AsyncReturnT]) -> Callable[..., _AsyncReturnT]:
99
+ @async_wrap(executor)
100
+ def sync_fn(*args, **kwargs):
101
+ loop = asyncio.new_event_loop()
102
+ return loop.run_until_complete(func(*args, **kwargs))
103
+ return sync_fn
104
+ return _concurrent_wrap
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.7.6
3
+ Version: 0.7.8
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -1,33 +1,35 @@
1
1
  Readme.md,sha256=vsPotlwPAaHI5plh4aaszpi3rr7ZGDn7-wLdEYTWQ0k,1275
2
2
  docs/Known_issues.md,sha256=rfdG3j1OJF-59S9E06VPyn0nZKbW-ybPxkoZ7MEZWp8,81
3
3
  docs/Permission.md,sha256=X0VNfBKU52f93QYqcVyiBFJ3yURiSkhIo9S_5fdSgzM,2265
4
- frontend/api.js,sha256=lHqT7zGmsUZItE-FRR0LfTl_WYCcqlNssfa00XYo-EY,7865
5
- frontend/index.html,sha256=VPJDs2LG8ep9kjlsKzjWzpN9vc1VGgdvOUlNTZWyQoQ,2088
6
- frontend/popup.css,sha256=VzkjG1ZTLxhHMtTyobnlvqYmVsTmdbJJed2Pu1cc06c,1007
4
+ frontend/api.js,sha256=DxWmqO0AAOsWLXYtbgAzEnSmVyEJyzcxXSCH7H3STUk,7925
5
+ frontend/index.html,sha256=Mem8de9vwmZoe4x1DKqpu_aFgIBURqT3mIGdeOOTbIs,2051
6
+ frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
7
+ frontend/info.js,sha256=mXCQnRESSx7iiR1FW63sAXHnm0NZ3REeaRmnjZNeQbU,5454
8
+ frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
7
9
  frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
8
- frontend/scripts.js,sha256=hQ8m3L7P-LplLqrPUWD6pBo4C_tCUl2XZKRNtkWBy8I,21155
9
- frontend/styles.css,sha256=wly8O-zF4EUgV12Tv1bATSfmJsLITv2u3_SiyXVaxv4,4096
10
+ frontend/scripts.js,sha256=OP99BSbnyTE1LJebGVUvV3WUnDBiZdqaC3a9SE1FF6U,20286
11
+ frontend/styles.css,sha256=37aU9Iep_hTz3LnAAAcEhC_I7AC0A4lX6apnMuGPTlA,4214
10
12
  frontend/utils.js,sha256=Ts4nlef8pkrEgpwX-uQwAhWvwxlIzex8ijDLNCa22ps,2372
11
- lfss/cli/balance.py,sha256=heOgwH6oNnfYsKJfA4VxWKdEXPstdVbbRXWxcDqLIS0,4176
13
+ lfss/cli/balance.py,sha256=X6Q6e7sb6LMlCXG4qSD2MUMlixV2Kc9EXPluIe2S5DA,5090
12
14
  lfss/cli/cli.py,sha256=LH1nx5wI1K2DZ3hvHz7oq5HcXVDoW2V6sr7q9gJ8gqo,4621
13
15
  lfss/cli/panel.py,sha256=iGdVmdWYjA_7a78ZzWEB_3ggIOBeUKTzg6F5zLaB25c,1401
14
16
  lfss/cli/serve.py,sha256=bO3GT0kuylMGN-7bZWP4e71MlugGZ_lEMkYaYld_Ntg,985
15
- lfss/cli/user.py,sha256=-ePmx_jhqfPQDl_9i_C_St9ujlCyyWSqNJUvc26v4_4,3686
17
+ lfss/cli/user.py,sha256=ETLtj0N-kmxv0mhmeAsO6cY7kPq7nOOP4DetxIRoQpQ,3405
16
18
  lfss/client/__init__.py,sha256=8uvcKs3PYQamDd_cjfN-fX9QUohEzJqeJlOYkBlzC3M,4556
17
19
  lfss/client/api.py,sha256=kSkB4wADTu012-1wl6v90OiZrw6aTQ42GU4jtV4KO0k,5764
18
20
  lfss/sql/init.sql,sha256=C-JtQAlaOjESI8uoF1Y_9dKukEVSw5Ll-7yA3gG-XHU,1210
19
21
  lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
20
22
  lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- lfss/src/config.py,sha256=aTfjWORE9Mx7LSEjbfmHnULlrmIWEvEBSZ4fJKWZNjM,530
22
- lfss/src/connection_pool.py,sha256=teW_4DMiwlCN_bS7AhjkbY9cHZqUFlmHE_J2yPjHVsA,5125
23
- lfss/src/database.py,sha256=G9U_Iijp7euuGj3fcWdSGJPetMhn56X0vI8iWr6ZUr8,31904
23
+ lfss/src/config.py,sha256=CIbVFWRu86dl2GVlXlCDv93W8PLwT89NtznU6TCKvtk,729
24
+ lfss/src/connection_pool.py,sha256=r4Ho5d_Gd4S_KbT7515UJoiyfIgS6xyttqMsKqOfaIg,5190
25
+ lfss/src/database.py,sha256=-itbpGb7cQrywZLFk4aNcuy38Krsyemtyiz8GIt4i7M,31944
24
26
  lfss/src/datatype.py,sha256=BLS7vuuKnFZQg0nrKeP9SymqUhcN6HwPgejU0yBd_Ak,1622
25
27
  lfss/src/error.py,sha256=imbhwnbhnI3HLhkbfICROe3F0gleKrOk4XnqHJDOtuI,285
26
28
  lfss/src/log.py,sha256=xOnkuH-gB_jSVGqNnDVEW05iki6SCJ2xdEhjz5eEsMo,5136
27
29
  lfss/src/server.py,sha256=EA5fK4qc98tF8qoS9F6VaxIE65D5X8Ztkjqy8EUYIv8,16276
28
30
  lfss/src/stat.py,sha256=hTMtQyM_Ukmhc33Bb9FGCfBMIX02KrGHQg8nL7sC8sU,2082
29
- lfss/src/utils.py,sha256=miGsv7udupDtSpVSd66IvUt0_QMi3JXYCp_BjdPJY-M,2134
30
- lfss-0.7.6.dist-info/METADATA,sha256=QAv2WFiNKWUy8Dl42v0Of0B7uCkiyyoYwZ2EiVxq-ZM,1967
31
- lfss-0.7.6.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
- lfss-0.7.6.dist-info/entry_points.txt,sha256=d_Ri3GXxUW-S0E6q953A8od0YMmUAnZGlJSKS46OiW8,172
33
- lfss-0.7.6.dist-info/RECORD,,
31
+ lfss/src/utils.py,sha256=S9LCJ5OkNk_zM4rZnrHg1UDjnNkDVO_ejmfsBeNJs4s,3868
32
+ lfss-0.7.8.dist-info/METADATA,sha256=JwNtzTpXrk_e4ydOV286AkNBUANiIMlDxPiptRAoHH4,1967
33
+ lfss-0.7.8.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
34
+ lfss-0.7.8.dist-info/entry_points.txt,sha256=d_Ri3GXxUW-S0E6q953A8od0YMmUAnZGlJSKS46OiW8,172
35
+ lfss-0.7.8.dist-info/RECORD,,
File without changes