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 +1 -0
- frontend/index.html +1 -2
- frontend/info.css +36 -0
- frontend/info.js +137 -0
- frontend/popup.css +14 -0
- frontend/scripts.js +17 -32
- frontend/styles.css +13 -2
- lfss/cli/balance.py +24 -6
- lfss/cli/user.py +1 -11
- lfss/src/config.py +7 -2
- lfss/src/connection_pool.py +2 -1
- lfss/src/database.py +2 -1
- lfss/src/utils.py +46 -1
- {lfss-0.7.6.dist-info → lfss-0.7.8.dist-info}/METADATA +1 -1
- {lfss-0.7.6.dist-info → lfss-0.7.8.dist-info}/RECORD +17 -15
- {lfss-0.7.6.dist-info → lfss-0.7.8.dist-info}/WHEEL +0 -0
- {lfss-0.7.6.dist-info → lfss-0.7.8.dist-info}/entry_points.txt +0 -0
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
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,
|
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
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
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(
|
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
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
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(
|
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(
|
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(
|
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 =
|
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 =
|
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
|
-
|
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
|
lfss/src/connection_pool.py
CHANGED
@@ -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,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=
|
5
|
-
frontend/index.html,sha256=
|
6
|
-
frontend/
|
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=
|
9
|
-
frontend/styles.css,sha256=
|
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=
|
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
|
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=
|
22
|
-
lfss/src/connection_pool.py,sha256=
|
23
|
-
lfss/src/database.py,sha256
|
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=
|
30
|
-
lfss-0.7.
|
31
|
-
lfss-0.7.
|
32
|
-
lfss-0.7.
|
33
|
-
lfss-0.7.
|
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
|
File without changes
|