lfss 0.7.6__tar.gz → 0.7.7__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.6 → lfss-0.7.7}/PKG-INFO +1 -1
  2. {lfss-0.7.6 → lfss-0.7.7}/frontend/api.js +1 -0
  3. {lfss-0.7.6 → lfss-0.7.7}/frontend/index.html +1 -2
  4. lfss-0.7.7/frontend/info.css +36 -0
  5. lfss-0.7.7/frontend/info.js +137 -0
  6. {lfss-0.7.6 → lfss-0.7.7}/frontend/popup.css +1 -0
  7. {lfss-0.7.6 → lfss-0.7.7}/frontend/scripts.js +15 -32
  8. {lfss-0.7.6 → lfss-0.7.7}/frontend/styles.css +3 -2
  9. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/database.py +2 -1
  10. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/utils.py +34 -1
  11. {lfss-0.7.6 → lfss-0.7.7}/pyproject.toml +1 -1
  12. {lfss-0.7.6 → lfss-0.7.7}/Readme.md +0 -0
  13. {lfss-0.7.6 → lfss-0.7.7}/docs/Known_issues.md +0 -0
  14. {lfss-0.7.6 → lfss-0.7.7}/docs/Permission.md +0 -0
  15. {lfss-0.7.6 → lfss-0.7.7}/frontend/popup.js +0 -0
  16. {lfss-0.7.6 → lfss-0.7.7}/frontend/utils.js +0 -0
  17. {lfss-0.7.6 → lfss-0.7.7}/lfss/cli/balance.py +0 -0
  18. {lfss-0.7.6 → lfss-0.7.7}/lfss/cli/cli.py +0 -0
  19. {lfss-0.7.6 → lfss-0.7.7}/lfss/cli/panel.py +0 -0
  20. {lfss-0.7.6 → lfss-0.7.7}/lfss/cli/serve.py +0 -0
  21. {lfss-0.7.6 → lfss-0.7.7}/lfss/cli/user.py +0 -0
  22. {lfss-0.7.6 → lfss-0.7.7}/lfss/client/__init__.py +0 -0
  23. {lfss-0.7.6 → lfss-0.7.7}/lfss/client/api.py +0 -0
  24. {lfss-0.7.6 → lfss-0.7.7}/lfss/sql/init.sql +0 -0
  25. {lfss-0.7.6 → lfss-0.7.7}/lfss/sql/pragma.sql +0 -0
  26. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/__init__.py +0 -0
  27. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/config.py +0 -0
  28. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/connection_pool.py +0 -0
  29. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/datatype.py +0 -0
  30. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/error.py +0 -0
  31. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/log.py +0 -0
  32. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/server.py +0 -0
  33. {lfss-0.7.6 → lfss-0.7.7}/lfss/src/stat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.7.6
3
+ Version: 0.7.7
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -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
@@ -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>
@@ -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
+ }
@@ -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">Filename</td>
23
+ <td class="info-table-value">${decodePathURI(r.url).split('/').pop()}</td>
24
+ </tr>
25
+ <tr>
26
+ <td class="info-table-key">File-Type</td>
27
+ <td class="info-table-value">${r.mime_type}</td>
28
+ </tr>
29
+ <tr>
30
+ <td class="info-table-key">Size</td>
31
+ <td class="info-table-value">${formatSize(r.file_size)}</td>
32
+ </tr>
33
+ <tr>
34
+ <td class="info-table-key">Owner</td>
35
+ <td class="info-table-value">${r.owner_id}</td>
36
+ </tr>
37
+ <tr>
38
+ <td class="info-table-key">Create-Time</td>
39
+ <td class="info-table-value">${cvtGMT2Local(r.create_time)}</td>
40
+ </td>
41
+ <tr>
42
+ <td class="info-table-key">Access-Time</td>
43
+ <td class="info-table-value">${cvtGMT2Local(r.access_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">Pathname</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
+ }
@@ -39,6 +39,7 @@ div.popup-window{
39
39
  display: block;
40
40
  text-align: left;
41
41
  animation: popup-appear 0.5s ease;
42
+ z-index: 102;
42
43
  }
43
44
 
44
45
  @keyframes popup-appear{
@@ -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,14 @@ 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.addEventListener('click', () => {
281
+ showDirInfoPanel(dir, userRecord, conn);
291
282
  });
292
- actContainer.appendChild(showMetaButton);
283
+ actContainer.appendChild(infoButton);
293
284
 
294
285
  const moveButton = document.createElement('a');
295
286
  moveButton.textContent = 'Move';
@@ -369,13 +360,6 @@ function refreshFileList(){
369
360
  tr.appendChild(dateTd);
370
361
  }
371
362
 
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
363
  {
380
364
  const accessTd = document.createElement('td');
381
365
  if (file.owner_id === userRecord.id || userRecord.is_admin){
@@ -416,14 +400,13 @@ function refreshFileList(){
416
400
  const actContainer = document.createElement('div');
417
401
  actContainer.classList.add('action-container');
418
402
 
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"});
403
+ const infoButton = document.createElement('a');
404
+ infoButton.style.cursor = 'pointer';
405
+ infoButton.textContent = 'Details';
406
+ infoButton.addEventListener('click', () => {
407
+ showInfoPanel(file, userRecord);
425
408
  });
426
- actContainer.appendChild(copyButton);
409
+ actContainer.appendChild(infoButton);
427
410
 
428
411
  const viewButton = document.createElement('a');
429
412
  viewButton.textContent = 'View';
@@ -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
 
@@ -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:]
@@ -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,33 @@ 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
+
66
+ _FnReturnT = TypeVar('_FnReturnT')
67
+ _AsyncReturnT = Awaitable[_FnReturnT]
68
+ _g_executor = None
69
+ def get_global_executor():
70
+ global _g_executor
71
+ if _g_executor is None:
72
+ _g_executor = ThreadPoolExecutor(max_workers=4 if (cpu_count:=os.cpu_count()) and cpu_count > 4 else cpu_count)
73
+ return _g_executor
74
+ def async_wrap(executor=None):
75
+ if executor is None:
76
+ executor = get_global_executor()
77
+ def _async_wrap(func: Callable[..., _FnReturnT]) -> Callable[..., Awaitable[_FnReturnT]]:
78
+ @wraps(func)
79
+ async def run(*args, **kwargs):
80
+ loop = asyncio.get_event_loop()
81
+ pfunc = partial(func, *args, **kwargs)
82
+ return await loop.run_in_executor(executor, pfunc)
83
+ return run
84
+ return _async_wrap
85
+ def concurrent_wrap(executor=None):
86
+ def _concurrent_wrap(func: Callable[..., _AsyncReturnT]) -> Callable[..., _AsyncReturnT]:
87
+ @async_wrap(executor)
88
+ def sync_fn(*args, **kwargs):
89
+ loop = asyncio.new_event_loop()
90
+ return loop.run_until_complete(func(*args, **kwargs))
91
+ return sync_fn
92
+ return _concurrent_wrap
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "lfss"
3
- version = "0.7.6"
3
+ version = "0.7.7"
4
4
  description = "Lightweight file storage service"
5
5
  authors = ["li, mengxun <limengxun45@outlook.com>"]
6
6
  readme = "Readme.md"
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
File without changes