lfss 0.7.7__tar.gz → 0.7.9__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.
- {lfss-0.7.7 → lfss-0.7.9}/PKG-INFO +1 -1
- {lfss-0.7.7 → lfss-0.7.9}/frontend/api.js +1 -0
- {lfss-0.7.7 → lfss-0.7.9}/frontend/info.js +16 -10
- {lfss-0.7.7 → lfss-0.7.9}/frontend/popup.css +13 -0
- {lfss-0.7.7 → lfss-0.7.9}/frontend/scripts.js +2 -0
- {lfss-0.7.7 → lfss-0.7.9}/frontend/styles.css +10 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/cli/balance.py +56 -12
- {lfss-0.7.7 → lfss-0.7.9}/lfss/cli/user.py +1 -11
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/config.py +7 -2
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/connection_pool.py +2 -1
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/database.py +14 -8
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/datatype.py +3 -2
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/server.py +2 -2
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/utils.py +12 -0
- {lfss-0.7.7 → lfss-0.7.9}/pyproject.toml +1 -1
- {lfss-0.7.7 → lfss-0.7.9}/Readme.md +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/docs/Known_issues.md +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/docs/Permission.md +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/frontend/index.html +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/frontend/info.css +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/frontend/popup.js +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/frontend/utils.js +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/cli/cli.py +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/cli/panel.py +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/cli/serve.py +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/client/__init__.py +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/client/api.py +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/sql/init.sql +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/sql/pragma.sql +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/__init__.py +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/error.py +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/log.py +0 -0
- {lfss-0.7.7 → lfss-0.7.9}/lfss/src/stat.py +0 -0
@@ -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
|
@@ -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">
|
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">
|
31
|
-
<td class="info-table-value">${
|
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">
|
35
|
-
<td class="info-table-value">${r.
|
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">
|
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
|
}
|
@@ -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%;
|
@@ -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
|
});
|
@@ -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
|
}
|
@@ -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 =
|
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 =
|
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
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
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()
|
@@ -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()
|
@@ -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
|
-
|
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):
|
@@ -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 = [
|
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 = [
|
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
|
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
|
-
|
532
|
+
|
533
|
+
async with transaction() as w_cur:
|
534
|
+
await FileConn(w_cur).log_access(url)
|
529
535
|
|
530
536
|
return blob
|
531
537
|
|
@@ -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:
|
@@ -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
|
|
@@ -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]
|
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
|