lfss 0.7.7__py3-none-any.whl → 0.7.9__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
@@ -28,6 +28,7 @@
28
28
  * @property {string} size - the size of the directory, in bytes
29
29
  * @property {string} create_time - the time the directory was created
30
30
  * @property {string} access_time - the time the directory was last accessed
31
+ * @property {number} n_files - the number of total files in the directory, including subdirectories
31
32
  *
32
33
  * @typedef {Object} PathListResponse
33
34
  * @property {DirectoryRecord[]} dirs - the list of directories in the directory
frontend/info.js CHANGED
@@ -19,28 +19,28 @@ export function showInfoPanel(r, u){
19
19
  <div class="info-container-left">
20
20
  <table class="info-table">
21
21
  <tr>
22
- <td class="info-table-key">Filename</td>
22
+ <td class="info-table-key">Name</td>
23
23
  <td class="info-table-value">${decodePathURI(r.url).split('/').pop()}</td>
24
24
  </tr>
25
+ <tr>
26
+ <td class="info-table-key">Size</td>
27
+ <td class="info-table-value">${formatSize(r.file_size)}</td>
28
+ </tr>
25
29
  <tr>
26
30
  <td class="info-table-key">File-Type</td>
27
31
  <td class="info-table-value">${r.mime_type}</td>
28
32
  </tr>
29
33
  <tr>
30
- <td class="info-table-key">Size</td>
31
- <td class="info-table-value">${formatSize(r.file_size)}</td>
34
+ <td class="info-table-key">Owner-ID</td>
35
+ <td class="info-table-value">${r.owner_id}</td>
32
36
  </tr>
33
37
  <tr>
34
- <td class="info-table-key">Owner</td>
35
- <td class="info-table-value">${r.owner_id}</td>
38
+ <td class="info-table-key">Access-Time</td>
39
+ <td class="info-table-value">${cvtGMT2Local(r.access_time)}</td>
36
40
  </tr>
37
41
  <tr>
38
42
  <td class="info-table-key">Create-Time</td>
39
43
  <td class="info-table-value">${cvtGMT2Local(r.create_time)}</td>
40
- </td>
41
- <tr>
42
- <td class="info-table-key">Access-Time</td>
43
- <td class="info-table-value">${cvtGMT2Local(r.access_time)}</td>
44
44
  </tr>
45
45
  </table>
46
46
  </div>
@@ -82,13 +82,17 @@ export function showDirInfoPanel(r, u, c){
82
82
  <div class="info-container-left">
83
83
  <table class="info-table">
84
84
  <tr>
85
- <td class="info-table-key">Pathname</td>
85
+ <td class="info-table-key">Name</td>
86
86
  <td class="info-table-value" id="info-table-pathname">${fmtPath.split('/').pop()}</td>
87
87
  </tr>
88
88
  <tr>
89
89
  <td class="info-table-key">Size</td>
90
90
  <td class="info-table-value" id="info-table-pathsize">N/A</td>
91
91
  </tr>
92
+ <tr>
93
+ <td class="info-table-key">File-Count</td>
94
+ <td class="info-table-value" id="info-table-nfiles">N/A</td>
95
+ </tr>
92
96
  <tr>
93
97
  <td class="info-table-key">Access-Time</td>
94
98
  <td class="info-table-value" id="info-table-accesstime">1970-01-01 00:00:00</td>
@@ -124,6 +128,7 @@ export function showDirInfoPanel(r, u, c){
124
128
  const sizeValTd = document.querySelector('.info-table-value#info-table-pathsize');
125
129
  const createTimeValTd = document.querySelector('.info-table-value#info-table-createtime');
126
130
  const accessTimeValTd = document.querySelector('.info-table-value#info-table-accesstime');
131
+ const countValTd = document.querySelector('.info-table-value#info-table-nfiles');
127
132
  // console.log(sizeValTd, createTimeValTd, accessTimeValTd)
128
133
  c.getMetadata(ensureSlashEnd(r.url)).then((meta) => {
129
134
  if (!meta) {
@@ -133,5 +138,6 @@ export function showDirInfoPanel(r, u, c){
133
138
  sizeValTd.textContent = formatSize(meta.size);
134
139
  createTimeValTd.textContent = cvtGMT2Local(meta.create_time);
135
140
  accessTimeValTd.textContent = cvtGMT2Local(meta.access_time);
141
+ countValTd.textContent = meta.n_files;
136
142
  });
137
143
  }
frontend/popup.css CHANGED
@@ -9,6 +9,19 @@ div.floating-window.blocker{
9
9
  z-index: 100;
10
10
  }
11
11
 
12
+ @keyframes fade-in{
13
+ from{
14
+ opacity: 0;
15
+ transform: translate(-50%, calc(-50% + 0.25rem));
16
+ }
17
+ to{
18
+ opacity: 1;
19
+ transform: translate(-50%, -50%);
20
+ }
21
+ }
22
+ div.floating-window.window{
23
+ animation: fade-in 0.1s ease-in;
24
+ }
12
25
  div.floating-window.window{
13
26
  position: fixed;
14
27
  top: 50%;
frontend/scripts.js CHANGED
@@ -277,6 +277,8 @@ function refreshFileList(){
277
277
  infoButton.style.cursor = 'pointer';
278
278
  infoButton.textContent = 'Details';
279
279
  infoButton.style.width = '100%';
280
+ infoButton.style.display = 'block';
281
+ infoButton.style.textAlign = 'center';
280
282
  infoButton.addEventListener('click', () => {
281
283
  showDirInfoPanel(dir, userRecord, conn);
282
284
  });
frontend/styles.css CHANGED
@@ -219,4 +219,14 @@ a{
219
219
  .delete-btn:hover{
220
220
  color: white !important;
221
221
  background-color: #990511c7 !important;
222
+ }
223
+
224
+ button{
225
+ transition: all 0.2s;
226
+ }
227
+ button:hover {
228
+ transform: scale(1.05);
229
+ }
230
+ button:active {
231
+ transform: scale(0.95);
222
232
  }
lfss/cli/balance.py CHANGED
@@ -3,13 +3,22 @@ Balance the storage by ensuring that large file thresholds are met.
3
3
  """
4
4
 
5
5
  from lfss.src.config import LARGE_BLOB_DIR, LARGE_FILE_BYTES
6
- import argparse, time
6
+ import argparse, time, itertools
7
7
  from functools import wraps
8
8
  from asyncio import Semaphore
9
9
  import aiofiles, asyncio
10
+ import aiofiles.os
11
+ from contextlib import contextmanager
10
12
  from lfss.src.database import transaction, unique_cursor
11
13
  from lfss.src.connection_pool import global_entrance
12
14
 
15
+ @contextmanager
16
+ def indicator(name: str):
17
+ print(f"\033[1;33mRunning {name}... \033[0m")
18
+ s = time.time()
19
+ yield
20
+ print(f"{name} took {time.time() - s:.2f} seconds.")
21
+
13
22
  sem = Semaphore(1)
14
23
 
15
24
  def _get_sem():
@@ -30,7 +39,6 @@ async def move_to_external(f_id: str, flag: str = ''):
30
39
  if blob_row is None:
31
40
  print(f"{flag}File {f_id} not found in blobs.fdata")
32
41
  return
33
- await c.execute("BEGIN")
34
42
  blob: bytes = blob_row[0]
35
43
  async with aiofiles.open(LARGE_BLOB_DIR / f_id, 'wb') as f:
36
44
  await f.write(blob)
@@ -54,13 +62,10 @@ async def move_to_internal(f_id: str, flag: str = ''):
54
62
 
55
63
  @global_entrance()
56
64
  async def _main(batch_size: int = 10000):
57
-
58
65
  tasks = []
59
- start_time = time.time()
60
66
 
61
67
  e_cout = 0
62
- batch_count = 0
63
- while True:
68
+ for batch_count in itertools.count(start=0):
64
69
  async with unique_cursor() as conn:
65
70
  exceeded_rows = list(await (await conn.execute(
66
71
  "SELECT file_id FROM fmeta WHERE file_size > ? AND external = 0 LIMIT ? OFFSET ?",
@@ -76,8 +81,7 @@ async def _main(batch_size: int = 10000):
76
81
  await asyncio.gather(*tasks)
77
82
 
78
83
  i_count = 0
79
- batch_count = 0
80
- while True:
84
+ for batch_count in itertools.count(start=0):
81
85
  async with unique_cursor() as conn:
82
86
  under_rows = list(await (await conn.execute(
83
87
  "SELECT file_id, file_size, external FROM fmeta WHERE file_size <= ? AND external = 1 LIMIT ? OFFSET ?",
@@ -92,18 +96,58 @@ async def _main(batch_size: int = 10000):
92
96
  tasks.append(move_to_internal(f_id, flag=f"[b{batch_count+1}-i{i+1}/{len(under_rows)}] "))
93
97
  await asyncio.gather(*tasks)
94
98
 
95
- end_time = time.time()
96
- print(f"Balancing complete, took {end_time - start_time:.2f} seconds. "
97
- f"{e_cout} files moved to external storage, {i_count} files moved to internal storage.")
99
+ print(f"Finished. {e_cout} files moved to external storage, {i_count} files moved to internal storage.")
100
+
101
+ @global_entrance()
102
+ async def vacuum(index: bool = False, blobs: bool = False):
103
+
104
+ # check if any file in the Large Blob directory is not in the database
105
+ # the reverse operation is not necessary, because by design, the database should be the source of truth...
106
+ # we allow un-referenced files in the Large Blob directory on failure, but not the other way around (unless manually deleted)
107
+ async def ensure_external_consistency(f_id: str):
108
+ @barriered
109
+ async def fn():
110
+ async with unique_cursor() as c:
111
+ cursor = await c.execute("SELECT file_id FROM fmeta WHERE file_id = ?", (f_id,))
112
+ if not await cursor.fetchone():
113
+ print(f"File {f_id} not found in database, removing from external storage.")
114
+ await aiofiles.os.remove(f)
115
+ await asyncio.create_task(fn())
116
+
117
+ # create a temporary index to speed up the process...
118
+ with indicator("Clearing un-referenced files in external storage"):
119
+ async with transaction() as c:
120
+ await c.execute("CREATE INDEX IF NOT EXISTS fmeta_file_id ON fmeta (file_id)")
121
+ for i, f in enumerate(LARGE_BLOB_DIR.iterdir()):
122
+ f_id = f.name
123
+ await ensure_external_consistency(f_id)
124
+ if (i+1) % 1_000 == 0:
125
+ print(f"Checked {(i+1)//1000}k files in external storage.", end='\r')
126
+ async with transaction() as c:
127
+ await c.execute("DROP INDEX IF EXISTS fmeta_file_id")
128
+
129
+ async with unique_cursor(is_write=True) as c:
130
+
131
+ if index:
132
+ with indicator("VACUUM-index"):
133
+ await c.execute("VACUUM main")
134
+ if blobs:
135
+ with indicator("VACUUM-blobs"):
136
+ await c.execute("VACUUM blobs")
98
137
 
99
138
  def main():
100
139
  global sem
101
140
  parser = argparse.ArgumentParser(description="Balance the storage by ensuring that large file thresholds are met.")
102
141
  parser.add_argument("-j", "--jobs", type=int, default=2, help="Number of concurrent jobs")
103
142
  parser.add_argument("-b", "--batch-size", type=int, default=10000, help="Batch size for processing files")
143
+ parser.add_argument("--vacuum", action="store_true", help="Run VACUUM only on index.db after balancing")
144
+ parser.add_argument("--vacuum-all", action="store_true", help="Run VACUUM on both index.db and blobs.db after balancing")
104
145
  args = parser.parse_args()
105
146
  sem = Semaphore(args.jobs)
106
- asyncio.run(_main(args.batch_size))
147
+ with indicator("Balancing"):
148
+ asyncio.run(_main(args.batch_size))
149
+ if args.vacuum or args.vacuum_all:
150
+ asyncio.run(vacuum(index=args.vacuum or args.vacuum_all, blobs=args.vacuum_all))
107
151
 
108
152
  if __name__ == '__main__':
109
153
  main()
lfss/cli/user.py CHANGED
@@ -1,20 +1,10 @@
1
1
  import argparse, asyncio
2
2
  from contextlib import asynccontextmanager
3
3
  from .cli import parse_permission, FileReadPermission
4
+ from ..src.utils import parse_storage_size
4
5
  from ..src.database import Database, FileReadPermission, transaction, UserConn
5
6
  from ..src.connection_pool import global_entrance
6
7
 
7
- def parse_storage_size(s: str) -> int:
8
- if s[-1] in 'Kk':
9
- return int(s[:-1]) * 1024
10
- if s[-1] in 'Mm':
11
- return int(s[:-1]) * 1024 * 1024
12
- if s[-1] in 'Gg':
13
- return int(s[:-1]) * 1024 * 1024 * 1024
14
- if s[-1] in 'Tt':
15
- return int(s[:-1]) * 1024 * 1024 * 1024 * 1024
16
- return int(s)
17
-
18
8
  @global_entrance(1)
19
9
  async def _main():
20
10
  parser = argparse.ArgumentParser()
lfss/src/config.py CHANGED
@@ -1,5 +1,6 @@
1
- from pathlib import Path
2
1
  import os
2
+ from pathlib import Path
3
+ from .utils import parse_storage_size
3
4
 
4
5
  __default_dir = '.storage_data'
5
6
 
@@ -12,6 +13,10 @@ LARGE_BLOB_DIR = DATA_HOME / 'large_blobs'
12
13
  LARGE_BLOB_DIR.mkdir(exist_ok=True)
13
14
 
14
15
  # https://sqlite.org/fasterthanfs.html
15
- LARGE_FILE_BYTES = 8 * 1024 * 1024 # 8MB
16
+ __env_large_file = os.environ.get('LFSS_LARGE_FILE', None)
17
+ if __env_large_file is not None:
18
+ LARGE_FILE_BYTES = parse_storage_size(__env_large_file)
19
+ else:
20
+ LARGE_FILE_BYTES = 8 * 1024 * 1024 # 8MB
16
21
  MAX_FILE_BYTES = 512 * 1024 * 1024 # 512MB
17
22
  MAX_BUNDLE_BYTES = 512 * 1024 * 1024 # 512MB
@@ -5,6 +5,7 @@ from contextlib import asynccontextmanager
5
5
  from dataclasses import dataclass
6
6
  from asyncio import Semaphore, Lock
7
7
  from functools import wraps
8
+ from typing import Callable, Awaitable
8
9
 
9
10
  from .log import get_logger
10
11
  from .config import DATA_HOME
@@ -125,7 +126,7 @@ async def global_connection(n_read: int = 1):
125
126
  await global_connection_close()
126
127
 
127
128
  def global_entrance(n_read: int = 1):
128
- def decorator(func):
129
+ def decorator(func: Callable[..., Awaitable]):
129
130
  @wraps(func)
130
131
  async def wrapper(*args, **kwargs):
131
132
  async with global_connection(n_read):
lfss/src/database.py CHANGED
@@ -139,7 +139,7 @@ class FileConn(DBObjectBase):
139
139
  return []
140
140
  return [self.parse_record(r) for r in res]
141
141
 
142
- async def list_root_dirs(self, *usernames: str) -> list[DirectoryRecord]:
142
+ async def list_root_dirs(self, *usernames: str, skim = False) -> list[DirectoryRecord]:
143
143
  """
144
144
  Efficiently list users' directories, if usernames is empty, list all users' directories.
145
145
  """
@@ -148,12 +148,12 @@ class FileConn(DBObjectBase):
148
148
  await self.cur.execute("SELECT username FROM user")
149
149
  res = await self.cur.fetchall()
150
150
  dirnames = [u[0] + '/' for u in res]
151
- dirs = [DirectoryRecord(u, await self.path_size(u, include_subpath=True)) for u in dirnames]
151
+ dirs = [await self.get_path_record(u) for u in dirnames] if not skim else [DirectoryRecord(u) for u in dirnames]
152
152
  return dirs
153
153
  else:
154
154
  # list specific users
155
155
  dirnames = [uname + '/' for uname in usernames]
156
- dirs = [DirectoryRecord(u, await self.path_size(u, include_subpath=True)) for u in dirnames]
156
+ dirs = [await self.get_path_record(u) for u in dirnames] if not skim else [DirectoryRecord(u) for u in dirnames]
157
157
  return dirs
158
158
 
159
159
  async def list_path(self, url: str, flat: bool = False) -> PathContents:
@@ -207,20 +207,24 @@ class FileConn(DBObjectBase):
207
207
  return PathContents(dirs, files)
208
208
 
209
209
  async def get_path_record(self, url: str) -> DirectoryRecord:
210
+ """
211
+ Get the full record of a directory, including size, create_time, update_time, access_time etc.
212
+ """
210
213
  assert url.endswith('/'), "Path must end with /"
211
214
  cursor = await self.cur.execute("""
212
215
  SELECT MIN(create_time) as create_time,
213
216
  MAX(create_time) as update_time,
214
- MAX(access_time) as access_time
217
+ MAX(access_time) as access_time,
218
+ COUNT(*) as n_files
215
219
  FROM fmeta
216
220
  WHERE url LIKE ?
217
221
  """, (url + '%', ))
218
222
  result = await cursor.fetchone()
219
223
  if result is None or any(val is None for val in result):
220
224
  raise PathNotFoundError(f"Path {url} not found")
221
- create_time, update_time, access_time = result
225
+ create_time, update_time, access_time, n_files = result
222
226
  p_size = await self.path_size(url, include_subpath=True)
223
- return DirectoryRecord(url, p_size, create_time=create_time, update_time=update_time, access_time=access_time)
227
+ return DirectoryRecord(url, p_size, create_time=create_time, update_time=update_time, access_time=access_time, n_files=n_files)
224
228
 
225
229
  async def user_size(self, user_id: int) -> int:
226
230
  cursor = await self.cur.execute("SELECT size FROM usize WHERE user_id = ?", (user_id, ))
@@ -513,7 +517,7 @@ class Database:
513
517
  async def read_file(self, url: str) -> bytes:
514
518
  validate_url(url)
515
519
 
516
- async with transaction() as cur:
520
+ async with unique_cursor() as cur:
517
521
  fconn = FileConn(cur)
518
522
  r = await fconn.get_file_record(url)
519
523
  if r is None:
@@ -525,7 +529,9 @@ class Database:
525
529
  blob = await fconn.get_file_blob(f_id)
526
530
  if blob is None:
527
531
  raise FileNotFoundError(f"File {url} data not found")
528
- await fconn.log_access(url)
532
+
533
+ async with transaction() as w_cur:
534
+ await FileConn(w_cur).log_access(url)
529
535
 
530
536
  return blob
531
537
 
lfss/src/datatype.py CHANGED
@@ -40,13 +40,14 @@ class FileRecord:
40
40
  @dataclasses.dataclass
41
41
  class DirectoryRecord:
42
42
  url: str
43
- size: int
43
+ size: int = -1
44
44
  create_time: str = ""
45
45
  update_time: str = ""
46
46
  access_time: str = ""
47
+ n_files: int = -1
47
48
 
48
49
  def __str__(self):
49
- return f"Directory {self.url} (size={self.size})"
50
+ return f"Directory {self.url} (size={self.size}, created at {self.create_time}, updated at {self.update_time}, accessed at {self.access_time}, n_files={self.n_files})"
50
51
 
51
52
  @dataclasses.dataclass
52
53
  class PathContents:
lfss/src/server.py CHANGED
@@ -135,8 +135,8 @@ async def get_file(path: str, download: bool = False, flat: bool = False, user:
135
135
  if flat:
136
136
  raise HTTPException(status_code=400, detail="Flat query not supported for root path")
137
137
  return PathContents(
138
- dirs = await fconn.list_root_dirs(user.username) \
139
- if not user.is_admin else await fconn.list_root_dirs(),
138
+ dirs = await fconn.list_root_dirs(user.username, skim=True) \
139
+ if not user.is_admin else await fconn.list_root_dirs(skim=True),
140
140
  files = []
141
141
  )
142
142
 
lfss/src/utils.py CHANGED
@@ -62,6 +62,18 @@ def now_stamp() -> float:
62
62
  def stamp_to_str(stamp: float) -> str:
63
63
  return datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d %H:%M:%S')
64
64
 
65
+ def parse_storage_size(s: str) -> int:
66
+ """ Parse the file size string to bytes """
67
+ if s[-1].isdigit():
68
+ return int(s)
69
+ unit = s[-1].lower()
70
+ match unit:
71
+ case 'b': return int(s[:-1])
72
+ case 'k': return int(s[:-1]) * 1024
73
+ case 'm': return int(s[:-1]) * 1024**2
74
+ case 'g': return int(s[:-1]) * 1024**3
75
+ case 't': return int(s[:-1]) * 1024**4
76
+ case _: raise ValueError(f"Invalid file size string: {s}")
65
77
 
66
78
  _FnReturnT = TypeVar('_FnReturnT')
67
79
  _AsyncReturnT = Awaitable[_FnReturnT]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.7.7
3
+ Version: 0.7.9
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li, mengxun
@@ -1,35 +1,35 @@
1
1
  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
- frontend/api.js,sha256=DxWmqO0AAOsWLXYtbgAzEnSmVyEJyzcxXSCH7H3STUk,7925
4
+ frontend/api.js,sha256=o1sP4rKxxnM-rebxnlMlPkhPHzKaVW4kZC7B4ufbOK4,8026
5
5
  frontend/index.html,sha256=Mem8de9vwmZoe4x1DKqpu_aFgIBURqT3mIGdeOOTbIs,2051
6
6
  frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
7
- frontend/info.js,sha256=N3fAniM0Zir_P9t-Mt9_zOWEeqDuhA2uDRfIZsOHn4w,5459
8
- frontend/popup.css,sha256=06Z3ut9-RviCUB2CjiONuEyJRpV3M7LAT6Qzl_nYf9s,1025
7
+ frontend/info.js,sha256=WhOGaeqMoezEAfg4nIpK26hvejC7AZ-ZDLiJmRj0kDk,5758
8
+ frontend/popup.css,sha256=TJZYFW1ZcdD1IVTlNPYNtMWKPbN6XDbQ4hKBOFK8uLg,1284
9
9
  frontend/popup.js,sha256=3PgaGZmxSdV1E-D_MWgcR7aHWkcsHA1BNKSOkmP66tA,5191
10
- frontend/scripts.js,sha256=zp_ENEnWxiwZRM_gE7HP0j_MXEI8yActO6cQfiTkaD8,20171
11
- frontend/styles.css,sha256=l5_SECKR6vkEe4llydRIFxx0C2SOqcacmdMrtdLZRVM,4086
10
+ frontend/scripts.js,sha256=OP99BSbnyTE1LJebGVUvV3WUnDBiZdqaC3a9SE1FF6U,20286
11
+ frontend/styles.css,sha256=37aU9Iep_hTz3LnAAAcEhC_I7AC0A4lX6apnMuGPTlA,4214
12
12
  frontend/utils.js,sha256=Ts4nlef8pkrEgpwX-uQwAhWvwxlIzex8ijDLNCa22ps,2372
13
- lfss/cli/balance.py,sha256=heOgwH6oNnfYsKJfA4VxWKdEXPstdVbbRXWxcDqLIS0,4176
13
+ lfss/cli/balance.py,sha256=TmK48DGU7xPMLv7kASOCS-PY8TIs6GQEsRVRK_4YtXY,6456
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=bO3GT0kuylMGN-7bZWP4e71MlugGZ_lEMkYaYld_Ntg,985
17
- lfss/cli/user.py,sha256=-ePmx_jhqfPQDl_9i_C_St9ujlCyyWSqNJUvc26v4_4,3686
17
+ lfss/cli/user.py,sha256=ETLtj0N-kmxv0mhmeAsO6cY7kPq7nOOP4DetxIRoQpQ,3405
18
18
  lfss/client/__init__.py,sha256=8uvcKs3PYQamDd_cjfN-fX9QUohEzJqeJlOYkBlzC3M,4556
19
19
  lfss/client/api.py,sha256=kSkB4wADTu012-1wl6v90OiZrw6aTQ42GU4jtV4KO0k,5764
20
20
  lfss/sql/init.sql,sha256=C-JtQAlaOjESI8uoF1Y_9dKukEVSw5Ll-7yA3gG-XHU,1210
21
21
  lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
22
22
  lfss/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- lfss/src/config.py,sha256=aTfjWORE9Mx7LSEjbfmHnULlrmIWEvEBSZ4fJKWZNjM,530
24
- lfss/src/connection_pool.py,sha256=teW_4DMiwlCN_bS7AhjkbY9cHZqUFlmHE_J2yPjHVsA,5125
25
- lfss/src/database.py,sha256=-itbpGb7cQrywZLFk4aNcuy38Krsyemtyiz8GIt4i7M,31944
26
- lfss/src/datatype.py,sha256=BLS7vuuKnFZQg0nrKeP9SymqUhcN6HwPgejU0yBd_Ak,1622
23
+ lfss/src/config.py,sha256=CIbVFWRu86dl2GVlXlCDv93W8PLwT89NtznU6TCKvtk,729
24
+ lfss/src/connection_pool.py,sha256=r4Ho5d_Gd4S_KbT7515UJoiyfIgS6xyttqMsKqOfaIg,5190
25
+ lfss/src/database.py,sha256=AmyQwPFjrntFyZPTJOUB5kGvFpWuex6TPtxZKR2KK8k,32244
26
+ lfss/src/datatype.py,sha256=WfrLALU_7wei5-i_b0TxY8xWI5mwxLUHFepHSps49zA,1767
27
27
  lfss/src/error.py,sha256=imbhwnbhnI3HLhkbfICROe3F0gleKrOk4XnqHJDOtuI,285
28
28
  lfss/src/log.py,sha256=xOnkuH-gB_jSVGqNnDVEW05iki6SCJ2xdEhjz5eEsMo,5136
29
- lfss/src/server.py,sha256=EA5fK4qc98tF8qoS9F6VaxIE65D5X8Ztkjqy8EUYIv8,16276
29
+ lfss/src/server.py,sha256=rrrhFDFrglth4yCvdvvYko-4JfVJ_MTixhCr9_Hbhx0,16296
30
30
  lfss/src/stat.py,sha256=hTMtQyM_Ukmhc33Bb9FGCfBMIX02KrGHQg8nL7sC8sU,2082
31
- lfss/src/utils.py,sha256=LXsjNatuwg3iDCKTBgC-t7iEuM2X_mPsgNdDy92zhwo,3405
32
- lfss-0.7.7.dist-info/METADATA,sha256=j3VO-GRmoC-_fseS5NAUcUJaOMBUyIx0OS9Amiji9Hg,1967
33
- lfss-0.7.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
34
- lfss-0.7.7.dist-info/entry_points.txt,sha256=d_Ri3GXxUW-S0E6q953A8od0YMmUAnZGlJSKS46OiW8,172
35
- lfss-0.7.7.dist-info/RECORD,,
31
+ lfss/src/utils.py,sha256=S9LCJ5OkNk_zM4rZnrHg1UDjnNkDVO_ejmfsBeNJs4s,3868
32
+ lfss-0.7.9.dist-info/METADATA,sha256=rPPqsNm5iZI8XyG8YzY1wWn0X1TbGa8tzrEykDkXf3o,1967
33
+ lfss-0.7.9.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
34
+ lfss-0.7.9.dist-info/entry_points.txt,sha256=d_Ri3GXxUW-S0E6q953A8od0YMmUAnZGlJSKS46OiW8,172
35
+ lfss-0.7.9.dist-info/RECORD,,
File without changes