lfss 0.7.12__tar.gz → 0.7.13__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.12 → lfss-0.7.13}/PKG-INFO +1 -1
- {lfss-0.7.12 → lfss-0.7.13}/frontend/index.html +21 -3
- {lfss-0.7.12 → lfss-0.7.13}/frontend/scripts.js +42 -5
- {lfss-0.7.12 → lfss-0.7.13}/frontend/styles.css +9 -7
- lfss-0.7.13/frontend/thumb.css +16 -0
- lfss-0.7.13/frontend/thumb.js +68 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/config.py +3 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/server.py +3 -1
- lfss-0.7.13/lfss/src/thumb.py +91 -0
- {lfss-0.7.12 → lfss-0.7.13}/pyproject.toml +1 -1
- lfss-0.7.12/lfss/src/thumb.py +0 -121
- {lfss-0.7.12 → lfss-0.7.13}/Readme.md +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/docs/Known_issues.md +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/docs/Permission.md +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/frontend/api.js +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/frontend/info.css +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/frontend/info.js +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/frontend/popup.css +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/frontend/popup.js +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/frontend/utils.js +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/cli/balance.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/cli/cli.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/cli/panel.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/cli/serve.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/cli/user.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/cli/vacuum.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/client/__init__.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/client/api.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/sql/init.sql +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/sql/pragma.sql +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/__init__.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/bounded_pool.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/connection_pool.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/database.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/datatype.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/error.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/log.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/stat.py +0 -0
- {lfss-0.7.12 → lfss-0.7.13}/lfss/src/utils.py +0 -0
@@ -24,9 +24,27 @@
|
|
24
24
|
</div>
|
25
25
|
|
26
26
|
<div class="container content" id="view">
|
27
|
-
<div id="
|
28
|
-
<
|
29
|
-
|
27
|
+
<div id="top-bar">
|
28
|
+
<div id="position-hint" class='disconnected'>
|
29
|
+
<span></span>
|
30
|
+
<label></label>
|
31
|
+
</div>
|
32
|
+
<div>
|
33
|
+
<span style="margin-right: 0.25rem; color: #999;">
|
34
|
+
Sort by</span>
|
35
|
+
<select id="sort-by-sel">
|
36
|
+
<option value="none">None</option>
|
37
|
+
<option value="name">Name</option>
|
38
|
+
<option value="size">Size</option>
|
39
|
+
<option value="access">Accessed</option>
|
40
|
+
<option value="create">Created</option>
|
41
|
+
<option value="mime">Type</option>
|
42
|
+
</select>
|
43
|
+
<select id="sort-order-sel">
|
44
|
+
<option value="asc">Ascending</option>
|
45
|
+
<option value="desc">Descending</option>
|
46
|
+
</select>
|
47
|
+
</div>
|
30
48
|
</div>
|
31
49
|
<table id="files">
|
32
50
|
<thead>
|
@@ -3,6 +3,7 @@ import { permMap } from './api.js';
|
|
3
3
|
import { showFloatingWindowLineInput, showPopup } from './popup.js';
|
4
4
|
import { formatSize, decodePathURI, ensurePathURI, getRandomString, cvtGMT2Local, debounce, encodePathURI, asHtmlText } from './utils.js';
|
5
5
|
import { showInfoPanel, showDirInfoPanel } from './info.js';
|
6
|
+
import { makeThumbHtml } from './thumb.js';
|
6
7
|
|
7
8
|
const conn = new Connector();
|
8
9
|
let userRecord = null;
|
@@ -22,6 +23,8 @@ const uploadFileSelector = document.querySelector('#file-selector');
|
|
22
23
|
const uploadFileNameInput = document.querySelector('#file-name');
|
23
24
|
const uploadButton = document.querySelector('#upload-btn');
|
24
25
|
const randomizeFnameButton = document.querySelector('#randomize-fname-btn');
|
26
|
+
const sortBySelect = document.querySelector('#sort-by-sel');
|
27
|
+
const sortOrderSelect = document.querySelector('#sort-order-sel');
|
25
28
|
|
26
29
|
conn.config.endpoint = endpointInput.value;
|
27
30
|
conn.config.token = tokenInput.value;
|
@@ -219,6 +222,40 @@ function maybeRefreshFileList(){
|
|
219
222
|
}
|
220
223
|
}
|
221
224
|
|
225
|
+
let sortBy = sortBySelect.value;
|
226
|
+
let sortOrder = sortOrderSelect.value;
|
227
|
+
/** @param {import('./api.js').DirectoryRecord} dirs */
|
228
|
+
function sortDirList(dirs){
|
229
|
+
if (sortBy === 'name'){
|
230
|
+
dirs.sort((a, b) => { return a.url.localeCompare(b.url); });
|
231
|
+
}
|
232
|
+
if (sortOrder === 'desc'){ dirs.reverse(); }
|
233
|
+
}
|
234
|
+
/** @param {import('./api.js').FileRecord} files */
|
235
|
+
function sortFileList(files){
|
236
|
+
function timestr2num(timestr){
|
237
|
+
return new Date(timestr).getTime();
|
238
|
+
}
|
239
|
+
if (sortBy === 'name'){
|
240
|
+
files.sort((a, b) => { return a.url.localeCompare(b.url); });
|
241
|
+
}
|
242
|
+
if (sortBy === 'size'){
|
243
|
+
files.sort((a, b) => { return a.file_size - b.file_size; });
|
244
|
+
}
|
245
|
+
if (sortBy === 'access'){
|
246
|
+
files.sort((a, b) => { return timestr2num(a.access_time) - timestr2num(b.access_time); });
|
247
|
+
}
|
248
|
+
if (sortBy === 'create'){
|
249
|
+
files.sort((a, b) => { return timestr2num(a.create_time) - timestr2num(b.create_time); });
|
250
|
+
}
|
251
|
+
if (sortBy === 'mime'){
|
252
|
+
files.sort((a, b) => { return a.mime_type.localeCompare(b.mime_type); });
|
253
|
+
}
|
254
|
+
if (sortOrder === 'desc'){ files.reverse(); }
|
255
|
+
}
|
256
|
+
sortBySelect.addEventListener('change', (elem) => {sortBy = elem.target.value; refreshFileList();});
|
257
|
+
sortOrderSelect.addEventListener('change', (elem) => {sortOrder = elem.target.value; refreshFileList();});
|
258
|
+
|
222
259
|
function refreshFileList(){
|
223
260
|
conn.listPath(pathInput.value)
|
224
261
|
.then(data => {
|
@@ -232,6 +269,9 @@ function refreshFileList(){
|
|
232
269
|
if (!data.dirs){ data.dirs = []; }
|
233
270
|
if (!data.files){ data.files = []; }
|
234
271
|
|
272
|
+
sortDirList(data.dirs);
|
273
|
+
sortFileList(data.files);
|
274
|
+
|
235
275
|
data.dirs.forEach(dir => {
|
236
276
|
const tr = document.createElement('tr');
|
237
277
|
const sizeTd = document.createElement('td');
|
@@ -253,10 +293,7 @@ function refreshFileList(){
|
|
253
293
|
dirLink.href = '#';
|
254
294
|
const nameDiv = document.createElement('div');
|
255
295
|
nameDiv.classList.add('filename-container');
|
256
|
-
|
257
|
-
thumbImg.classList.add('thumb');
|
258
|
-
thumbImg.src = conn.config.endpoint + '/' + ensureSlashEnd(dir.url) + '?token=' + conn.config.token + '&thumb=true';
|
259
|
-
nameDiv.appendChild(thumbImg);
|
296
|
+
nameDiv.innerHTML = makeThumbHtml(conn, dir);
|
260
297
|
nameDiv.appendChild(dirLink);
|
261
298
|
nameTd.appendChild(nameDiv);
|
262
299
|
|
@@ -353,7 +390,7 @@ function refreshFileList(){
|
|
353
390
|
|
354
391
|
nameTd.innerHTML = `
|
355
392
|
<div class="filename-container">
|
356
|
-
|
393
|
+
${makeThumbHtml(conn, file)}
|
357
394
|
<span>${asHtmlText(fileName)}</span>
|
358
395
|
</div>
|
359
396
|
`
|
@@ -1,5 +1,6 @@
|
|
1
1
|
@import "./popup.css";
|
2
2
|
@import "./info.css";
|
3
|
+
@import "./thumb.css";
|
3
4
|
|
4
5
|
body{
|
5
6
|
font-family: Arial, sans-serif;
|
@@ -120,6 +121,13 @@ input#path{
|
|
120
121
|
cursor: pointer;
|
121
122
|
}
|
122
123
|
|
124
|
+
div#top-bar{
|
125
|
+
display: flex;
|
126
|
+
flex-direction: row;
|
127
|
+
justify-content: space-between;
|
128
|
+
align-items: center;
|
129
|
+
gap: 1rem;
|
130
|
+
}
|
123
131
|
div#position-hint{
|
124
132
|
color: rgb(138, 138, 138);
|
125
133
|
border-radius: 0.5rem;
|
@@ -186,14 +194,8 @@ div.filename-container{
|
|
186
194
|
flex-direction: row;
|
187
195
|
align-items: center;
|
188
196
|
gap: 0.5rem;
|
189
|
-
height: 32px;
|
197
|
+
height: 32px;
|
190
198
|
}
|
191
|
-
img.thumb{
|
192
|
-
max-height: 32px; /* smaller than backend */
|
193
|
-
max-width: 32px;
|
194
|
-
border-radius: 0.25rem;
|
195
|
-
}
|
196
|
-
|
197
199
|
label#upload-file-prefix{
|
198
200
|
width: 800px;
|
199
201
|
text-align: right;
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
div.thumb{
|
3
|
+
height: 32px;
|
4
|
+
width: 32px;
|
5
|
+
border-radius: 0.25rem;
|
6
|
+
display: contents;
|
7
|
+
}
|
8
|
+
img.thumb{
|
9
|
+
max-height: 32px; /* smaller than backend */
|
10
|
+
max-width: 32px;
|
11
|
+
border-radius: 0.25rem;
|
12
|
+
}
|
13
|
+
img.thumb-svg{
|
14
|
+
/* dark blue */
|
15
|
+
filter: invert(30%) sepia(100%) saturate(80%) hue-rotate(180deg);
|
16
|
+
}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
/**
|
3
|
+
* @import { FileRecord, DirectoryRecord } from './api.js'
|
4
|
+
*/
|
5
|
+
import Connector from './api.js';
|
6
|
+
|
7
|
+
// from: https://pictogrammers.com/library/mdi/
|
8
|
+
const ICON_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>folder-outline</title><path d="M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z" /></svg>';
|
9
|
+
const ICON_FILE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>file-document-outline</title><path d="M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z" /></svg>'
|
10
|
+
const ICON_PDF = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>file-pdf-box</title><path d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M9.5 11.5C9.5 12.3 8.8 13 8 13H7V15H5.5V9H8C8.8 9 9.5 9.7 9.5 10.5V11.5M14.5 13.5C14.5 14.3 13.8 15 13 15H10.5V9H13C13.8 9 14.5 9.7 14.5 10.5V13.5M18.5 10.5H17V11.5H18.5V13H17V15H15.5V9H18.5V10.5M12 10.5H13V13.5H12V10.5M7 10.5H8V11.5H7V10.5Z" /></svg>'
|
11
|
+
const ICON_EXE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>application-brackets-outline</title><path d="M9.5,8.5L11,10L8,13L11,16L9.5,17.5L5,13L9.5,8.5M14.5,17.5L13,16L16,13L13,10L14.5,8.5L19,13L14.5,17.5M21,2H3A2,2 0 0,0 1,4V20A2,2 0 0,0 3,22H21A2,2 0 0,0 23,20V4A2,2 0 0,0 21,2M21,20H3V6H21V20Z" /></svg>'
|
12
|
+
const ICON_ZIP = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>zip-box-outline</title><path d="M12 17V15H14V17H12M14 13V11H12V13H14M14 9V7H12V9H14M10 11H12V9H10V11M10 15H12V13H10V15M21 5V19C21 20.1 20.1 21 19 21H5C3.9 21 3 20.1 3 19V5C3 3.9 3.9 3 5 3H19C20.1 3 21 3.9 21 5M19 5H12V7H10V5H5V19H19V5Z" /></svg>'
|
13
|
+
const ICON_CODE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>file-code-outline</title><path d="M14 2H6C4.89 2 4 2.9 4 4V20C4 21.11 4.89 22 6 22H18C19.11 22 20 21.11 20 20V8L14 2M18 20H6V4H13V9H18V20M9.54 15.65L11.63 17.74L10.35 19L7 15.65L10.35 12.3L11.63 13.56L9.54 15.65M17 15.65L13.65 19L12.38 17.74L14.47 15.65L12.38 13.56L13.65 12.3L17 15.65Z" /></svg>'
|
14
|
+
const ICON_VIDEO = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>television-play</title><path d="M21,3H3C1.89,3 1,3.89 1,5V17A2,2 0 0,0 3,19H8V21H16V19H21A2,2 0 0,0 23,17V5C23,3.89 22.1,3 21,3M21,17H3V5H21M16,11L9,15V7" /></svg>'
|
15
|
+
const ICON_IMAGE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>image-outline</title><path d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M13.96,12.29L11.21,15.83L9.25,13.47L6.5,17H17.5L13.96,12.29Z" /></svg>'
|
16
|
+
|
17
|
+
function getIconSVGFromMimeType(mimeType){
|
18
|
+
if (mimeType == 'directory'){
|
19
|
+
return ICON_FOLDER;
|
20
|
+
}
|
21
|
+
if (mimeType.startsWith('video/')){
|
22
|
+
return ICON_VIDEO;
|
23
|
+
}
|
24
|
+
if (mimeType.startsWith('image/')){
|
25
|
+
return ICON_IMAGE;
|
26
|
+
}
|
27
|
+
switch (mimeType){
|
28
|
+
case
|
29
|
+
'application/pdf' || 'application/x-pdf':
|
30
|
+
return ICON_PDF;
|
31
|
+
case
|
32
|
+
'application/x-msdownload' || 'application/x-msdos-program' || 'application/x-msi' || 'application/x-ms-wim' || 'application/octet-stream' || 'application/x-apple-diskimage':
|
33
|
+
return ICON_EXE;
|
34
|
+
case
|
35
|
+
'application/zip' || 'application/x-zip-compressed' || 'application/x-7z-compressed' || 'application/x-rar-compressed' || 'application/x-tar' || 'application/x-gzip':
|
36
|
+
return ICON_ZIP;
|
37
|
+
case
|
38
|
+
'text/x-python' || 'text/x-c' || 'text/x-c++' || 'text/x-java' || 'text/x-javascript' || 'text/x-php' || 'text/x-rust' || 'text/x-go' || 'text/x-csharp' || 'text/x-typescript' ||
|
39
|
+
'text/x-html' || 'text/x-css' || 'text/x-sql' || 'text/x-xml' || 'text/x-yaml' || 'text/x-json' || 'text/x-markdown' || 'text/x-shellscript' ||
|
40
|
+
'text/x-bat' || 'text/x-powershell' || 'text/x-bash' || 'text/x-perl' || 'text/x-ruby' || 'text/x-lua' || 'text/x-tcl' || 'text/x-lisp' ||
|
41
|
+
'text/x-haskell' || 'text/x-elm' || 'text/x-crystal' || 'text/x-nim' || 'text/x-zig':
|
42
|
+
return ICON_CODE;
|
43
|
+
default:
|
44
|
+
return ICON_FILE;
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
function getSafeIconUrl(icon_str){
|
49
|
+
return 'data:image/svg+xml,' + encodeURIComponent(icon_str);
|
50
|
+
}
|
51
|
+
|
52
|
+
let thumb_counter = 0;
|
53
|
+
/**
|
54
|
+
* @param {Connector} c
|
55
|
+
* @param {FileRecord | DirectoryRecord} r
|
56
|
+
* @returns {string}
|
57
|
+
*/
|
58
|
+
export function makeThumbHtml(c, r){
|
59
|
+
const token = c.config.token;
|
60
|
+
const mtype = r.mime_type? r.mime_type : 'directory';
|
61
|
+
const thumb_id = `thumb-${thumb_counter++}`;
|
62
|
+
return `
|
63
|
+
<div class="thumb" id="${thumb_id}"> \
|
64
|
+
<img src="${c.config.endpoint}/${r.url}?token=${token}&thumb=true" alt="${r.url}" class="thumb" \
|
65
|
+
onerror="this.src='${getSafeIconUrl(getIconSVGFromMimeType(mtype))}';this.classList.add('thumb-svg');" \
|
66
|
+
</div>
|
67
|
+
`;
|
68
|
+
}
|
@@ -127,7 +127,9 @@ async def emit_thumbnail(
|
|
127
127
|
fname = path.split("/")[-2]
|
128
128
|
else:
|
129
129
|
fname = path.split("/")[-1]
|
130
|
-
|
130
|
+
if (thumb_res := await get_thumb(path)) is None:
|
131
|
+
raise HTTPException(status_code=415, detail="Thumbnail not supported")
|
132
|
+
thumb_blob, mime_type = thumb_res
|
131
133
|
disp = "inline" if not download else "attachment"
|
132
134
|
headers = {
|
133
135
|
"Content-Disposition": f"{disp}; filename={fname}.thumb.jpg",
|
@@ -0,0 +1,91 @@
|
|
1
|
+
from lfss.src.config import THUMB_DB, THUMB_SIZE
|
2
|
+
from lfss.src.database import FileConn
|
3
|
+
from lfss.src.connection_pool import unique_cursor
|
4
|
+
from typing import Optional
|
5
|
+
from PIL import Image
|
6
|
+
from io import BytesIO
|
7
|
+
import aiosqlite
|
8
|
+
|
9
|
+
async def _maybe_init_thumb(c: aiosqlite.Cursor):
|
10
|
+
await c.execute('''
|
11
|
+
CREATE TABLE IF NOT EXISTS thumbs (
|
12
|
+
path TEXT PRIMARY KEY,
|
13
|
+
ctime TEXT,
|
14
|
+
thumb BLOB
|
15
|
+
)
|
16
|
+
''')
|
17
|
+
|
18
|
+
async def _get_cache_thumb(c: aiosqlite.Cursor, path: str, ctime: str) -> Optional[bytes]:
|
19
|
+
res = await c.execute('''
|
20
|
+
SELECT ctime, thumb FROM thumbs WHERE path = ?
|
21
|
+
''', (path, ))
|
22
|
+
row = await res.fetchone()
|
23
|
+
if row is None:
|
24
|
+
return None
|
25
|
+
# check if ctime matches, if not delete and return None
|
26
|
+
if row[0] != ctime:
|
27
|
+
await _delete_cache_thumb(c, path)
|
28
|
+
return None
|
29
|
+
blob: bytes = row[1]
|
30
|
+
return blob
|
31
|
+
|
32
|
+
async def _save_cache_thumb(c: aiosqlite.Cursor, path: str, ctime: str, raw_bytes: bytes) -> bytes:
|
33
|
+
raw_img = Image.open(BytesIO(raw_bytes))
|
34
|
+
raw_img.thumbnail(THUMB_SIZE)
|
35
|
+
img = raw_img.convert('RGB')
|
36
|
+
bio = BytesIO()
|
37
|
+
img.save(bio, 'JPEG')
|
38
|
+
blob = bio.getvalue()
|
39
|
+
await c.execute('''
|
40
|
+
INSERT OR REPLACE INTO thumbs (path, ctime, thumb) VALUES (?, ?, ?)
|
41
|
+
''', (path, ctime, blob))
|
42
|
+
await c.execute('COMMIT') # commit immediately
|
43
|
+
return blob
|
44
|
+
|
45
|
+
async def _delete_cache_thumb(c: aiosqlite.Cursor, path: str):
|
46
|
+
await c.execute('''
|
47
|
+
DELETE FROM thumbs WHERE path = ?
|
48
|
+
''', (path, ))
|
49
|
+
await c.execute('COMMIT')
|
50
|
+
|
51
|
+
async def get_thumb(path: str) -> Optional[tuple[bytes, str]]:
|
52
|
+
"""
|
53
|
+
returns [image bytes of thumbnail, mime type] if supported,
|
54
|
+
or None if not supported.
|
55
|
+
Raises FileNotFoundError if file does not exist
|
56
|
+
"""
|
57
|
+
if path.endswith('/'):
|
58
|
+
return None
|
59
|
+
|
60
|
+
async with aiosqlite.connect(THUMB_DB) as conn:
|
61
|
+
cur = await conn.cursor()
|
62
|
+
await _maybe_init_thumb(cur)
|
63
|
+
|
64
|
+
async with unique_cursor() as main_c:
|
65
|
+
fconn = FileConn(main_c)
|
66
|
+
r = await fconn.get_file_record(path)
|
67
|
+
if r is None:
|
68
|
+
await _delete_cache_thumb(cur, path)
|
69
|
+
raise FileNotFoundError(f'File not found: {path}')
|
70
|
+
|
71
|
+
if not r.mime_type.startswith('image/'):
|
72
|
+
return None
|
73
|
+
|
74
|
+
c_time = r.create_time
|
75
|
+
thumb_blob = await _get_cache_thumb(cur, path, c_time)
|
76
|
+
if thumb_blob is not None:
|
77
|
+
return thumb_blob, "image/jpeg"
|
78
|
+
|
79
|
+
# generate thumb
|
80
|
+
async with unique_cursor() as main_c:
|
81
|
+
fconn = FileConn(main_c)
|
82
|
+
if r.external:
|
83
|
+
data = b""
|
84
|
+
async for chunk in fconn.get_file_blob_external(r.file_id):
|
85
|
+
data += chunk
|
86
|
+
else:
|
87
|
+
data = await fconn.get_file_blob(r.file_id)
|
88
|
+
assert data is not None
|
89
|
+
|
90
|
+
thumb_blob = await _save_cache_thumb(cur, path, c_time, data)
|
91
|
+
return thumb_blob, "image/jpeg"
|
lfss-0.7.12/lfss/src/thumb.py
DELETED
@@ -1,121 +0,0 @@
|
|
1
|
-
from lfss.src.config import DATA_HOME
|
2
|
-
from lfss.src.database import FileConn
|
3
|
-
from lfss.src.connection_pool import unique_cursor
|
4
|
-
from typing import Optional
|
5
|
-
from PIL import Image
|
6
|
-
from io import BytesIO
|
7
|
-
import aiosqlite
|
8
|
-
|
9
|
-
def prepare_svg(svg_str: str):
|
10
|
-
"""
|
11
|
-
Injects grey color to the svg string, and encodes it to bytes
|
12
|
-
"""
|
13
|
-
color = "#666"
|
14
|
-
x = svg_str.replace('<path', '<path fill="{}"'.format(color))
|
15
|
-
x = x.replace('<svg', '<svg fill="{}"'.format(color))
|
16
|
-
return x.encode()
|
17
|
-
|
18
|
-
# from: https://pictogrammers.com/library/mdi/
|
19
|
-
ICON_FOLER = prepare_svg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>folder-outline</title><path d="M20,18H4V8H20M20,6H12L10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6Z" /></svg>')
|
20
|
-
ICON_FILE = prepare_svg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>file-document-outline</title><path d="M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z" /></svg>')
|
21
|
-
ICON_PDF = prepare_svg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>file-pdf-box</title><path d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M9.5 11.5C9.5 12.3 8.8 13 8 13H7V15H5.5V9H8C8.8 9 9.5 9.7 9.5 10.5V11.5M14.5 13.5C14.5 14.3 13.8 15 13 15H10.5V9H13C13.8 9 14.5 9.7 14.5 10.5V13.5M18.5 10.5H17V11.5H18.5V13H17V15H15.5V9H18.5V10.5M12 10.5H13V13.5H12V10.5M7 10.5H8V11.5H7V10.5Z" /></svg>')
|
22
|
-
ICON_EXE = prepare_svg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>application-brackets-outline</title><path d="M9.5,8.5L11,10L8,13L11,16L9.5,17.5L5,13L9.5,8.5M14.5,17.5L13,16L16,13L13,10L14.5,8.5L19,13L14.5,17.5M21,2H3A2,2 0 0,0 1,4V20A2,2 0 0,0 3,22H21A2,2 0 0,0 23,20V4A2,2 0 0,0 21,2M21,20H3V6H21V20Z" /></svg>')
|
23
|
-
ICON_ZIP = prepare_svg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>zip-box-outline</title><path d="M12 17V15H14V17H12M14 13V11H12V13H14M14 9V7H12V9H14M10 11H12V9H10V11M10 15H12V13H10V15M21 5V19C21 20.1 20.1 21 19 21H5C3.9 21 3 20.1 3 19V5C3 3.9 3.9 3 5 3H19C20.1 3 21 3.9 21 5M19 5H12V7H10V5H5V19H19V5Z" /></svg>')
|
24
|
-
ICON_CODE = prepare_svg('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>file-code-outline</title><path d="M14 2H6C4.89 2 4 2.9 4 4V20C4 21.11 4.89 22 6 22H18C19.11 22 20 21.11 20 20V8L14 2M18 20H6V4H13V9H18V20M9.54 15.65L11.63 17.74L10.35 19L7 15.65L10.35 12.3L11.63 13.56L9.54 15.65M17 15.65L13.65 19L12.38 17.74L14.47 15.65L12.38 13.56L13.65 12.3L17 15.65Z" /></svg>')
|
25
|
-
|
26
|
-
THUMB_DB = DATA_HOME / 'thumbs.db'
|
27
|
-
THUMB_SIZE = (48, 48)
|
28
|
-
|
29
|
-
async def _maybe_init_thumb(c: aiosqlite.Cursor):
|
30
|
-
await c.execute('''
|
31
|
-
CREATE TABLE IF NOT EXISTS thumbs (
|
32
|
-
path TEXT PRIMARY KEY,
|
33
|
-
ctime TEXT,
|
34
|
-
thumb BLOB
|
35
|
-
)
|
36
|
-
''')
|
37
|
-
|
38
|
-
async def _get_cache_thumb(c: aiosqlite.Cursor, path: str, ctime: str) -> Optional[bytes]:
|
39
|
-
res = await c.execute('''
|
40
|
-
SELECT ctime, thumb FROM thumbs WHERE path = ?
|
41
|
-
''', (path, ))
|
42
|
-
row = await res.fetchone()
|
43
|
-
if row is None:
|
44
|
-
return None
|
45
|
-
# check if ctime matches, if not delete and return None
|
46
|
-
if row[0] != ctime:
|
47
|
-
await _delete_cache_thumb(c, path)
|
48
|
-
return None
|
49
|
-
blob: bytes = row[1]
|
50
|
-
return blob
|
51
|
-
|
52
|
-
async def _save_cache_thumb(c: aiosqlite.Cursor, path: str, ctime: str, raw_bytes: bytes) -> bytes:
|
53
|
-
raw_img = Image.open(BytesIO(raw_bytes))
|
54
|
-
raw_img.thumbnail(THUMB_SIZE)
|
55
|
-
img = raw_img.convert('RGB')
|
56
|
-
bio = BytesIO()
|
57
|
-
img.save(bio, 'JPEG')
|
58
|
-
blob = bio.getvalue()
|
59
|
-
await c.execute('''
|
60
|
-
INSERT OR REPLACE INTO thumbs (path, ctime, thumb) VALUES (?, ?, ?)
|
61
|
-
''', (path, ctime, blob))
|
62
|
-
await c.execute('COMMIT') # commit immediately
|
63
|
-
return blob
|
64
|
-
|
65
|
-
async def _delete_cache_thumb(c: aiosqlite.Cursor, path: str):
|
66
|
-
await c.execute('''
|
67
|
-
DELETE FROM thumbs WHERE path = ?
|
68
|
-
''', (path, ))
|
69
|
-
await c.execute('COMMIT')
|
70
|
-
|
71
|
-
async def get_thumb(path: str) -> tuple[bytes, str]:
|
72
|
-
"""
|
73
|
-
returns [image bytes of thumbnail, mime type] if supported,
|
74
|
-
or None if not supported.
|
75
|
-
Raises FileNotFoundError if file does not exist
|
76
|
-
"""
|
77
|
-
if path.endswith('/'):
|
78
|
-
return ICON_FOLER, 'image/svg+xml'
|
79
|
-
|
80
|
-
async with aiosqlite.connect(THUMB_DB) as conn:
|
81
|
-
cur = await conn.cursor()
|
82
|
-
await _maybe_init_thumb(cur)
|
83
|
-
|
84
|
-
async with unique_cursor() as main_c:
|
85
|
-
fconn = FileConn(main_c)
|
86
|
-
r = await fconn.get_file_record(path)
|
87
|
-
if r is None:
|
88
|
-
await _delete_cache_thumb(cur, path)
|
89
|
-
raise FileNotFoundError(f'File not found: {path}')
|
90
|
-
|
91
|
-
if not r.mime_type.startswith('image/'):
|
92
|
-
match r.mime_type:
|
93
|
-
case 'application/pdf' | 'application/x-pdf':
|
94
|
-
return ICON_PDF, 'image/svg+xml'
|
95
|
-
case 'application/x-msdownload' | 'application/x-msdos-program' | 'application/x-msi' | 'application/octet-stream':
|
96
|
-
return ICON_EXE, 'image/svg+xml'
|
97
|
-
case 'application/zip' | 'application/x-zip-compressed' | 'application/x-zip' | 'application/x-compressed' | 'application/x-compress':
|
98
|
-
return ICON_ZIP, 'image/svg+xml'
|
99
|
-
case 'text/plain' | 'text/x-python' | 'text/x-c' | 'text/x-c++' | 'text/x-java' | 'text/x-php' | 'text/x-shellscript' | 'text/x-perl' | 'text/x-ruby' | 'text/x-go' | 'text/x-rust' | 'text/x-haskell' | 'text/x-lisp' | 'text/x-lua' | 'text/x-tcl' | 'text/x-sql' | 'text/x-yaml' | 'text/x-xml' | 'text/x-markdown' | 'text/x-tex' | 'text/x-asm' | 'text/x-fortran' | 'text/x-pascal' | 'text/x-erlang' | 'text/x-ocaml' | 'text/x-matlab' | 'text/x-csharp' | 'text/x-swift' | 'text/x-kotlin' | 'text/x-dart' | 'text/x-julia' | 'text/x-scala' | 'text/x-clojure' | 'text/x-elm' | 'text/x-crystal' | 'text/x-nim' | 'text/x-zig':
|
100
|
-
return ICON_CODE, 'image/svg+xml'
|
101
|
-
case _:
|
102
|
-
return ICON_FILE, 'image/svg+xml'
|
103
|
-
|
104
|
-
c_time = r.create_time
|
105
|
-
thumb_blob = await _get_cache_thumb(cur, path, c_time)
|
106
|
-
if thumb_blob is not None:
|
107
|
-
return thumb_blob, "image/jpeg"
|
108
|
-
|
109
|
-
# generate thumb
|
110
|
-
async with unique_cursor() as main_c:
|
111
|
-
fconn = FileConn(main_c)
|
112
|
-
if r.external:
|
113
|
-
data = b""
|
114
|
-
async for chunk in fconn.get_file_blob_external(r.file_id):
|
115
|
-
data += chunk
|
116
|
-
else:
|
117
|
-
data = await fconn.get_file_blob(r.file_id)
|
118
|
-
assert data is not None
|
119
|
-
|
120
|
-
thumb_blob = await _save_cache_thumb(cur, path, c_time, data)
|
121
|
-
return thumb_blob, "image/jpeg"
|
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
|