lfss 0.7.11__py3-none-any.whl → 0.7.12__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/scripts.js +16 -3
- frontend/styles.css +13 -0
- frontend/utils.js +7 -0
- lfss/client/__init__.py +3 -3
- lfss/client/api.py +3 -2
- lfss/src/bounded_pool.py +44 -0
- lfss/src/server.py +63 -27
- lfss/src/thumb.py +121 -0
- {lfss-0.7.11.dist-info → lfss-0.7.12.dist-info}/METADATA +2 -1
- {lfss-0.7.11.dist-info → lfss-0.7.12.dist-info}/RECORD +12 -10
- {lfss-0.7.11.dist-info → lfss-0.7.12.dist-info}/WHEEL +0 -0
- {lfss-0.7.11.dist-info → lfss-0.7.12.dist-info}/entry_points.txt +0 -0
frontend/scripts.js
CHANGED
@@ -1,7 +1,7 @@
|
|
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, getRandomString, cvtGMT2Local, debounce, encodePathURI } from './utils.js';
|
4
|
+
import { formatSize, decodePathURI, ensurePathURI, getRandomString, cvtGMT2Local, debounce, encodePathURI, asHtmlText } from './utils.js';
|
5
5
|
import { showInfoPanel, showDirInfoPanel } from './info.js';
|
6
6
|
|
7
7
|
const conn = new Connector();
|
@@ -251,7 +251,14 @@ function refreshFileList(){
|
|
251
251
|
onPathChange();
|
252
252
|
});
|
253
253
|
dirLink.href = '#';
|
254
|
-
|
254
|
+
const nameDiv = document.createElement('div');
|
255
|
+
nameDiv.classList.add('filename-container');
|
256
|
+
const thumbImg = document.createElement('img');
|
257
|
+
thumbImg.classList.add('thumb');
|
258
|
+
thumbImg.src = conn.config.endpoint + '/' + ensureSlashEnd(dir.url) + '?token=' + conn.config.token + '&thumb=true';
|
259
|
+
nameDiv.appendChild(thumbImg);
|
260
|
+
nameDiv.appendChild(dirLink);
|
261
|
+
nameTd.appendChild(nameDiv);
|
255
262
|
|
256
263
|
tr.appendChild(nameTd);
|
257
264
|
tbody.appendChild(tr);
|
@@ -343,7 +350,13 @@ function refreshFileList(){
|
|
343
350
|
const nameTd = document.createElement('td');
|
344
351
|
const plainUrl = decodePathURI(file.url);
|
345
352
|
const fileName = plainUrl.split('/').pop();
|
346
|
-
|
353
|
+
|
354
|
+
nameTd.innerHTML = `
|
355
|
+
<div class="filename-container">
|
356
|
+
<img class="thumb" src="${conn.config.endpoint}/${file.url}?token=${conn.config.token}&thumb=true" />
|
357
|
+
<span>${asHtmlText(fileName)}</span>
|
358
|
+
</div>
|
359
|
+
`
|
347
360
|
tr.appendChild(nameTd);
|
348
361
|
tbody.appendChild(tr);
|
349
362
|
}
|
frontend/styles.css
CHANGED
@@ -181,6 +181,19 @@ table#files tr td:nth-child(3), table#files tr td:nth-child(5){
|
|
181
181
|
width: 12%;
|
182
182
|
}
|
183
183
|
|
184
|
+
div.filename-container{
|
185
|
+
display: flex;
|
186
|
+
flex-direction: row;
|
187
|
+
align-items: center;
|
188
|
+
gap: 0.5rem;
|
189
|
+
height: 32px; /* same as thumbnail */
|
190
|
+
}
|
191
|
+
img.thumb{
|
192
|
+
max-height: 32px; /* smaller than backend */
|
193
|
+
max-width: 32px;
|
194
|
+
border-radius: 0.25rem;
|
195
|
+
}
|
196
|
+
|
184
197
|
label#upload-file-prefix{
|
185
198
|
width: 800px;
|
186
199
|
text-align: right;
|
frontend/utils.js
CHANGED
@@ -86,4 +86,11 @@ export function debounce(fn,wait){
|
|
86
86
|
if (timeout) clearTimeout(timeout);
|
87
87
|
timeout = setTimeout(() => fn.apply(context, args), wait);
|
88
88
|
}
|
89
|
+
}
|
90
|
+
|
91
|
+
export function asHtmlText(text){
|
92
|
+
const anonElem = document.createElement('div');
|
93
|
+
anonElem.textContent = text;
|
94
|
+
const htmlText = anonElem.innerHTML;
|
95
|
+
return htmlText;
|
89
96
|
}
|
lfss/client/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import os, time, pathlib
|
2
2
|
from threading import Lock
|
3
|
-
from concurrent.futures import ThreadPoolExecutor
|
4
3
|
from .api import Connector
|
4
|
+
from ..src.bounded_pool import BoundedThreadPoolExecutor
|
5
5
|
|
6
6
|
def upload_file(
|
7
7
|
connector: Connector,
|
@@ -68,7 +68,7 @@ def upload_directory(
|
|
68
68
|
):
|
69
69
|
faild_files.append(file_path)
|
70
70
|
|
71
|
-
with
|
71
|
+
with BoundedThreadPoolExecutor(n_concurrent) as executor:
|
72
72
|
for root, dirs, files in os.walk(directory):
|
73
73
|
for file in files:
|
74
74
|
executor.submit(put_file, os.path.join(root, file))
|
@@ -149,7 +149,7 @@ def download_directory(
|
|
149
149
|
):
|
150
150
|
failed_files.append(src_url)
|
151
151
|
|
152
|
-
with
|
152
|
+
with BoundedThreadPoolExecutor(n_concurrent) as executor:
|
153
153
|
for file in connector.list_path(src_path, flat=True).files:
|
154
154
|
executor.submit(get_file, file.url)
|
155
155
|
return failed_files
|
lfss/client/api.py
CHANGED
@@ -31,8 +31,9 @@ class Connector:
|
|
31
31
|
headers.update({
|
32
32
|
'Authorization': f"Bearer {self.config['token']}",
|
33
33
|
})
|
34
|
-
|
35
|
-
|
34
|
+
with requests.Session() as s:
|
35
|
+
response = s.request(method, url, headers=headers, **kwargs)
|
36
|
+
response.raise_for_status()
|
36
37
|
return response
|
37
38
|
return f
|
38
39
|
|
lfss/src/bounded_pool.py
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
"""
|
2
|
+
Code modified from: https://github.com/mowshon/bounded_pool_executor
|
3
|
+
"""
|
4
|
+
|
5
|
+
import multiprocessing
|
6
|
+
import concurrent.futures
|
7
|
+
import multiprocessing.synchronize
|
8
|
+
import threading
|
9
|
+
from typing import Optional
|
10
|
+
|
11
|
+
class _BoundedPoolExecutor:
|
12
|
+
|
13
|
+
semaphore: Optional[multiprocessing.synchronize.BoundedSemaphore | threading.BoundedSemaphore] = None
|
14
|
+
_max_workers: int
|
15
|
+
|
16
|
+
def acquire(self):
|
17
|
+
assert self.semaphore is not None
|
18
|
+
self.semaphore.acquire()
|
19
|
+
|
20
|
+
def release(self, fn):
|
21
|
+
assert self.semaphore is not None
|
22
|
+
self.semaphore.release()
|
23
|
+
|
24
|
+
def submit(self, fn, *args, **kwargs):
|
25
|
+
self.acquire()
|
26
|
+
future = super().submit(fn, *args, **kwargs) # type: ignore
|
27
|
+
future.add_done_callback(self.release)
|
28
|
+
|
29
|
+
return future
|
30
|
+
|
31
|
+
|
32
|
+
class BoundedProcessPoolExecutor(_BoundedPoolExecutor, concurrent.futures.ProcessPoolExecutor):
|
33
|
+
|
34
|
+
def __init__(self, max_workers=None):
|
35
|
+
super().__init__(max_workers)
|
36
|
+
self.semaphore = multiprocessing.BoundedSemaphore(self._max_workers * 2)
|
37
|
+
|
38
|
+
|
39
|
+
class BoundedThreadPoolExecutor(_BoundedPoolExecutor, concurrent.futures.ThreadPoolExecutor):
|
40
|
+
|
41
|
+
def __init__(self, max_workers=None):
|
42
|
+
super().__init__(max_workers)
|
43
|
+
self.semaphore = threading.BoundedSemaphore(self._max_workers * 2)
|
44
|
+
|
lfss/src/server.py
CHANGED
@@ -19,6 +19,7 @@ from .config import MAX_BUNDLE_BYTES, MAX_FILE_BYTES, LARGE_FILE_BYTES, CHUNK_SI
|
|
19
19
|
from .utils import ensure_uri_compnents, format_last_modified, now_stamp
|
20
20
|
from .connection_pool import global_connection_init, global_connection_close, unique_cursor
|
21
21
|
from .database import Database, UserRecord, DECOY_USER, FileRecord, check_user_permission, FileReadPermission, UserConn, FileConn, PathContents
|
22
|
+
from .thumb import get_thumb
|
22
23
|
|
23
24
|
logger = get_logger("server", term_level="DEBUG")
|
24
25
|
logger_failed_request = get_logger("failed_requests", term_level="INFO")
|
@@ -49,7 +50,7 @@ def handle_exception(fn):
|
|
49
50
|
if isinstance(e, FileNotFoundError): raise HTTPException(status_code=404, detail=str(e))
|
50
51
|
if isinstance(e, FileExistsError): raise HTTPException(status_code=409, detail=str(e))
|
51
52
|
logger.error(f"Uncaptured error in {fn.__name__}: {e}")
|
52
|
-
raise
|
53
|
+
raise
|
53
54
|
return wrapper
|
54
55
|
|
55
56
|
async def get_credential_from_params(request: Request):
|
@@ -118,9 +119,59 @@ async def log_requests(request: Request, call_next):
|
|
118
119
|
|
119
120
|
router_fs = APIRouter(prefix="")
|
120
121
|
|
122
|
+
async def emit_thumbnail(
|
123
|
+
path: str, download: bool,
|
124
|
+
create_time: Optional[str] = None
|
125
|
+
):
|
126
|
+
if path.endswith("/"):
|
127
|
+
fname = path.split("/")[-2]
|
128
|
+
else:
|
129
|
+
fname = path.split("/")[-1]
|
130
|
+
thumb_blob, mime_type = await get_thumb(path)
|
131
|
+
disp = "inline" if not download else "attachment"
|
132
|
+
headers = {
|
133
|
+
"Content-Disposition": f"{disp}; filename={fname}.thumb.jpg",
|
134
|
+
"Content-Length": str(len(thumb_blob)),
|
135
|
+
}
|
136
|
+
if create_time is not None:
|
137
|
+
headers["Last-Modified"] = format_last_modified(create_time)
|
138
|
+
return Response(
|
139
|
+
content=thumb_blob, media_type=mime_type, headers=headers
|
140
|
+
)
|
141
|
+
async def emit_file(
|
142
|
+
file_record: FileRecord,
|
143
|
+
media_type: Optional[str] = None,
|
144
|
+
disposition = "attachment"
|
145
|
+
):
|
146
|
+
if media_type is None:
|
147
|
+
media_type = file_record.mime_type
|
148
|
+
path = file_record.url
|
149
|
+
fname = path.split("/")[-1]
|
150
|
+
if not file_record.external:
|
151
|
+
fblob = await db.read_file(path)
|
152
|
+
return Response(
|
153
|
+
content=fblob, media_type=media_type, headers={
|
154
|
+
"Content-Disposition": f"{disposition}; filename={fname}",
|
155
|
+
"Content-Length": str(len(fblob)),
|
156
|
+
"Last-Modified": format_last_modified(file_record.create_time)
|
157
|
+
}
|
158
|
+
)
|
159
|
+
else:
|
160
|
+
return StreamingResponse(
|
161
|
+
await db.read_file_stream(path), media_type=media_type, headers={
|
162
|
+
"Content-Disposition": f"{disposition}; filename={fname}",
|
163
|
+
"Content-Length": str(file_record.file_size),
|
164
|
+
"Last-Modified": format_last_modified(file_record.create_time)
|
165
|
+
}
|
166
|
+
)
|
167
|
+
|
121
168
|
@router_fs.get("/{path:path}")
|
122
169
|
@handle_exception
|
123
|
-
async def get_file(
|
170
|
+
async def get_file(
|
171
|
+
path: str,
|
172
|
+
download: bool = False, flat: bool = False, thumb: bool = False,
|
173
|
+
user: UserRecord = Depends(get_current_user)
|
174
|
+
):
|
124
175
|
path = ensure_uri_compnents(path)
|
125
176
|
|
126
177
|
# handle directory query
|
@@ -131,6 +182,9 @@ async def get_file(path: str, download: bool = False, flat: bool = False, user:
|
|
131
182
|
fconn = FileConn(conn)
|
132
183
|
if user.id == 0:
|
133
184
|
raise HTTPException(status_code=401, detail="Permission denied, credential required")
|
185
|
+
if thumb:
|
186
|
+
return await emit_thumbnail(path, download, create_time=None)
|
187
|
+
|
134
188
|
if path == "/":
|
135
189
|
if flat:
|
136
190
|
raise HTTPException(status_code=400, detail="Flat query not supported for root path")
|
@@ -145,6 +199,7 @@ async def get_file(path: str, download: bool = False, flat: bool = False, user:
|
|
145
199
|
|
146
200
|
return await fconn.list_path(path, flat = flat)
|
147
201
|
|
202
|
+
# handle file query
|
148
203
|
async with unique_cursor() as conn:
|
149
204
|
fconn = FileConn(conn)
|
150
205
|
file_record = await fconn.get_file_record(path)
|
@@ -159,32 +214,13 @@ async def get_file(path: str, download: bool = False, flat: bool = False, user:
|
|
159
214
|
if not allow_access:
|
160
215
|
raise HTTPException(status_code=403, detail=reason)
|
161
216
|
|
162
|
-
|
163
|
-
|
164
|
-
if media_type is None:
|
165
|
-
media_type = file_record.mime_type
|
166
|
-
if not file_record.external:
|
167
|
-
fblob = await db.read_file(path)
|
168
|
-
return Response(
|
169
|
-
content=fblob, media_type=media_type, headers={
|
170
|
-
"Content-Disposition": f"{disposition}; filename={fname}",
|
171
|
-
"Content-Length": str(len(fblob)),
|
172
|
-
"Last-Modified": format_last_modified(file_record.create_time)
|
173
|
-
}
|
174
|
-
)
|
175
|
-
else:
|
176
|
-
return StreamingResponse(
|
177
|
-
await db.read_file_stream(path), media_type=media_type, headers={
|
178
|
-
"Content-Disposition": f"{disposition}; filename={fname}",
|
179
|
-
"Content-Length": str(file_record.file_size),
|
180
|
-
"Last-Modified": format_last_modified(file_record.create_time)
|
181
|
-
}
|
182
|
-
)
|
183
|
-
|
184
|
-
if download:
|
185
|
-
return await send('application/octet-stream', "attachment")
|
217
|
+
if thumb:
|
218
|
+
return await emit_thumbnail(path, download, create_time=file_record.create_time)
|
186
219
|
else:
|
187
|
-
|
220
|
+
if download:
|
221
|
+
return await emit_file(file_record, 'application/octet-stream', "attachment")
|
222
|
+
else:
|
223
|
+
return await emit_file(file_record, None, "inline")
|
188
224
|
|
189
225
|
@router_fs.put("/{path:path}")
|
190
226
|
@handle_exception
|
lfss/src/thumb.py
ADDED
@@ -0,0 +1,121 @@
|
|
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"
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lfss
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.12
|
4
4
|
Summary: Lightweight file storage service
|
5
5
|
Home-page: https://github.com/MenxLi/lfss
|
6
6
|
Author: li, mengxun
|
@@ -15,6 +15,7 @@ Requires-Dist: aiofiles (==23.*)
|
|
15
15
|
Requires-Dist: aiosqlite (==0.*)
|
16
16
|
Requires-Dist: fastapi (==0.*)
|
17
17
|
Requires-Dist: mimesniff (==1.*)
|
18
|
+
Requires-Dist: pillow
|
18
19
|
Requires-Dist: uvicorn (==0.*)
|
19
20
|
Project-URL: Repository, https://github.com/MenxLi/lfss
|
20
21
|
Description-Content-Type: text/markdown
|
@@ -7,30 +7,32 @@ frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
|
|
7
7
|
frontend/info.js,sha256=WhOGaeqMoezEAfg4nIpK26hvejC7AZ-ZDLiJmRj0kDk,5758
|
8
8
|
frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
|
9
9
|
frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
|
10
|
-
frontend/scripts.js,sha256=
|
11
|
-
frontend/styles.css,sha256=
|
12
|
-
frontend/utils.js,sha256=
|
10
|
+
frontend/scripts.js,sha256=4LHc1M2QwxtCJsbShxXu0uKYBY8auv84JuYcd1zwpQM,21069
|
11
|
+
frontend/styles.css,sha256=ghtQLSi7-zS8Yuac49TNiGVxNgggh-R3SM7HDYQM97c,4482
|
12
|
+
frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
|
13
13
|
lfss/cli/balance.py,sha256=R2rbO2tg9TVnnQIVeU0GJVeMS-5LDhEdk4mbOE9qGq0,4121
|
14
14
|
lfss/cli/cli.py,sha256=LH1nx5wI1K2DZ3hvHz7oq5HcXVDoW2V6sr7q9gJ8gqo,4621
|
15
15
|
lfss/cli/panel.py,sha256=iGdVmdWYjA_7a78ZzWEB_3ggIOBeUKTzg6F5zLaB25c,1401
|
16
16
|
lfss/cli/serve.py,sha256=T-jz_PJcaY9FJRRfyWV9MbjDI73YdOOCn4Nr2PO-s0c,993
|
17
17
|
lfss/cli/user.py,sha256=ETLtj0N-kmxv0mhmeAsO6cY7kPq7nOOP4DetxIRoQpQ,3405
|
18
18
|
lfss/cli/vacuum.py,sha256=4TMMYC_5yEt7jeaFTVC3iX0X6aUzYXBiCsrcIYgw_uA,3695
|
19
|
-
lfss/client/__init__.py,sha256=
|
20
|
-
lfss/client/api.py,sha256=
|
19
|
+
lfss/client/__init__.py,sha256=kcClDdHaQowAhHFYdrFyFqHIOe5MEjfENYq1icuj3Mg,4577
|
20
|
+
lfss/client/api.py,sha256=aHorSuxl79xvRRfwuitXANYoTogWKVO11XX7mdrhHTE,5807
|
21
21
|
lfss/sql/init.sql,sha256=C-JtQAlaOjESI8uoF1Y_9dKukEVSw5Ll-7yA3gG-XHU,1210
|
22
22
|
lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
|
23
23
|
lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
24
|
+
lfss/src/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
|
24
25
|
lfss/src/config.py,sha256=2qN2nu3onP1M7U6ENC0ZbVVRE90aDomTcQ1GsBLLHF8,800
|
25
26
|
lfss/src/connection_pool.py,sha256=r4Ho5d_Gd4S_KbT7515UJoiyfIgS6xyttqMsKqOfaIg,5190
|
26
27
|
lfss/src/database.py,sha256=w2QPE3h1Lx0D0fUmdtu9s1XHpNp9p27zqm8AVeP2UVg,32476
|
27
28
|
lfss/src/datatype.py,sha256=WfrLALU_7wei5-i_b0TxY8xWI5mwxLUHFepHSps49zA,1767
|
28
29
|
lfss/src/error.py,sha256=imbhwnbhnI3HLhkbfICROe3F0gleKrOk4XnqHJDOtuI,285
|
29
30
|
lfss/src/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
|
30
|
-
lfss/src/server.py,sha256=
|
31
|
+
lfss/src/server.py,sha256=hFUeg14-G2nIfY2sD9fL_3YvRv7TuuGQk932HCT_NPs,17224
|
31
32
|
lfss/src/stat.py,sha256=Wr-ug_JqtbSIf3XwQnv1xheGhsDTEOlLWuYoKO_26Jo,3201
|
33
|
+
lfss/src/thumb.py,sha256=KnKvjP3RwhjqFZxZabdUTuk5l9fv3AXO2YUgu0Xzvps,6770
|
32
34
|
lfss/src/utils.py,sha256=TBGYvgt6xMP8UC5wTGHAr9fmdhu0_gjOtxcSeyvGyVM,3918
|
33
|
-
lfss-0.7.
|
34
|
-
lfss-0.7.
|
35
|
-
lfss-0.7.
|
36
|
-
lfss-0.7.
|
35
|
+
lfss-0.7.12.dist-info/METADATA,sha256=bFSR41dbc7Y_Ui4ALbYEqbCjvmpvh4WNBmonJ3Pn868,2021
|
36
|
+
lfss-0.7.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
37
|
+
lfss-0.7.12.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
|
38
|
+
lfss-0.7.12.dist-info/RECORD,,
|
File without changes
|
File without changes
|