lfss 0.5.1__py3-none-any.whl → 0.5.2__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/api.js CHANGED
@@ -191,7 +191,7 @@ export default class Connector {
191
191
  * @param {string} path - file path(url)
192
192
  * @param {string} newPath - new file path(url)
193
193
  */
194
- async moveFile(path, newPath){
194
+ async move(path, newPath){
195
195
  if (path.startsWith('/')){ path = path.slice(1); }
196
196
  if (newPath.startsWith('/')){ newPath = newPath.slice(1); }
197
197
  const dst = new URL(this.config.endpoint + '/_api/meta');
frontend/popup.js CHANGED
@@ -40,7 +40,7 @@ export function createFloatingWindow(innerHTML = '', {
40
40
  return [floatingWindow, closeWindow];
41
41
  }
42
42
 
43
- /* select can be "last-filename" */
43
+ /* select can be "last-filename" or "last-pathname" */
44
44
  export function showFloatingWindowLineInput(onSubmit = (v) => {}, {
45
45
  text = "",
46
46
  placeholder = "Enter text",
@@ -72,6 +72,7 @@ export function showFloatingWindowLineInput(onSubmit = (v) => {}, {
72
72
  };
73
73
 
74
74
  if (select === "last-filename") {
75
+ // select the last filename, e.g. "file" in "/path/to/file.txt"
75
76
  const inputVal = input.value;
76
77
  let lastSlash = inputVal.lastIndexOf("/");
77
78
  if (lastSlash === -1) {
@@ -84,6 +85,17 @@ export function showFloatingWindowLineInput(onSubmit = (v) => {}, {
84
85
  }
85
86
  input.setSelectionRange(lastSlash + 1, lastSlash + lastDot + 1);
86
87
  }
88
+ else if (select === "last-pathname") {
89
+ // select the last pathname, e.g. "to" in "/path/to/<filename>"
90
+ const lastSlash = input.value.lastIndexOf("/");
91
+ const secondLastSlash = input.value.lastIndexOf("/", input.value.lastIndexOf("/") - 1);
92
+ if (secondLastSlash !== -1) {
93
+ input.setSelectionRange(secondLastSlash + 1, lastSlash);
94
+ }
95
+ else {
96
+ input.setSelectionRange(0, lastSlash);
97
+ }
98
+ }
87
99
 
88
100
  return [floatingWindow, closeWindow];
89
101
  }
frontend/scripts.js CHANGED
@@ -5,6 +5,9 @@ import { formatSize, decodePathURI, ensurePathURI, copyToClipboard, getRandomStr
5
5
 
6
6
  const conn = new Connector();
7
7
  let userRecord = null;
8
+ const ensureSlashEnd = (path) => {
9
+ return path.endsWith('/') ? path : path + '/';
10
+ }
8
11
 
9
12
  const endpointInput = document.querySelector('input#endpoint');
10
13
  const tokenInput = document.querySelector('input#token');
@@ -128,11 +131,13 @@ uploadButton.addEventListener('click', () => {
128
131
  throw new Error('File name cannot end with /');
129
132
  }
130
133
  path = path + fileName;
134
+ showPopup('Uploading...', {level: 'info', timeout: 3000});
131
135
  conn.put(path, file)
132
136
  .then(() => {
133
137
  refreshFileList();
134
138
  uploadFileNameInput.value = '';
135
139
  onFileNameInpuChange();
140
+ showPopup('Upload success.', {level: 'success', timeout: 3000});
136
141
  },
137
142
  (err) => {
138
143
  showPopup('Failed to upload file: ' + err, {level: 'error', timeout: 5000});
@@ -191,9 +196,14 @@ Are you sure you want to proceed?
191
196
  const path = dstPath + file.name;
192
197
  promises.push(uploadFile(file, path));
193
198
  }
199
+ showPopup('Uploading multiple files...', {level: 'info', timeout: 3000});
194
200
  Promise.all(promises).then(
195
201
  () => {
202
+ showPopup('Upload success.', {level: 'success', timeout: 3000});
196
203
  refreshFileList();
204
+ },
205
+ (err) => {
206
+ showPopup('Failed to upload some files: ' + err, {level: 'error', timeout: 5000});
197
207
  }
198
208
  );
199
209
  }
@@ -260,15 +270,16 @@ function refreshFileList(){
260
270
  tr.appendChild(accessTd);
261
271
  }
262
272
  {
273
+ const dirurl = ensureSlashEnd(dir.url);
263
274
  const actTd = document.createElement('td');
264
275
  const actContainer = document.createElement('div');
265
276
  actContainer.classList.add('action-container');
266
277
 
267
278
  const showMetaButton = document.createElement('a');
268
- showMetaButton.textContent = 'Details';
279
+ showMetaButton.textContent = 'Reveal';
269
280
  showMetaButton.style.cursor = 'pointer';
270
281
  showMetaButton.addEventListener('click', () => {
271
- const dirUrlEncap = dir.url + (dir.url.endsWith('/') ? '' : '/');
282
+ const dirUrlEncap = dirurl;
272
283
  conn.getMetadata(dirUrlEncap).then(
273
284
  (meta) => {
274
285
  sizeTd.textContent = formatSize(meta.size);
@@ -280,6 +291,30 @@ function refreshFileList(){
280
291
  });
281
292
  actContainer.appendChild(showMetaButton);
282
293
 
294
+ const moveButton = document.createElement('a');
295
+ moveButton.textContent = 'Move';
296
+ moveButton.style.cursor = 'pointer';
297
+ moveButton.addEventListener('click', () => {
298
+ showFloatingWindowLineInput((dstPath) => {
299
+ dstPath = encodePathURI(dstPath);
300
+ console.log("Moving", dirurl, "to", dstPath);
301
+ conn.move(dirurl, dstPath)
302
+ .then(() => {
303
+ refreshFileList();
304
+ },
305
+ (err) => {
306
+ showPopup('Failed to move path: ' + err, {level: 'error'});
307
+ }
308
+ );
309
+ }, {
310
+ text: 'Enter the destination path: ',
311
+ placeholder: 'Destination path',
312
+ value: decodePathURI(dirurl),
313
+ select: "last-pathname"
314
+ });
315
+ });
316
+ actContainer.appendChild(moveButton);
317
+
283
318
  const downloadButton = document.createElement('a');
284
319
  downloadButton.textContent = 'Download';
285
320
  downloadButton.href = conn.config.endpoint + '/_api/bundle?' +
@@ -402,10 +437,7 @@ function refreshFileList(){
402
437
  moveButton.addEventListener('click', () => {
403
438
  showFloatingWindowLineInput((dstPath) => {
404
439
  dstPath = encodePathURI(dstPath);
405
- if (dstPath.endsWith('/')){
406
- dstPath = dstPath.slice(0, -1);
407
- }
408
- conn.moveFile(file.url, dstPath)
440
+ conn.move(file.url, dstPath)
409
441
  .then(() => {
410
442
  refreshFileList();
411
443
  },
frontend/styles.css CHANGED
@@ -192,7 +192,7 @@ input#file-name.duplicate{
192
192
  display: flex;
193
193
  flex-direction: row;
194
194
  gap: 10px;
195
- /* justify-content: flex-end; */
195
+ justify-content: flex-end;
196
196
  }
197
197
  a{
198
198
  color: #195f8b;
lfss/cli/balance.py CHANGED
@@ -68,44 +68,57 @@ async def move_to_internal(f_id: str, flag: str = ''):
68
68
  raise e
69
69
 
70
70
 
71
- async def _main():
71
+ async def _main(batch_size: int = 10000):
72
72
 
73
73
  tasks = []
74
74
  start_time = time.time()
75
- async with aiosqlite.connect(db_file) as conn:
76
- exceeded_rows = await (await conn.execute(
77
- "SELECT file_id FROM fmeta WHERE file_size > ? AND external = 0",
78
- (LARGE_FILE_BYTES,)
79
- )).fetchall()
80
-
81
- for i in range(0, len(exceeded_rows)):
82
- row = exceeded_rows[i]
83
- f_id = row[0]
84
- tasks.append(move_to_external(f_id, flag=f"[e-{i+1}/{len(exceeded_rows)}] "))
85
75
 
86
- async with aiosqlite.connect(db_file) as conn:
87
- under_rows = await (await conn.execute(
88
- "SELECT file_id, file_size, external FROM fmeta WHERE file_size <= ? AND external = 1",
89
- (LARGE_FILE_BYTES,)
90
- )).fetchall()
76
+ e_cout = 0
77
+ batch_count = 0
78
+ while True:
79
+ async with aiosqlite.connect(db_file) as conn:
80
+ exceeded_rows = list(await (await conn.execute(
81
+ "SELECT file_id FROM fmeta WHERE file_size > ? AND external = 0 LIMIT ? OFFSET ?",
82
+ (LARGE_FILE_BYTES, batch_size, batch_size * batch_count)
83
+ )).fetchall())
84
+ if not exceeded_rows:
85
+ break
86
+ e_cout += len(exceeded_rows)
87
+ for i in range(0, len(exceeded_rows)):
88
+ row = exceeded_rows[i]
89
+ f_id = row[0]
90
+ tasks.append(move_to_external(f_id, flag=f"[b{batch_count+1}-e{i+1}/{len(exceeded_rows)}] "))
91
+ await asyncio.gather(*tasks)
92
+
93
+ i_count = 0
94
+ batch_count = 0
95
+ while True:
96
+ async with aiosqlite.connect(db_file) as conn:
97
+ under_rows = list(await (await conn.execute(
98
+ "SELECT file_id, file_size, external FROM fmeta WHERE file_size <= ? AND external = 1 LIMIT ? OFFSET ?",
99
+ (LARGE_FILE_BYTES, batch_size, batch_size * batch_count)
100
+ )).fetchall())
101
+ if not under_rows:
102
+ break
103
+ i_count += len(under_rows)
104
+ for i in range(0, len(under_rows)):
105
+ row = under_rows[i]
106
+ f_id = row[0]
107
+ tasks.append(move_to_internal(f_id, flag=f"[b{batch_count+1}-i{i+1}/{len(under_rows)}] "))
108
+ await asyncio.gather(*tasks)
91
109
 
92
- for i in range(0, len(under_rows)):
93
- row = under_rows[i]
94
- f_id = row[0]
95
- tasks.append(move_to_internal(f_id, flag=f"[i-{i+1}/{len(under_rows)}] "))
96
-
97
- await asyncio.gather(*tasks)
98
110
  end_time = time.time()
99
111
  print(f"Balancing complete, took {end_time - start_time:.2f} seconds. "
100
- f"{len(exceeded_rows)} files moved to external storage, {len(under_rows)} files moved to internal storage.")
112
+ f"{e_cout} files moved to external storage, {i_count} files moved to internal storage.")
101
113
 
102
114
  def main():
103
115
  global sem
104
116
  parser = argparse.ArgumentParser(description="Balance the storage by ensuring that large file thresholds are met.")
105
117
  parser.add_argument("-j", "--jobs", type=int, default=2, help="Number of concurrent jobs")
118
+ parser.add_argument("-b", "--batch-size", type=int, default=10000, help="Batch size for processing files")
106
119
  args = parser.parse_args()
107
120
  sem = Semaphore(args.jobs)
108
- asyncio.run(_main())
121
+ asyncio.run(_main(args.batch_size))
109
122
 
110
123
  if __name__ == '__main__':
111
124
  main()
lfss/client/api.py CHANGED
@@ -36,8 +36,6 @@ class Connector:
36
36
 
37
37
  def put(self, path: str, file_data: bytes, permission: int | FileReadPermission = 0, conflict: Literal['overwrite', 'abort', 'skip'] = 'abort'):
38
38
  """Uploads a file to the specified path."""
39
- if path.startswith('/'):
40
- path = path[1:]
41
39
  response = self._fetch('PUT', path, search_params={
42
40
  'permission': int(permission),
43
41
  'conflict': conflict
@@ -47,20 +45,41 @@ class Connector:
47
45
  )
48
46
  return response.json()
49
47
 
50
- def get(self, path: str) -> Optional[bytes]:
51
- """Downloads a file from the specified path."""
48
+ def put_json(self, path: str, data: dict, permission: int | FileReadPermission = 0, conflict: Literal['overwrite', 'abort', 'skip'] = 'abort'):
49
+ """Uploads a JSON file to the specified path."""
50
+ assert path.endswith('.json'), "Path must end with .json"
51
+ response = self._fetch('PUT', path, search_params={
52
+ 'permission': int(permission),
53
+ 'conflict': conflict
54
+ })(
55
+ json=data,
56
+ headers={'Content-Type': 'application/json'}
57
+ )
58
+ return response.json()
59
+
60
+ def _get(self, path: str) -> Optional[requests.Response]:
52
61
  try:
53
62
  response = self._fetch('GET', path)()
54
63
  except requests.exceptions.HTTPError as e:
55
64
  if e.response.status_code == 404:
56
65
  return None
57
66
  raise e
67
+ return response
68
+
69
+ def get(self, path: str) -> Optional[bytes]:
70
+ """Downloads a file from the specified path."""
71
+ response = self._get(path)
72
+ if response is None: return None
58
73
  return response.content
74
+
75
+ def get_json(self, path: str) -> Optional[dict]:
76
+ response = self._get(path)
77
+ if response is None: return None
78
+ assert response.headers['Content-Type'] == 'application/json'
79
+ return response.json()
59
80
 
60
81
  def delete(self, path: str):
61
82
  """Deletes the file at the specified path."""
62
- if path.startswith('/'):
63
- path = path[1:]
64
83
  self._fetch('DELETE', path)()
65
84
 
66
85
  def get_metadata(self, path: str) -> Optional[FileRecord | DirectoryRecord]:
@@ -87,8 +106,8 @@ class Connector:
87
106
  headers={'Content-Type': 'application/www-form-urlencoded'}
88
107
  )
89
108
 
90
- def move_file(self, path: str, new_path: str):
91
- """Moves a file to a new location."""
109
+ def move(self, path: str, new_path: str):
110
+ """Move file or directory to a new path."""
92
111
  self._fetch('POST', '_api/meta', {'path': path, 'new_path': new_path})(
93
112
  headers = {'Content-Type': 'application/www-form-urlencoded'}
94
113
  )
lfss/src/database.py CHANGED
@@ -331,7 +331,7 @@ class FileConn(DBConnBase):
331
331
  dirs = await asyncio.gather(*[get_dir(url + d) for d in dirs_str])
332
332
  return PathContents(dirs, files)
333
333
 
334
- async def get_path_record(self, url: str) -> Optional[DirectoryRecord]:
334
+ async def get_path_record(self, url: str) -> DirectoryRecord:
335
335
  assert url.endswith('/'), "Path must end with /"
336
336
  async with self.conn.execute("""
337
337
  SELECT MIN(create_time) as create_time,
@@ -421,6 +421,25 @@ class FileConn(DBConnBase):
421
421
  async with self.conn.execute("UPDATE fmeta SET url = ?, create_time = CURRENT_TIMESTAMP WHERE url = ?", (new_url, old_url)):
422
422
  self.logger.info(f"Moved file {old_url} to {new_url}")
423
423
 
424
+ @atomic
425
+ async def move_path(self, old_url: str, new_url: str, conflict_handler: Literal['skip', 'overwrite'] = 'overwrite', user_id: Optional[int] = None):
426
+ assert old_url.endswith('/'), "Old path must end with /"
427
+ assert new_url.endswith('/'), "New path must end with /"
428
+ if user_id is None:
429
+ async with self.conn.execute("SELECT * FROM fmeta WHERE url LIKE ?", (old_url + '%', )) as cursor:
430
+ res = await cursor.fetchall()
431
+ else:
432
+ async with self.conn.execute("SELECT * FROM fmeta WHERE url LIKE ? AND owner_id = ?", (old_url + '%', user_id)) as cursor:
433
+ res = await cursor.fetchall()
434
+ for r in res:
435
+ new_r = new_url + r[0][len(old_url):]
436
+ if conflict_handler == 'overwrite':
437
+ await self.conn.execute("DELETE FROM fmeta WHERE url = ?", (new_r, ))
438
+ elif conflict_handler == 'skip':
439
+ if (await self.conn.execute("SELECT url FROM fmeta WHERE url = ?", (new_r, ))) is not None:
440
+ continue
441
+ await self.conn.execute("UPDATE fmeta SET url = ?, create_time = CURRENT_TIMESTAMP WHERE url = ?", (new_r, r[0]))
442
+
424
443
  async def log_access(self, url: str):
425
444
  await self.conn.execute("UPDATE fmeta SET access_time = CURRENT_TIMESTAMP WHERE url = ?", (url, ))
426
445
 
@@ -664,6 +683,13 @@ class Database:
664
683
 
665
684
  async with transaction(self):
666
685
  await self.file.move_file(old_url, new_url)
686
+
687
+ async def move_path(self, old_url: str, new_url: str, user_id: Optional[int] = None):
688
+ validate_url(old_url, is_file=False)
689
+ validate_url(new_url, is_file=False)
690
+
691
+ async with transaction(self):
692
+ await self.file.move_path(old_url, new_url, 'overwrite', user_id)
667
693
 
668
694
  async def __batch_delete_file_blobs(self, file_records: list[FileRecord], batch_size: int = 512):
669
695
  # https://github.com/langchain-ai/langchain/issues/10321
lfss/src/server.py CHANGED
@@ -263,6 +263,7 @@ async def delete_file(path: str, user: UserRecord = Depends(get_current_user)):
263
263
  else:
264
264
  res = await conn.delete_file(path)
265
265
 
266
+ await conn.user.set_active(user.username)
266
267
  if res:
267
268
  return Response(status_code=200, content="Deleted")
268
269
  else:
@@ -335,26 +336,57 @@ async def update_file_meta(
335
336
  if user.id == 0:
336
337
  raise HTTPException(status_code=401, detail="Permission denied")
337
338
  path = ensure_uri_compnents(path)
338
- file_record = await conn.file.get_file_record(path)
339
- if not file_record:
340
- logger.debug(f"Reject update meta request from {user.username} to {path}")
341
- raise HTTPException(status_code=404, detail="File not found")
342
-
343
- if not (user.is_admin or user.id == file_record.owner_id):
344
- logger.debug(f"Reject update meta request from {user.username} to {path}")
345
- raise HTTPException(status_code=403, detail="Permission denied")
339
+ if path.startswith("/"):
340
+ path = path[1:]
341
+ await conn.user.set_active(user.username)
342
+
343
+ # file
344
+ if not path.endswith("/"):
345
+ file_record = await conn.file.get_file_record(path)
346
+ if not file_record:
347
+ logger.debug(f"Reject update meta request from {user.username} to {path}")
348
+ raise HTTPException(status_code=404, detail="File not found")
349
+
350
+ if not (user.is_admin or user.id == file_record.owner_id):
351
+ logger.debug(f"Reject update meta request from {user.username} to {path}")
352
+ raise HTTPException(status_code=403, detail="Permission denied")
353
+
354
+ if perm is not None:
355
+ logger.info(f"Update permission of {path} to {perm}")
356
+ await conn.file.set_file_record(
357
+ url = file_record.url,
358
+ permission = FileReadPermission(perm)
359
+ )
346
360
 
347
- if perm is not None:
348
- logger.info(f"Update permission of {path} to {perm}")
349
- await conn.file.set_file_record(
350
- url = file_record.url,
351
- permission = FileReadPermission(perm)
352
- )
361
+ if new_path is not None:
362
+ new_path = ensure_uri_compnents(new_path)
363
+ logger.info(f"Update path of {path} to {new_path}")
364
+ await conn.move_file(path, new_path)
353
365
 
354
- if new_path is not None:
355
- new_path = ensure_uri_compnents(new_path)
356
- logger.info(f"Update path of {path} to {new_path}")
357
- await conn.move_file(path, new_path)
366
+ # directory
367
+ else:
368
+ assert perm is None, "Permission is not supported for directory"
369
+ if new_path is not None:
370
+ new_path = ensure_uri_compnents(new_path)
371
+ logger.info(f"Update path of {path} to {new_path}")
372
+ assert new_path.endswith("/"), "New path must end with /"
373
+ if new_path.startswith("/"):
374
+ new_path = new_path[1:]
375
+
376
+ # check if new path is under the user's directory
377
+ first_component = new_path.split("/")[0]
378
+ if not (first_component == user.username or user.is_admin):
379
+ raise HTTPException(status_code=403, detail="Permission denied, path must start with username")
380
+ elif user.is_admin:
381
+ _is_user = await conn.user.get_user(first_component)
382
+ if not _is_user:
383
+ raise HTTPException(status_code=404, detail="User not found, path must start with username")
384
+
385
+ # check if old path is under the user's directory (non-admin)
386
+ if not path.startswith(f"{user.username}/") and not user.is_admin:
387
+ raise HTTPException(status_code=403, detail="Permission denied, path must start with username")
388
+ # currently only move own file, with overwrite
389
+ await conn.move_path(path, new_path, user_id = user.id)
358
390
 
359
391
  return Response(status_code=200, content="OK")
360
392
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -1,31 +1,31 @@
1
1
  Readme.md,sha256=8ZAADV1K20gEDhwiL-Qq_rBAePlwWQn8c5uJ_X7OCIg,1128
2
2
  docs/Known_issues.md,sha256=rfdG3j1OJF-59S9E06VPyn0nZKbW-ybPxkoZ7MEZWp8,81
3
3
  docs/Permission.md,sha256=EY1Y4tT5PDdHDb2pXsVgMAJXxkUihTZfPT0_z7Q53FA,1485
4
- frontend/api.js,sha256=NugZGa1qZdCnrzWJi57NPhYm3kHGo7aSjTpTNgfpz0M,7681
4
+ frontend/api.js,sha256=-ouhsmucEunAK3m1H__MqffQkXAjoeVEfM15BvqfIZs,7677
5
5
  frontend/index.html,sha256=VPJDs2LG8ep9kjlsKzjWzpN9vc1VGgdvOUlNTZWyQoQ,2088
6
6
  frontend/popup.css,sha256=VzkjG1ZTLxhHMtTyobnlvqYmVsTmdbJJed2Pu1cc06c,1007
7
- frontend/popup.js,sha256=dH5n7C2Vo9gCsMfQ4ajL4D1ETl0Wk9rIldxUb7x0f_c,4634
8
- frontend/scripts.js,sha256=WUJ_AFlMvR7kYTNjfvZ_1ftpN87rNjuXIxsHoI_2lB8,19492
9
- frontend/styles.css,sha256=kN2XmEeRXLMyZQowSXYPhD4_1XVXgKwTKdTdtAKuX0E,4102
7
+ frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
8
+ frontend/scripts.js,sha256=JkjcyT-IpzSypwI4oWwgY9UDdKkR1ZSYdSc4c6MlukE,21128
9
+ frontend/styles.css,sha256=wly8O-zF4EUgV12Tv1bATSfmJsLITv2u3_SiyXVaxv4,4096
10
10
  frontend/utils.js,sha256=Ts4nlef8pkrEgpwX-uQwAhWvwxlIzex8ijDLNCa22ps,2372
11
- lfss/cli/balance.py,sha256=Vm7ELaFN_DdodXRt4oSAIMaK2WVXsR9sDKX3gCKwUoM,4046
11
+ lfss/cli/balance.py,sha256=5kyjfknN7oNiyEs2DcDYzTMKzoOTDm0caF2nZyxJcHw,4707
12
12
  lfss/cli/cli.py,sha256=-BcLIH-6jCJxZfg48nA3gulnitO59Hnba3HoKqyKeUA,2245
13
13
  lfss/cli/panel.py,sha256=iGdVmdWYjA_7a78ZzWEB_3ggIOBeUKTzg6F5zLaB25c,1401
14
14
  lfss/cli/serve.py,sha256=bO3GT0kuylMGN-7bZWP4e71MlugGZ_lEMkYaYld_Ntg,985
15
15
  lfss/cli/user.py,sha256=uSNgF8wGYpdOowN8Mah3V_ii6NlxRMNecrrVj3HaemU,3328
16
16
  lfss/client/__init__.py,sha256=YttaGTBup7mxnW0CtxdZPF0HWga2cGM4twz9MXJIrts,1827
17
- lfss/client/api.py,sha256=tqFBRXU2yvt9zNMGZZxj0U9FC7qh_PeePJ9ar5slhn4,3859
17
+ lfss/client/api.py,sha256=mGSFZN0i-kdo8nEdL4pfzIKEX1Em09o9cDNMN-x4ECA,4682
18
18
  lfss/sql/init.sql,sha256=DlXb47mZ1I7nvVht55UbBmCr1yJK50sBJvMC7eOzCjk,1148
19
19
  lfss/sql/pragma.sql,sha256=krTf0ALmU_4s2hGM4PSYwmSCqjRXcaOxChmgMEhtJqI,134
20
20
  lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  lfss/src/config.py,sha256=SM6WVmpkf7e-YkV3RELTJltOnQGRtXkVtA1rVHwsg0g,484
22
- lfss/src/database.py,sha256=KD2kMg7RfctZSVUGhqMG4nCIsmzC5G29UMdFSWtgQR0,32018
22
+ lfss/src/database.py,sha256=WKl9ZfV54g9ff7vmFivk3FZ2uVHlhPBEx_hFY1jtwzQ,33503
23
23
  lfss/src/error.py,sha256=imbhwnbhnI3HLhkbfICROe3F0gleKrOk4XnqHJDOtuI,285
24
24
  lfss/src/log.py,sha256=qNE04sHoZ2rMbQ5dR2zT7Xaz1KVAfYp5hKpWJX42S9g,5244
25
- lfss/src/server.py,sha256=wAfjlWLnBYksxlyGmC9mIJ5To-fwQg9a2nNDPyb7N_M,14636
25
+ lfss/src/server.py,sha256=oBN32zoUGEfy3eA_XBir7_iviPELZJVHfKnIvDqJgEc,16246
26
26
  lfss/src/stat.py,sha256=hTMtQyM_Ukmhc33Bb9FGCfBMIX02KrGHQg8nL7sC8sU,2082
27
27
  lfss/src/utils.py,sha256=8VkrtpSmurbMiX7GyK-n7Grvzy3uwSJXHdONEsuLCDI,2272
28
- lfss-0.5.1.dist-info/METADATA,sha256=vagdtvSFseeofRbwIvX_MdLxItdYlkN1a0rLwj6gwMk,1820
29
- lfss-0.5.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
30
- lfss-0.5.1.dist-info/entry_points.txt,sha256=d_Ri3GXxUW-S0E6q953A8od0YMmUAnZGlJSKS46OiW8,172
31
- lfss-0.5.1.dist-info/RECORD,,
28
+ lfss-0.5.2.dist-info/METADATA,sha256=FwF3kDcWgvuU378v1LO9lZowFbHUlezY_fTMROkiTQs,1820
29
+ lfss-0.5.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
30
+ lfss-0.5.2.dist-info/entry_points.txt,sha256=d_Ri3GXxUW-S0E6q953A8od0YMmUAnZGlJSKS46OiW8,172
31
+ lfss-0.5.2.dist-info/RECORD,,
File without changes