lfss 0.7.11__tar.gz → 0.7.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {lfss-0.7.11 → lfss-0.7.12}/PKG-INFO +2 -1
  2. {lfss-0.7.11 → lfss-0.7.12}/frontend/scripts.js +16 -3
  3. {lfss-0.7.11 → lfss-0.7.12}/frontend/styles.css +13 -0
  4. {lfss-0.7.11 → lfss-0.7.12}/frontend/utils.js +7 -0
  5. {lfss-0.7.11 → lfss-0.7.12}/lfss/client/__init__.py +3 -3
  6. {lfss-0.7.11 → lfss-0.7.12}/lfss/client/api.py +3 -2
  7. lfss-0.7.12/lfss/src/bounded_pool.py +44 -0
  8. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/server.py +63 -27
  9. lfss-0.7.12/lfss/src/thumb.py +121 -0
  10. {lfss-0.7.11 → lfss-0.7.12}/pyproject.toml +2 -1
  11. {lfss-0.7.11 → lfss-0.7.12}/Readme.md +0 -0
  12. {lfss-0.7.11 → lfss-0.7.12}/docs/Known_issues.md +0 -0
  13. {lfss-0.7.11 → lfss-0.7.12}/docs/Permission.md +0 -0
  14. {lfss-0.7.11 → lfss-0.7.12}/frontend/api.js +0 -0
  15. {lfss-0.7.11 → lfss-0.7.12}/frontend/index.html +0 -0
  16. {lfss-0.7.11 → lfss-0.7.12}/frontend/info.css +0 -0
  17. {lfss-0.7.11 → lfss-0.7.12}/frontend/info.js +0 -0
  18. {lfss-0.7.11 → lfss-0.7.12}/frontend/popup.css +0 -0
  19. {lfss-0.7.11 → lfss-0.7.12}/frontend/popup.js +0 -0
  20. {lfss-0.7.11 → lfss-0.7.12}/lfss/cli/balance.py +0 -0
  21. {lfss-0.7.11 → lfss-0.7.12}/lfss/cli/cli.py +0 -0
  22. {lfss-0.7.11 → lfss-0.7.12}/lfss/cli/panel.py +0 -0
  23. {lfss-0.7.11 → lfss-0.7.12}/lfss/cli/serve.py +0 -0
  24. {lfss-0.7.11 → lfss-0.7.12}/lfss/cli/user.py +0 -0
  25. {lfss-0.7.11 → lfss-0.7.12}/lfss/cli/vacuum.py +0 -0
  26. {lfss-0.7.11 → lfss-0.7.12}/lfss/sql/init.sql +0 -0
  27. {lfss-0.7.11 → lfss-0.7.12}/lfss/sql/pragma.sql +0 -0
  28. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/__init__.py +0 -0
  29. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/config.py +0 -0
  30. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/connection_pool.py +0 -0
  31. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/database.py +0 -0
  32. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/datatype.py +0 -0
  33. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/error.py +0 -0
  34. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/log.py +0 -0
  35. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/stat.py +0 -0
  36. {lfss-0.7.11 → lfss-0.7.12}/lfss/src/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.7.11
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
@@ -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
- nameTd.appendChild(dirLink);
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
- nameTd.textContent = fileName;
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
  }
@@ -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;
@@ -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
  }
@@ -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 ThreadPoolExecutor(n_concurrent) as executor:
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 ThreadPoolExecutor(n_concurrent) as executor:
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
@@ -31,8 +31,9 @@ class Connector:
31
31
  headers.update({
32
32
  'Authorization': f"Bearer {self.config['token']}",
33
33
  })
34
- response = requests.request(method, url, headers=headers, **kwargs)
35
- response.raise_for_status()
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
 
@@ -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
+
@@ -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 HTTPException(status_code=500, detail=str(e))
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(path: str, download: bool = False, flat: bool = False, user: UserRecord = Depends(get_current_user)):
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
- fname = path.split("/")[-1]
163
- async def send(media_type: Optional[str] = None, disposition = "attachment"):
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
- return await send(None, "inline")
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
@@ -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
  [tool.poetry]
2
2
  name = "lfss"
3
- version = "0.7.11"
3
+ version = "0.7.12"
4
4
  description = "Lightweight file storage service"
5
5
  authors = ["li, mengxun <limengxun45@outlook.com>"]
6
6
  readme = "Readme.md"
@@ -15,6 +15,7 @@ aiofiles = "23.*"
15
15
  mimesniff = "1.*"
16
16
  fastapi = "0.*"
17
17
  uvicorn = "0.*"
18
+ pillow = "*"
18
19
 
19
20
  [tool.poetry.dev-dependencies]
20
21
  pytest = "*"
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