lfss 0.7.14__tar.gz → 0.7.15__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.
- {lfss-0.7.14 → lfss-0.7.15}/PKG-INFO +1 -1
- {lfss-0.7.14 → lfss-0.7.15}/frontend/scripts.js +31 -46
- lfss-0.7.15/frontend/state.js +57 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/thumb.js +35 -24
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/thumb.py +17 -12
- {lfss-0.7.14 → lfss-0.7.15}/pyproject.toml +1 -1
- {lfss-0.7.14 → lfss-0.7.15}/Readme.md +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/docs/Known_issues.md +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/docs/Permission.md +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/api.js +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/index.html +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/info.css +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/info.js +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/popup.css +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/popup.js +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/styles.css +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/thumb.css +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/frontend/utils.js +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/cli/balance.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/cli/cli.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/cli/panel.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/cli/serve.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/cli/user.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/cli/vacuum.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/client/__init__.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/client/api.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/sql/init.sql +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/sql/pragma.sql +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/__init__.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/bounded_pool.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/config.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/connection_pool.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/database.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/datatype.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/error.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/log.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/server.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/stat.py +0 -0
- {lfss-0.7.14 → lfss-0.7.15}/lfss/src/utils.py +0 -0
@@ -1,12 +1,13 @@
|
|
1
|
-
import Connector from './api.js';
|
2
1
|
import { permMap } from './api.js';
|
3
2
|
import { showFloatingWindowLineInput, showPopup } from './popup.js';
|
4
3
|
import { formatSize, decodePathURI, ensurePathURI, getRandomString, cvtGMT2Local, debounce, encodePathURI, asHtmlText } from './utils.js';
|
5
4
|
import { showInfoPanel, showDirInfoPanel } from './info.js';
|
6
5
|
import { makeThumbHtml } from './thumb.js';
|
6
|
+
import { store } from './state.js';
|
7
7
|
|
8
|
-
|
8
|
+
/** @type {import('./api.js').UserRecord}*/
|
9
9
|
let userRecord = null;
|
10
|
+
|
10
11
|
const ensureSlashEnd = (path) => {
|
11
12
|
return path.endsWith('/') ? path : path + '/';
|
12
13
|
}
|
@@ -26,25 +27,16 @@ const randomizeFnameButton = document.querySelector('#randomize-fname-btn');
|
|
26
27
|
const sortBySelect = document.querySelector('#sort-by-sel');
|
27
28
|
const sortOrderSelect = document.querySelector('#sort-order-sel');
|
28
29
|
|
29
|
-
conn
|
30
|
-
|
30
|
+
const conn = store.conn;
|
31
|
+
store.init();
|
31
32
|
|
32
33
|
{
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
conn.config.endpoint = endpoint;
|
37
|
-
}
|
38
|
-
const token = window.localStorage.getItem('token');
|
39
|
-
if (token){
|
40
|
-
tokenInput.value = token;
|
41
|
-
conn.config.token = token;
|
42
|
-
}
|
43
|
-
const path = window.localStorage.getItem('path');
|
44
|
-
if (path){
|
45
|
-
pathInput.value = path;
|
46
|
-
}
|
34
|
+
tokenInput.value = store.token;
|
35
|
+
endpointInput.value = store.endpoint;
|
36
|
+
pathInput.value = store.dirpath;
|
47
37
|
uploadFilePrefixLabel.textContent = pathInput.value;
|
38
|
+
sortBySelect.value = store.sortby;
|
39
|
+
sortOrderSelect.value = store.sortorder;
|
48
40
|
maybeRefreshUserRecord().then(
|
49
41
|
() => maybeRefreshFileList()
|
50
42
|
);
|
@@ -52,20 +44,18 @@ conn.config.token = tokenInput.value;
|
|
52
44
|
|
53
45
|
function onPathChange(){
|
54
46
|
uploadFilePrefixLabel.textContent = pathInput.value;
|
55
|
-
|
47
|
+
store.dirpath = pathInput.value;
|
56
48
|
maybeRefreshFileList();
|
57
49
|
}
|
58
50
|
|
59
51
|
endpointInput.addEventListener('blur', () => {
|
60
|
-
|
61
|
-
window.localStorage.setItem('endpoint', endpointInput.value);
|
52
|
+
store.endpoint = endpointInput.value;
|
62
53
|
maybeRefreshUserRecord().then(
|
63
54
|
() => maybeRefreshFileList()
|
64
55
|
);
|
65
56
|
});
|
66
57
|
tokenInput.addEventListener('blur', () => {
|
67
|
-
|
68
|
-
window.localStorage.setItem('token', tokenInput.value);
|
58
|
+
store.token = tokenInput.value;
|
69
59
|
maybeRefreshUserRecord().then(
|
70
60
|
() => maybeRefreshFileList()
|
71
61
|
);
|
@@ -94,7 +84,7 @@ function onFileNameInpuChange(){
|
|
94
84
|
uploadFileNameInput.classList.remove('duplicate');
|
95
85
|
}
|
96
86
|
else {
|
97
|
-
const p = ensurePathURI(
|
87
|
+
const p = ensurePathURI(store.dirpath + fileName);
|
98
88
|
conn.getMetadata(p).then(
|
99
89
|
(data) => {
|
100
90
|
console.log("Got file meta", data);
|
@@ -126,7 +116,7 @@ uploadFileSelector.addEventListener('change', () => {
|
|
126
116
|
});
|
127
117
|
uploadButton.addEventListener('click', () => {
|
128
118
|
const file = uploadFileSelector.files[0];
|
129
|
-
let path =
|
119
|
+
let path = store.dirpath;
|
130
120
|
let fileName = uploadFileNameInput.value;
|
131
121
|
if (fileName.length === 0){
|
132
122
|
throw new Error('File name cannot be empty');
|
@@ -172,7 +162,7 @@ uploadFileNameInput.addEventListener('input', debounce(onFileNameInpuChange, 500
|
|
172
162
|
uploadFileNameInput.focus();
|
173
163
|
}
|
174
164
|
else if (files.length > 1){
|
175
|
-
let dstPath =
|
165
|
+
let dstPath = store.dirpath + uploadFileNameInput.value;
|
176
166
|
if (!dstPath.endsWith('/')){ dstPath += '/'; }
|
177
167
|
if (!confirm(`
|
178
168
|
You are trying to upload multiple files at once.
|
@@ -216,52 +206,50 @@ Are you sure you want to proceed?
|
|
216
206
|
|
217
207
|
function maybeRefreshFileList(){
|
218
208
|
if (
|
219
|
-
|
209
|
+
store.dirpath && store.dirpath.length > 0 && store.dirpath.endsWith('/')
|
220
210
|
){
|
221
211
|
refreshFileList();
|
222
212
|
}
|
223
213
|
}
|
224
214
|
|
225
|
-
let sortBy = sortBySelect.value;
|
226
|
-
let sortOrder = sortOrderSelect.value;
|
227
215
|
/** @param {import('./api.js').DirectoryRecord} dirs */
|
228
216
|
function sortDirList(dirs){
|
229
|
-
if (
|
217
|
+
if (store.sortby === 'name'){
|
230
218
|
dirs.sort((a, b) => { return a.url.localeCompare(b.url); });
|
231
219
|
}
|
232
|
-
if (
|
220
|
+
if (store.sortorder === 'desc'){ dirs.reverse(); }
|
233
221
|
}
|
234
222
|
/** @param {import('./api.js').FileRecord} files */
|
235
223
|
function sortFileList(files){
|
236
224
|
function timestr2num(timestr){
|
237
225
|
return new Date(timestr).getTime();
|
238
226
|
}
|
239
|
-
if (
|
227
|
+
if (store.sortby === 'name'){
|
240
228
|
files.sort((a, b) => { return a.url.localeCompare(b.url); });
|
241
229
|
}
|
242
|
-
if (
|
230
|
+
if (store.sortby === 'size'){
|
243
231
|
files.sort((a, b) => { return a.file_size - b.file_size; });
|
244
232
|
}
|
245
|
-
if (
|
233
|
+
if (store.sortby === 'access'){
|
246
234
|
files.sort((a, b) => { return timestr2num(a.access_time) - timestr2num(b.access_time); });
|
247
235
|
}
|
248
|
-
if (
|
236
|
+
if (store.sortby === 'create'){
|
249
237
|
files.sort((a, b) => { return timestr2num(a.create_time) - timestr2num(b.create_time); });
|
250
238
|
}
|
251
|
-
if (
|
239
|
+
if (store.sortby === 'mime'){
|
252
240
|
files.sort((a, b) => { return a.mime_type.localeCompare(b.mime_type); });
|
253
241
|
}
|
254
|
-
if (
|
242
|
+
if (store.sortorder === 'desc'){ files.reverse(); }
|
255
243
|
}
|
256
|
-
sortBySelect.addEventListener('change', (elem) => {
|
257
|
-
sortOrderSelect.addEventListener('change', (elem) => {
|
244
|
+
sortBySelect.addEventListener('change', (elem) => {store.sortby = elem.target.value; refreshFileList();});
|
245
|
+
sortOrderSelect.addEventListener('change', (elem) => {store.sortorder = elem.target.value; refreshFileList();});
|
258
246
|
|
259
247
|
function refreshFileList(){
|
260
|
-
conn.listPath(
|
248
|
+
conn.listPath(store.dirpath)
|
261
249
|
.then(data => {
|
262
250
|
pathHintDiv.classList.remove('disconnected');
|
263
251
|
pathHintDiv.classList.add('connected');
|
264
|
-
pathHintLabel.textContent =
|
252
|
+
pathHintLabel.textContent = store.dirpath;
|
265
253
|
tbody.innerHTML = '';
|
266
254
|
|
267
255
|
console.log("Got data", data);
|
@@ -520,7 +508,7 @@ function refreshFileList(){
|
|
520
508
|
(err) => {
|
521
509
|
pathHintDiv.classList.remove('connected');
|
522
510
|
pathHintDiv.classList.add('disconnected');
|
523
|
-
pathHintLabel.textContent =
|
511
|
+
pathHintLabel.textContent = store.dirpath;
|
524
512
|
tbody.innerHTML = '';
|
525
513
|
console.log("Error");
|
526
514
|
console.error(err);
|
@@ -530,7 +518,7 @@ function refreshFileList(){
|
|
530
518
|
|
531
519
|
|
532
520
|
async function maybeRefreshUserRecord(){
|
533
|
-
if (
|
521
|
+
if (store.endpoint && store.token){
|
534
522
|
await refreshUserRecord();
|
535
523
|
}
|
536
524
|
}
|
@@ -545,9 +533,6 @@ async function refreshUserRecord(){
|
|
545
533
|
console.error("Failed to get user record");
|
546
534
|
return false;
|
547
535
|
}
|
548
|
-
|
549
|
-
// UI updates.
|
550
|
-
|
551
536
|
return true;
|
552
537
|
}
|
553
538
|
|
@@ -0,0 +1,57 @@
|
|
1
|
+
|
2
|
+
import Connector from './api.js';
|
3
|
+
|
4
|
+
function loadPersistedState(key, defaultValue) {
|
5
|
+
const persistedValue = window.localStorage.getItem(key);
|
6
|
+
return persistedValue ? persistedValue : defaultValue;
|
7
|
+
}
|
8
|
+
|
9
|
+
function setPersistedState(key, value) {
|
10
|
+
window.localStorage.setItem(key, value);
|
11
|
+
}
|
12
|
+
|
13
|
+
export const store = {
|
14
|
+
conn: new Connector(),
|
15
|
+
|
16
|
+
init() {
|
17
|
+
this.conn.config.token = this.token;
|
18
|
+
this.conn.config.endpoint = this.endpoint;
|
19
|
+
},
|
20
|
+
|
21
|
+
get token() {
|
22
|
+
return loadPersistedState('token', '');
|
23
|
+
},
|
24
|
+
set token(t) {
|
25
|
+
setPersistedState('token', t);
|
26
|
+
this.conn.config.token = t;
|
27
|
+
},
|
28
|
+
|
29
|
+
get endpoint() {
|
30
|
+
return loadPersistedState('endpoint', 'http://localhost:8000');
|
31
|
+
},
|
32
|
+
set endpoint(url) {
|
33
|
+
setPersistedState('endpoint', url);
|
34
|
+
this.conn.config.endpoint = url;
|
35
|
+
},
|
36
|
+
|
37
|
+
get dirpath() {
|
38
|
+
return loadPersistedState('dirpath', '/');
|
39
|
+
},
|
40
|
+
set dirpath(pth) {
|
41
|
+
setPersistedState('dirpath', pth);
|
42
|
+
},
|
43
|
+
|
44
|
+
get sortby () {
|
45
|
+
return loadPersistedState('sortby', 'none');
|
46
|
+
},
|
47
|
+
set sortby (sb) {
|
48
|
+
setPersistedState('sortby', sb);
|
49
|
+
},
|
50
|
+
|
51
|
+
get sortorder () {
|
52
|
+
return loadPersistedState('sortorder', 'asc');
|
53
|
+
},
|
54
|
+
set sortorder (so) {
|
55
|
+
setPersistedState('sortorder', so);
|
56
|
+
},
|
57
|
+
};
|
@@ -24,25 +24,26 @@ function getIconSVGFromMimeType(mimeType){
|
|
24
24
|
if (mimeType.startsWith('image/')){
|
25
25
|
return ICON_IMAGE;
|
26
26
|
}
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
return ICON_FILE;
|
27
|
+
|
28
|
+
if (['application/pdf', 'application/x-pdf'].includes(mimeType)){
|
29
|
+
return ICON_PDF;
|
30
|
+
}
|
31
|
+
if (['application/x-msdownload', 'application/x-msdos-program', 'application/x-msi', 'application/x-ms-wim', 'application/octet-stream', 'application/x-apple-diskimage'].includes(mimeType)){
|
32
|
+
return ICON_EXE;
|
33
|
+
}
|
34
|
+
if (['application/zip', 'application/x-zip-compressed', 'application/x-7z-compressed', 'application/x-rar-compressed', 'application/x-tar', 'application/x-gzip'].includes(mimeType)){
|
35
|
+
return ICON_ZIP;
|
36
|
+
}
|
37
|
+
if ([
|
38
|
+
"text/html", "application/xhtml+xml", "application/xml", "text/css", "application/javascript", "text/javascript", "application/json", "text/x-python", "text/x-java-source",
|
39
|
+
"application/x-httpd-php", "text/x-ruby", "text/x-perl", "application/x-sh", "application/sql", "text/x-c", "text/x-c++", "text/x-csharp", "text/x-go", "text/x-haskell",
|
40
|
+
"text/x-lua", "text/x-markdown", "application/wasm", "application/x-tcl", "text/x-yaml", "application/x-latex", "application/x-tex", "text/x-scss", "application/x-lisp",
|
41
|
+
"application/x-rust", "application/x-ruby", "text/x-asm"
|
42
|
+
].includes(mimeType)){
|
43
|
+
return ICON_CODE;
|
45
44
|
}
|
45
|
+
|
46
|
+
return ICON_FILE;
|
46
47
|
}
|
47
48
|
|
48
49
|
function getSafeIconUrl(icon_str){
|
@@ -66,10 +67,20 @@ export function makeThumbHtml(c, r){
|
|
66
67
|
const mtype = r.mime_type? r.mime_type : 'directory';
|
67
68
|
const thumb_id = `thumb-${thumb_counter++}`;
|
68
69
|
const url = mtype == 'directory'? ensureSlashEnd(r.url) : r.url;
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
70
|
+
|
71
|
+
if (mtype.startsWith('image/')){
|
72
|
+
return `
|
73
|
+
<div class="thumb" id="${thumb_id}"> \
|
74
|
+
<img src="${c.config.endpoint}/${url}?token=${token}&thumb=true" alt="${r.url}" class="thumb" \
|
75
|
+
onerror="this.src='${getSafeIconUrl(getIconSVGFromMimeType(mtype))}';this.classList.add('thumb-svg');" /> \
|
76
|
+
</div>
|
77
|
+
`;
|
78
|
+
}
|
79
|
+
else{
|
80
|
+
return `
|
81
|
+
<div class="thumb" id="${thumb_id}"> \
|
82
|
+
<img src="${getSafeIconUrl(getIconSVGFromMimeType(mtype))}" alt="${r.url}" class="thumb thumb-svg"/ >
|
83
|
+
</div>
|
84
|
+
`;
|
85
|
+
}
|
75
86
|
}
|
@@ -5,6 +5,7 @@ from typing import Optional
|
|
5
5
|
from PIL import Image
|
6
6
|
from io import BytesIO
|
7
7
|
import aiosqlite
|
8
|
+
from contextlib import asynccontextmanager
|
8
9
|
|
9
10
|
async def _maybe_init_thumb(c: aiosqlite.Cursor):
|
10
11
|
await c.execute('''
|
@@ -49,6 +50,13 @@ async def _delete_cache_thumb(c: aiosqlite.Cursor, path: str):
|
|
49
50
|
''', (path, ))
|
50
51
|
await c.execute('COMMIT')
|
51
52
|
|
53
|
+
@asynccontextmanager
|
54
|
+
async def cache_cursor():
|
55
|
+
async with aiosqlite.connect(THUMB_DB) as conn:
|
56
|
+
cur = await conn.cursor()
|
57
|
+
await _maybe_init_thumb(cur)
|
58
|
+
yield cur
|
59
|
+
|
52
60
|
async def get_thumb(path: str) -> Optional[tuple[bytes, str]]:
|
53
61
|
"""
|
54
62
|
returns [image bytes of thumbnail, mime type] if supported,
|
@@ -58,20 +66,17 @@ async def get_thumb(path: str) -> Optional[tuple[bytes, str]]:
|
|
58
66
|
if path.endswith('/'):
|
59
67
|
return None
|
60
68
|
|
61
|
-
async with
|
62
|
-
|
63
|
-
await
|
64
|
-
|
65
|
-
|
66
|
-
fconn = FileConn(main_c)
|
67
|
-
r = await fconn.get_file_record(path)
|
68
|
-
if r is None:
|
69
|
+
async with unique_cursor() as main_c:
|
70
|
+
fconn = FileConn(main_c)
|
71
|
+
r = await fconn.get_file_record(path)
|
72
|
+
if r is None:
|
73
|
+
async with cache_cursor() as cur:
|
69
74
|
await _delete_cache_thumb(cur, path)
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
return None
|
75
|
+
raise FileNotFoundError(f'File not found: {path}')
|
76
|
+
if not r.mime_type.startswith('image/'):
|
77
|
+
return None
|
74
78
|
|
79
|
+
async with cache_cursor() as cur:
|
75
80
|
c_time = r.create_time
|
76
81
|
thumb_blob = await _get_cache_thumb(cur, path, c_time)
|
77
82
|
if thumb_blob is not None:
|
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
|
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
|