lfss 0.5.2__tar.gz → 0.6.0__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.5.2 → lfss-0.6.0}/PKG-INFO +1 -1
- {lfss-0.5.2 → lfss-0.6.0}/lfss/client/api.py +1 -1
- {lfss-0.5.2 → lfss-0.6.0}/lfss/sql/init.sql +7 -6
- {lfss-0.5.2 → lfss-0.6.0}/lfss/src/database.py +70 -89
- lfss-0.6.0/lfss/src/datatype.py +55 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/src/server.py +16 -13
- {lfss-0.5.2 → lfss-0.6.0}/pyproject.toml +1 -1
- {lfss-0.5.2 → lfss-0.6.0}/Readme.md +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/docs/Known_issues.md +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/docs/Permission.md +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/frontend/api.js +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/frontend/index.html +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/frontend/popup.css +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/frontend/popup.js +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/frontend/scripts.js +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/frontend/styles.css +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/frontend/utils.js +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/cli/balance.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/cli/cli.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/cli/panel.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/cli/serve.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/cli/user.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/client/__init__.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/sql/pragma.sql +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/src/__init__.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/src/config.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/src/error.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/src/log.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/src/stat.py +0 -0
- {lfss-0.5.2 → lfss-0.6.0}/lfss/src/utils.py +0 -0
@@ -1,7 +1,7 @@
|
|
1
1
|
CREATE TABLE IF NOT EXISTS user (
|
2
2
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
3
|
-
username VARCHAR(
|
4
|
-
credential VARCHAR(
|
3
|
+
username VARCHAR(256) UNIQUE NOT NULL,
|
4
|
+
credential VARCHAR(256) NOT NULL,
|
5
5
|
is_admin BOOLEAN DEFAULT FALSE,
|
6
6
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
7
7
|
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
@@ -10,19 +10,20 @@ CREATE TABLE IF NOT EXISTS user (
|
|
10
10
|
);
|
11
11
|
|
12
12
|
CREATE TABLE IF NOT EXISTS fmeta (
|
13
|
-
url VARCHAR(
|
13
|
+
url VARCHAR(1024) PRIMARY KEY,
|
14
14
|
owner_id INTEGER NOT NULL,
|
15
|
-
file_id
|
15
|
+
file_id CHAR(32) NOT NULL,
|
16
16
|
file_size INTEGER,
|
17
17
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
18
18
|
access_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
19
19
|
permission INTEGER DEFAULT 0,
|
20
|
-
external BOOLEAN DEFAULT FALSE,
|
20
|
+
external BOOLEAN DEFAULT FALSE,
|
21
|
+
mime_type VARCHAR(256) DEFAULT 'application/octet-stream',
|
21
22
|
FOREIGN KEY(owner_id) REFERENCES user(id)
|
22
23
|
);
|
23
24
|
|
24
25
|
CREATE TABLE IF NOT EXISTS fdata (
|
25
|
-
file_id
|
26
|
+
file_id CHAR(32) PRIMARY KEY,
|
26
27
|
data BLOB
|
27
28
|
);
|
28
29
|
|
@@ -4,16 +4,16 @@ from abc import ABC, abstractmethod
|
|
4
4
|
|
5
5
|
import urllib.parse
|
6
6
|
from pathlib import Path
|
7
|
-
import
|
7
|
+
import hashlib, uuid
|
8
8
|
from contextlib import asynccontextmanager
|
9
9
|
from functools import wraps
|
10
|
-
from enum import IntEnum
|
11
10
|
import zipfile, io, asyncio
|
12
11
|
|
13
12
|
import aiosqlite, aiofiles
|
14
13
|
import aiofiles.os
|
15
14
|
from asyncio import Lock
|
16
15
|
|
16
|
+
from .datatype import UserRecord, FileReadPermission, FileRecord, DirectoryRecord, PathContents
|
17
17
|
from .config import DATA_HOME, LARGE_BLOB_DIR
|
18
18
|
from .log import get_logger
|
19
19
|
from .utils import decode_uri_compnents
|
@@ -64,26 +64,6 @@ class DBConnBase(ABC):
|
|
64
64
|
async def commit(self):
|
65
65
|
await self.conn.commit()
|
66
66
|
|
67
|
-
class FileReadPermission(IntEnum):
|
68
|
-
UNSET = 0 # not set
|
69
|
-
PUBLIC = 1 # accessible by anyone
|
70
|
-
PROTECTED = 2 # accessible by any user
|
71
|
-
PRIVATE = 3 # accessible by owner only (including admin)
|
72
|
-
|
73
|
-
@dataclasses.dataclass
|
74
|
-
class UserRecord:
|
75
|
-
id: int
|
76
|
-
username: str
|
77
|
-
credential: str
|
78
|
-
is_admin: bool
|
79
|
-
create_time: str
|
80
|
-
last_active: str
|
81
|
-
max_storage: int
|
82
|
-
permission: 'FileReadPermission'
|
83
|
-
|
84
|
-
def __str__(self):
|
85
|
-
return f"User {self.username} (id={self.id}, admin={self.is_admin}, created at {self.create_time}, last active at {self.last_active}, storage={self.max_storage}, permission={self.permission})"
|
86
|
-
|
87
67
|
DECOY_USER = UserRecord(0, 'decoy', 'decoy', False, '2021-01-01 00:00:00', '2021-01-01 00:00:00', 0, FileReadPermission.PRIVATE)
|
88
68
|
class UserConn(DBConnBase):
|
89
69
|
|
@@ -174,37 +154,6 @@ class UserConn(DBConnBase):
|
|
174
154
|
await self.conn.execute("DELETE FROM user WHERE username = ?", (username, ))
|
175
155
|
self.logger.info(f"Delete user {username}")
|
176
156
|
|
177
|
-
@dataclasses.dataclass
|
178
|
-
class FileRecord:
|
179
|
-
url: str
|
180
|
-
owner_id: int
|
181
|
-
file_id: str # defines mapping from fmata to fdata
|
182
|
-
file_size: int
|
183
|
-
create_time: str
|
184
|
-
access_time: str
|
185
|
-
permission: FileReadPermission
|
186
|
-
external: bool
|
187
|
-
|
188
|
-
def __str__(self):
|
189
|
-
return f"File {self.url} (owner={self.owner_id}, created at {self.create_time}, accessed at {self.access_time}, " + \
|
190
|
-
f"file_id={self.file_id}, permission={self.permission}, size={self.file_size}, external={self.external})"
|
191
|
-
|
192
|
-
@dataclasses.dataclass
|
193
|
-
class DirectoryRecord:
|
194
|
-
url: str
|
195
|
-
size: int
|
196
|
-
create_time: str = ""
|
197
|
-
update_time: str = ""
|
198
|
-
access_time: str = ""
|
199
|
-
|
200
|
-
def __str__(self):
|
201
|
-
return f"Directory {self.url} (size={self.size})"
|
202
|
-
|
203
|
-
@dataclasses.dataclass
|
204
|
-
class PathContents:
|
205
|
-
dirs: list[DirectoryRecord]
|
206
|
-
files: list[FileRecord]
|
207
|
-
|
208
157
|
class FileConn(DBConnBase):
|
209
158
|
|
210
159
|
@staticmethod
|
@@ -235,6 +184,38 @@ class FileConn(DBConnBase):
|
|
235
184
|
ALTER TABLE fmeta ADD COLUMN external BOOLEAN DEFAULT FALSE
|
236
185
|
''')
|
237
186
|
|
187
|
+
# backward compatibility, since 0.6.0
|
188
|
+
async with self.conn.execute("SELECT * FROM fmeta") as cursor:
|
189
|
+
res = await cursor.fetchone()
|
190
|
+
if res and len(res) < 9:
|
191
|
+
self.logger.info("Updating fmeta table")
|
192
|
+
await self.conn.execute('''
|
193
|
+
ALTER TABLE fmeta ADD COLUMN mime_type TEXT DEFAULT 'application/octet-stream'
|
194
|
+
''')
|
195
|
+
# check all mime types
|
196
|
+
import mimetypes, mimesniff
|
197
|
+
async with self.conn.execute("SELECT url, file_id, external FROM fmeta") as cursor:
|
198
|
+
res = await cursor.fetchall()
|
199
|
+
async with self.conn.execute("SELECT count(*) FROM fmeta") as cursor:
|
200
|
+
count = await cursor.fetchone()
|
201
|
+
assert count is not None
|
202
|
+
for counter, r in enumerate(res, start=1):
|
203
|
+
print(f"Checking mimetype for {counter}/{count[0]}")
|
204
|
+
url, f_id, external = r
|
205
|
+
fname = url.split('/')[-1]
|
206
|
+
mime_type, _ = mimetypes.guess_type(fname)
|
207
|
+
if mime_type is None:
|
208
|
+
# try to sniff the file
|
209
|
+
if not external:
|
210
|
+
async with self.conn.execute("SELECT data FROM fdata WHERE file_id = ?", (f_id, )) as cursor:
|
211
|
+
blob = await cursor.fetchone()
|
212
|
+
assert blob is not None
|
213
|
+
blob = blob[0]
|
214
|
+
mime_type = mimesniff.what(blob)
|
215
|
+
else:
|
216
|
+
mime_type = mimesniff.what(LARGE_BLOB_DIR / f_id)
|
217
|
+
await self.conn.execute("UPDATE fmeta SET mime_type = ? WHERE url = ?", (mime_type, url))
|
218
|
+
|
238
219
|
return self
|
239
220
|
|
240
221
|
async def get_file_record(self, url: str) -> Optional[FileRecord]:
|
@@ -372,43 +353,42 @@ class FileConn(DBConnBase):
|
|
372
353
|
assert res is not None
|
373
354
|
return res[0] or 0
|
374
355
|
|
356
|
+
@atomic
|
357
|
+
async def update_file_record(
|
358
|
+
self, url, owner_id: Optional[int] = None, permission: Optional[FileReadPermission] = None
|
359
|
+
):
|
360
|
+
old = await self.get_file_record(url)
|
361
|
+
assert old is not None, f"File {url} not found"
|
362
|
+
if owner_id is None:
|
363
|
+
owner_id = old.owner_id
|
364
|
+
if permission is None:
|
365
|
+
permission = old.permission
|
366
|
+
await self.conn.execute(
|
367
|
+
"UPDATE fmeta SET owner_id = ?, permission = ? WHERE url = ?",
|
368
|
+
(owner_id, int(permission), url)
|
369
|
+
)
|
370
|
+
self.logger.info(f"Updated file {url}")
|
371
|
+
|
375
372
|
@atomic
|
376
373
|
async def set_file_record(
|
377
374
|
self, url: str,
|
378
|
-
owner_id:
|
379
|
-
file_id:
|
380
|
-
file_size:
|
381
|
-
permission:
|
382
|
-
external:
|
375
|
+
owner_id: int,
|
376
|
+
file_id:str,
|
377
|
+
file_size: int,
|
378
|
+
permission: FileReadPermission,
|
379
|
+
external: bool,
|
380
|
+
mime_type: str
|
383
381
|
):
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
if permission is None: permission = old.permission
|
395
|
-
await self.conn.execute(
|
396
|
-
"""
|
397
|
-
UPDATE fmeta SET owner_id = ?, permission = ?,
|
398
|
-
access_time = CURRENT_TIMESTAMP WHERE url = ?
|
399
|
-
""", (owner_id, int(permission), url))
|
400
|
-
self.logger.info(f"File {url} updated")
|
401
|
-
else:
|
402
|
-
self.logger.debug(f"Creating fmeta {url}: permission={permission}, owner_id={owner_id}, file_id={file_id}, file_size={file_size}, external={external}")
|
403
|
-
if permission is None:
|
404
|
-
permission = FileReadPermission.UNSET
|
405
|
-
assert owner_id is not None and file_id is not None and file_size is not None and external is not None
|
406
|
-
await self.conn.execute(
|
407
|
-
"INSERT INTO fmeta (url, owner_id, file_id, file_size, permission, external) VALUES (?, ?, ?, ?, ?, ?)",
|
408
|
-
(url, owner_id, file_id, file_size, int(permission), external)
|
409
|
-
)
|
410
|
-
await self._user_size_inc(owner_id, file_size)
|
411
|
-
self.logger.info(f"File {url} created")
|
382
|
+
self.logger.debug(f"Creating fmeta {url}: permission={permission}, owner_id={owner_id}, file_id={file_id}, file_size={file_size}, external={external}, mime_type={mime_type}")
|
383
|
+
if permission is None:
|
384
|
+
permission = FileReadPermission.UNSET
|
385
|
+
assert owner_id is not None and file_id is not None and file_size is not None and external is not None
|
386
|
+
await self.conn.execute(
|
387
|
+
"INSERT INTO fmeta (url, owner_id, file_id, file_size, permission, external, mime_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
388
|
+
(url, owner_id, file_id, file_size, int(permission), external, mime_type)
|
389
|
+
)
|
390
|
+
await self._user_size_inc(owner_id, file_size)
|
391
|
+
self.logger.info(f"File {url} created")
|
412
392
|
|
413
393
|
@atomic
|
414
394
|
async def move_file(self, old_url: str, new_url: str):
|
@@ -587,7 +567,8 @@ class Database:
|
|
587
567
|
async def save_file(
|
588
568
|
self, u: int | str, url: str,
|
589
569
|
blob: bytes | AsyncIterable[bytes],
|
590
|
-
permission: FileReadPermission = FileReadPermission.UNSET
|
570
|
+
permission: FileReadPermission = FileReadPermission.UNSET,
|
571
|
+
mime_type: str = 'application/octet-stream'
|
591
572
|
):
|
592
573
|
"""
|
593
574
|
if file_size is not provided, the blob must be bytes
|
@@ -619,7 +600,7 @@ class Database:
|
|
619
600
|
await self.file.set_file_blob(f_id, blob)
|
620
601
|
await self.file.set_file_record(
|
621
602
|
url, owner_id=user.id, file_id=f_id, file_size=file_size,
|
622
|
-
permission=permission, external=False)
|
603
|
+
permission=permission, external=False, mime_type=mime_type)
|
623
604
|
await self.user.set_active(user.username)
|
624
605
|
else:
|
625
606
|
assert isinstance(blob, AsyncIterable)
|
@@ -631,7 +612,7 @@ class Database:
|
|
631
612
|
raise StorageExceededError(f"Unable to save file, user {user.username} has storage limit of {user.max_storage}, used {user_size_used}, requested {file_size}")
|
632
613
|
await self.file.set_file_record(
|
633
614
|
url, owner_id=user.id, file_id=f_id, file_size=file_size,
|
634
|
-
permission=permission, external=True)
|
615
|
+
permission=permission, external=True, mime_type=mime_type)
|
635
616
|
await self.user.set_active(user.username)
|
636
617
|
|
637
618
|
async def read_file_stream(self, url: str) -> AsyncIterable[bytes]:
|
@@ -0,0 +1,55 @@
|
|
1
|
+
from enum import IntEnum
|
2
|
+
import dataclasses
|
3
|
+
|
4
|
+
class FileReadPermission(IntEnum):
|
5
|
+
UNSET = 0 # not set
|
6
|
+
PUBLIC = 1 # accessible by anyone
|
7
|
+
PROTECTED = 2 # accessible by any user
|
8
|
+
PRIVATE = 3 # accessible by owner only (including admin)
|
9
|
+
|
10
|
+
@dataclasses.dataclass
|
11
|
+
class UserRecord:
|
12
|
+
id: int
|
13
|
+
username: str
|
14
|
+
credential: str
|
15
|
+
is_admin: bool
|
16
|
+
create_time: str
|
17
|
+
last_active: str
|
18
|
+
max_storage: int
|
19
|
+
permission: 'FileReadPermission'
|
20
|
+
|
21
|
+
def __str__(self):
|
22
|
+
return f"User {self.username} (id={self.id}, admin={self.is_admin}, created at {self.create_time}, last active at {self.last_active}, storage={self.max_storage}, permission={self.permission})"
|
23
|
+
|
24
|
+
@dataclasses.dataclass
|
25
|
+
class FileRecord:
|
26
|
+
url: str
|
27
|
+
owner_id: int
|
28
|
+
file_id: str # defines mapping from fmata to fdata
|
29
|
+
file_size: int
|
30
|
+
create_time: str
|
31
|
+
access_time: str
|
32
|
+
permission: FileReadPermission
|
33
|
+
external: bool
|
34
|
+
mime_type: str
|
35
|
+
|
36
|
+
def __str__(self):
|
37
|
+
return f"File {self.url} [{self.mime_type}] (owner={self.owner_id}, created at {self.create_time}, accessed at {self.access_time}, " + \
|
38
|
+
f"file_id={self.file_id}, permission={self.permission}, size={self.file_size}, external={self.external})"
|
39
|
+
|
40
|
+
@dataclasses.dataclass
|
41
|
+
class DirectoryRecord:
|
42
|
+
url: str
|
43
|
+
size: int
|
44
|
+
create_time: str = ""
|
45
|
+
update_time: str = ""
|
46
|
+
access_time: str = ""
|
47
|
+
|
48
|
+
def __str__(self):
|
49
|
+
return f"Directory {self.url} (size={self.size})"
|
50
|
+
|
51
|
+
@dataclasses.dataclass
|
52
|
+
class PathContents:
|
53
|
+
dirs: list[DirectoryRecord]
|
54
|
+
files: list[FileRecord]
|
55
|
+
|
@@ -15,7 +15,7 @@ from contextlib import asynccontextmanager
|
|
15
15
|
from .error import *
|
16
16
|
from .log import get_logger
|
17
17
|
from .stat import RequestDB
|
18
|
-
from .config import MAX_BUNDLE_BYTES, MAX_FILE_BYTES,
|
18
|
+
from .config import MAX_BUNDLE_BYTES, MAX_FILE_BYTES, LARGE_FILE_BYTES
|
19
19
|
from .utils import ensure_uri_compnents, format_last_modified, now_stamp
|
20
20
|
from .database import Database, UserRecord, DECOY_USER, FileRecord, check_user_permission, FileReadPermission
|
21
21
|
|
@@ -142,12 +142,10 @@ async def get_file(path: str, download = False, user: UserRecord = Depends(get_c
|
|
142
142
|
|
143
143
|
fname = path.split("/")[-1]
|
144
144
|
async def send(media_type: Optional[str] = None, disposition = "attachment"):
|
145
|
+
if media_type is None:
|
146
|
+
media_type = file_record.mime_type
|
145
147
|
if not file_record.external:
|
146
148
|
fblob = await conn.read_file(path)
|
147
|
-
if media_type is None:
|
148
|
-
media_type, _ = mimetypes.guess_type(fname)
|
149
|
-
if media_type is None:
|
150
|
-
media_type = mimesniff.what(fblob)
|
151
149
|
return Response(
|
152
150
|
content=fblob, media_type=media_type, headers={
|
153
151
|
"Content-Disposition": f"{disposition}; filename={fname}",
|
@@ -155,12 +153,7 @@ async def get_file(path: str, download = False, user: UserRecord = Depends(get_c
|
|
155
153
|
"Last-Modified": format_last_modified(file_record.create_time)
|
156
154
|
}
|
157
155
|
)
|
158
|
-
|
159
156
|
else:
|
160
|
-
if media_type is None:
|
161
|
-
media_type, _ = mimetypes.guess_type(fname)
|
162
|
-
if media_type is None:
|
163
|
-
media_type = mimesniff.what(str((LARGE_BLOB_DIR / file_record.file_id).absolute()))
|
164
157
|
return StreamingResponse(
|
165
158
|
await conn.read_file_stream(path), media_type=media_type, headers={
|
166
159
|
"Content-Disposition": f"{disposition}; filename={fname}",
|
@@ -228,14 +221,24 @@ async def put_file(
|
|
228
221
|
blobs = await request.body()
|
229
222
|
else:
|
230
223
|
blobs = await request.body()
|
224
|
+
|
225
|
+
# check file type
|
226
|
+
assert not path.endswith("/"), "Path must be a file"
|
227
|
+
fname = path.split("/")[-1]
|
228
|
+
mime_t, _ = mimetypes.guess_type(fname)
|
229
|
+
if mime_t is None:
|
230
|
+
mime_t = mimesniff.what(blobs)
|
231
|
+
if mime_t is None:
|
232
|
+
mime_t = "application/octet-stream"
|
233
|
+
|
231
234
|
if len(blobs) > LARGE_FILE_BYTES:
|
232
235
|
async def blob_reader():
|
233
236
|
chunk_size = 16 * 1024 * 1024 # 16MB
|
234
237
|
for b in range(0, len(blobs), chunk_size):
|
235
238
|
yield blobs[b:b+chunk_size]
|
236
|
-
await conn.save_file(user.id, path, blob_reader(), permission = FileReadPermission(permission))
|
239
|
+
await conn.save_file(user.id, path, blob_reader(), permission = FileReadPermission(permission), mime_type = mime_t)
|
237
240
|
else:
|
238
|
-
await conn.save_file(user.id, path, blobs, permission = FileReadPermission(permission))
|
241
|
+
await conn.save_file(user.id, path, blobs, permission = FileReadPermission(permission), mime_type=mime_t)
|
239
242
|
|
240
243
|
# https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/PUT
|
241
244
|
if exists_flag:
|
@@ -353,7 +356,7 @@ async def update_file_meta(
|
|
353
356
|
|
354
357
|
if perm is not None:
|
355
358
|
logger.info(f"Update permission of {path} to {perm}")
|
356
|
-
await conn.file.
|
359
|
+
await conn.file.update_file_record(
|
357
360
|
url = file_record.url,
|
358
361
|
permission = FileReadPermission(perm)
|
359
362
|
)
|
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
|