lfss 0.7.13__py3-none-any.whl → 0.7.15__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/index.html CHANGED
@@ -30,7 +30,7 @@
30
30
  <label></label>
31
31
  </div>
32
32
  <div>
33
- <span style="margin-right: 0.25rem; color: #999;">
33
+ <span style="margin-right: 0.25rem; color: #999; font-size: small;">
34
34
  Sort by</span>
35
35
  <select id="sort-by-sel">
36
36
  <option value="none">None</option>
frontend/scripts.js CHANGED
@@ -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
- const conn = new Connector();
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.config.endpoint = endpointInput.value;
30
- conn.config.token = tokenInput.value;
30
+ const conn = store.conn;
31
+ store.init();
31
32
 
32
33
  {
33
- const endpoint = window.localStorage.getItem('endpoint');
34
- if (endpoint){
35
- endpointInput.value = endpoint;
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
- window.localStorage.setItem('path', pathInput.value);
47
+ store.dirpath = pathInput.value;
56
48
  maybeRefreshFileList();
57
49
  }
58
50
 
59
51
  endpointInput.addEventListener('blur', () => {
60
- conn.config.endpoint = endpointInput.value;
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
- conn.config.token = tokenInput.value;
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(pathInput.value + fileName);
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 = pathInput.value;
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 = pathInput.value + uploadFileNameInput.value;
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
- pathInput.value && pathInput.value.length > 0 && pathInput.value.endsWith('/')
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 (sortBy === 'name'){
217
+ if (store.sortby === 'name'){
230
218
  dirs.sort((a, b) => { return a.url.localeCompare(b.url); });
231
219
  }
232
- if (sortOrder === 'desc'){ dirs.reverse(); }
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 (sortBy === 'name'){
227
+ if (store.sortby === 'name'){
240
228
  files.sort((a, b) => { return a.url.localeCompare(b.url); });
241
229
  }
242
- if (sortBy === 'size'){
230
+ if (store.sortby === 'size'){
243
231
  files.sort((a, b) => { return a.file_size - b.file_size; });
244
232
  }
245
- if (sortBy === 'access'){
233
+ if (store.sortby === 'access'){
246
234
  files.sort((a, b) => { return timestr2num(a.access_time) - timestr2num(b.access_time); });
247
235
  }
248
- if (sortBy === 'create'){
236
+ if (store.sortby === 'create'){
249
237
  files.sort((a, b) => { return timestr2num(a.create_time) - timestr2num(b.create_time); });
250
238
  }
251
- if (sortBy === 'mime'){
239
+ if (store.sortby === 'mime'){
252
240
  files.sort((a, b) => { return a.mime_type.localeCompare(b.mime_type); });
253
241
  }
254
- if (sortOrder === 'desc'){ files.reverse(); }
242
+ if (store.sortorder === 'desc'){ files.reverse(); }
255
243
  }
256
- sortBySelect.addEventListener('change', (elem) => {sortBy = elem.target.value; refreshFileList();});
257
- sortOrderSelect.addEventListener('change', (elem) => {sortOrder = elem.target.value; refreshFileList();});
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(pathInput.value)
248
+ conn.listPath(store.dirpath)
261
249
  .then(data => {
262
250
  pathHintDiv.classList.remove('disconnected');
263
251
  pathHintDiv.classList.add('connected');
264
- pathHintLabel.textContent = pathInput.value;
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 = pathInput.value;
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 (endpointInput.value && tokenInput.value){
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
 
frontend/state.js ADDED
@@ -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
+ };
frontend/styles.css CHANGED
@@ -194,7 +194,6 @@ div.filename-container{
194
194
  flex-direction: row;
195
195
  align-items: center;
196
196
  gap: 0.5rem;
197
- height: 32px;
198
197
  }
199
198
  label#upload-file-prefix{
200
199
  width: 800px;
frontend/thumb.css CHANGED
@@ -1,16 +1,10 @@
1
1
 
2
2
  div.thumb{
3
- height: 32px;
4
- width: 32px;
5
- border-radius: 0.25rem;
6
3
  display: contents;
7
4
  }
8
5
  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);
6
+ height: 32px; /* smaller than backend */
7
+ width: 32px;
8
+ object-fit: contain;
9
+ border-radius: 0.1rem;
16
10
  }
frontend/thumb.js CHANGED
@@ -24,28 +24,34 @@ function getIconSVGFromMimeType(mimeType){
24
24
  if (mimeType.startsWith('image/')){
25
25
  return ICON_IMAGE;
26
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;
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){
50
+ // change icon color
51
+ const color = '#345';
52
+ icon_str = icon_str
53
+ .replace(/<svg/, `<svg fill="${color}"`)
54
+ .replace(/<path/, `<path fill="${color}"`);
49
55
  return 'data:image/svg+xml,' + encodeURIComponent(icon_str);
50
56
  }
51
57
 
@@ -56,13 +62,25 @@ let thumb_counter = 0;
56
62
  * @returns {string}
57
63
  */
58
64
  export function makeThumbHtml(c, r){
65
+ function ensureSlashEnd(url){ return url.endsWith('/')? url : url + '/'; }
59
66
  const token = c.config.token;
60
67
  const mtype = r.mime_type? r.mime_type : 'directory';
61
68
  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
- `;
69
+ const url = mtype == 'directory'? ensureSlashEnd(r.url) : r.url;
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
+ }
68
86
  }
lfss/src/server.py CHANGED
@@ -101,9 +101,11 @@ async def log_requests(request: Request, call_next):
101
101
  response_time = end_time - start_time
102
102
  response.headers["X-Response-Time"] = str(response_time)
103
103
 
104
+ if response.headers.get("X-Skip-Log", None) is not None:
105
+ return response
106
+
104
107
  if response.status_code >= 400:
105
108
  logger_failed_request.error(f"{request.method} {request.url.path} {response.status_code}")
106
-
107
109
  await req_conn.log_request(
108
110
  request_time_stamp,
109
111
  request.method, request.url.path, response.status_code, response_time,
@@ -114,11 +116,20 @@ async def log_requests(request: Request, call_next):
114
116
  response_size = int(response.headers.get("Content-Length", 0))
115
117
  )
116
118
  await req_conn.ensure_commit_once()
117
-
118
119
  return response
119
120
 
121
+ def skip_request_log(fn):
122
+ @wraps(fn)
123
+ async def wrapper(*args, **kwargs):
124
+ response = await fn(*args, **kwargs)
125
+ assert isinstance(response, Response), "Response expected"
126
+ response.headers["X-Skip-Log"] = "1"
127
+ return response
128
+ return wrapper
129
+
120
130
  router_fs = APIRouter(prefix="")
121
131
 
132
+ @skip_request_log
122
133
  async def emit_thumbnail(
123
134
  path: str, download: bool,
124
135
  create_time: Optional[str] = None
@@ -128,7 +139,7 @@ async def emit_thumbnail(
128
139
  else:
129
140
  fname = path.split("/")[-1]
130
141
  if (thumb_res := await get_thumb(path)) is None:
131
- raise HTTPException(status_code=415, detail="Thumbnail not supported")
142
+ return Response(status_code=415, content="Thumbnail not supported")
132
143
  thumb_blob, mime_type = thumb_res
133
144
  disp = "inline" if not download else "attachment"
134
145
  headers = {
lfss/src/thumb.py CHANGED
@@ -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('''
@@ -14,6 +15,7 @@ async def _maybe_init_thumb(c: aiosqlite.Cursor):
14
15
  thumb BLOB
15
16
  )
16
17
  ''')
18
+ await c.execute('CREATE INDEX IF NOT EXISTS thumbs_path_idx ON thumbs (path)')
17
19
 
18
20
  async def _get_cache_thumb(c: aiosqlite.Cursor, path: str, ctime: str) -> Optional[bytes]:
19
21
  res = await c.execute('''
@@ -48,6 +50,13 @@ async def _delete_cache_thumb(c: aiosqlite.Cursor, path: str):
48
50
  ''', (path, ))
49
51
  await c.execute('COMMIT')
50
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
+
51
60
  async def get_thumb(path: str) -> Optional[tuple[bytes, str]]:
52
61
  """
53
62
  returns [image bytes of thumbnail, mime type] if supported,
@@ -57,20 +66,17 @@ async def get_thumb(path: str) -> Optional[tuple[bytes, str]]:
57
66
  if path.endswith('/'):
58
67
  return None
59
68
 
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:
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:
68
74
  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
75
+ raise FileNotFoundError(f'File not found: {path}')
76
+ if not r.mime_type.startswith('image/'):
77
+ return None
73
78
 
79
+ async with cache_cursor() as cur:
74
80
  c_time = r.create_time
75
81
  thumb_blob = await _get_cache_thumb(cur, path, c_time)
76
82
  if thumb_blob is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.7.13
3
+ Version: 0.7.15
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -2,15 +2,16 @@ 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
4
  frontend/api.js,sha256=o1sP4rKxxnM-rebxnlMlPkhPHzKaVW4kZC7B4ufbOK4,8026
5
- frontend/index.html,sha256=29btC2xxSaCBhZ4ozAlK-lC7Q4FcsuRgfAop3S9nBQ8,2844
5
+ frontend/index.html,sha256=i45ilkRCorqXP0bTfMiwT3QmmEF23V34lZJC1nODHLo,2862
6
6
  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=hzX5kX2txIS3x2OLuYjrI3j-T5Na1M-_ktz4Rkxara4,22331
11
- frontend/styles.css,sha256=Xl8FTDS32DQ_dLVX-UIgvAVJlKx8csYEyeNFX5o1eUY,4502
12
- frontend/thumb.css,sha256=uVTt-8gDdgNiNmqJnsTVK1gvixEUabM2SRd3Ed_0Kag,319
13
- frontend/thumb.js,sha256=m0-Nip5bgtipdBz1GtRBk-0aaJ-4wKhlT9rEN_0oBh8,4881
10
+ frontend/scripts.js,sha256=tmA_tG3bLEEqn0jTgZ-DkcEl6egxdrFYJmstb9eZMr0,21862
11
+ frontend/state.js,sha256=Dda-2G4QzyqdxffjJa3Lb7rgJOrg2LvJ3TCRcB8YCrU,1327
12
+ frontend/styles.css,sha256=krMo6Ulroi8pqEq1exQsFEU-FJqT9GzI8vyARiNF11k,4484
13
+ frontend/thumb.css,sha256=1i8wudiMWGwdrnTqs6yrS8YaiPeHPR-A2YqUNJN20Ok,165
14
+ frontend/thumb.js,sha256=6m8bscQpi2sYaLRir2ZeY1H_1ZRKFVss5P28AnLvIVQ,5486
14
15
  frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
15
16
  lfss/cli/balance.py,sha256=R2rbO2tg9TVnnQIVeU0GJVeMS-5LDhEdk4mbOE9qGq0,4121
16
17
  lfss/cli/cli.py,sha256=LH1nx5wI1K2DZ3hvHz7oq5HcXVDoW2V6sr7q9gJ8gqo,4621
@@ -30,11 +31,11 @@ lfss/src/database.py,sha256=w2QPE3h1Lx0D0fUmdtu9s1XHpNp9p27zqm8AVeP2UVg,32476
30
31
  lfss/src/datatype.py,sha256=WfrLALU_7wei5-i_b0TxY8xWI5mwxLUHFepHSps49zA,1767
31
32
  lfss/src/error.py,sha256=imbhwnbhnI3HLhkbfICROe3F0gleKrOk4XnqHJDOtuI,285
32
33
  lfss/src/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
33
- lfss/src/server.py,sha256=d5PdRXR2LefvbVpBk7qC-MCBXP_Z3hGL_vBceSznpw4,17344
34
+ lfss/src/server.py,sha256=igkPC3gdJoIqcVTKBAKkVPRrclXR2ZNBdRIAEci4xMo,17717
34
35
  lfss/src/stat.py,sha256=Wr-ug_JqtbSIf3XwQnv1xheGhsDTEOlLWuYoKO_26Jo,3201
35
- lfss/src/thumb.py,sha256=QhFmPCBZ5fDwtNC07kt-PKttDVNtx2x0HFXphORlkes,3028
36
+ lfss/src/thumb.py,sha256=qjCNMpnCozMuzkhm-2uAYy1eAuYTeWG6xqs-13HX-7k,3266
36
37
  lfss/src/utils.py,sha256=TBGYvgt6xMP8UC5wTGHAr9fmdhu0_gjOtxcSeyvGyVM,3918
37
- lfss-0.7.13.dist-info/METADATA,sha256=M5e-IMIsEuQaVHXwm3osFRM-yzdk6PDod6uTZnImyVI,2021
38
- lfss-0.7.13.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
39
- lfss-0.7.13.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
40
- lfss-0.7.13.dist-info/RECORD,,
38
+ lfss-0.7.15.dist-info/METADATA,sha256=s6jCttzD9bRauwKiKGNgrBouK5UDNEUMTgDHZBY_VfY,2021
39
+ lfss-0.7.15.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
40
+ lfss-0.7.15.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
41
+ lfss-0.7.15.dist-info/RECORD,,
File without changes