lfss 0.7.2__tar.gz → 0.7.3__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.2 → lfss-0.7.3}/PKG-INFO +3 -3
- {lfss-0.7.2 → lfss-0.7.3}/Readme.md +2 -2
- {lfss-0.7.2 → lfss-0.7.3}/docs/Permission.md +24 -16
- {lfss-0.7.2 → lfss-0.7.3}/lfss/cli/cli.py +1 -1
- {lfss-0.7.2 → lfss-0.7.3}/lfss/cli/user.py +1 -1
- {lfss-0.7.2 → lfss-0.7.3}/lfss/client/api.py +13 -11
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/config.py +5 -2
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/connection_pool.py +1 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/database.py +50 -39
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/server.py +21 -21
- {lfss-0.7.2 → lfss-0.7.3}/pyproject.toml +5 -1
- {lfss-0.7.2 → lfss-0.7.3}/docs/Known_issues.md +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/frontend/api.js +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/frontend/index.html +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/frontend/popup.css +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/frontend/popup.js +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/frontend/scripts.js +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/frontend/styles.css +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/frontend/utils.js +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/cli/balance.py +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/cli/panel.py +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/cli/serve.py +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/client/__init__.py +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/sql/init.sql +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/sql/pragma.sql +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/__init__.py +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/datatype.py +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/error.py +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/log.py +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/stat.py +0 -0
- {lfss-0.7.2 → lfss-0.7.3}/lfss/src/utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: lfss
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.3
|
4
4
|
Summary: Lightweight file storage service
|
5
5
|
Home-page: https://github.com/MenxLi/lfss
|
6
6
|
Author: li, mengxun
|
@@ -22,8 +22,8 @@ Description-Content-Type: text/markdown
|
|
22
22
|
[](https://pypi.org/project/lfss/)
|
23
23
|
|
24
24
|
My experiment on a lightweight file/object storage service.
|
25
|
-
It stores small files and metadata in sqlite, large files in the filesystem
|
26
|
-
Tested on 2 million files, and it works fine...
|
25
|
+
It stores small files and metadata in sqlite, large files in the filesystem.
|
26
|
+
Tested on 2 million files, and it works fine...
|
27
27
|
|
28
28
|
Usage:
|
29
29
|
```sh
|
@@ -2,8 +2,8 @@
|
|
2
2
|
[](https://pypi.org/project/lfss/)
|
3
3
|
|
4
4
|
My experiment on a lightweight file/object storage service.
|
5
|
-
It stores small files and metadata in sqlite, large files in the filesystem
|
6
|
-
Tested on 2 million files, and it works fine...
|
5
|
+
It stores small files and metadata in sqlite, large files in the filesystem.
|
6
|
+
Tested on 2 million files, and it works fine...
|
7
7
|
|
8
8
|
Usage:
|
9
9
|
```sh
|
@@ -1,22 +1,12 @@
|
|
1
1
|
|
2
2
|
# Permission System
|
3
|
-
There are two roles in the system: Admin and User (
|
3
|
+
There are two roles in the system: Admin and User ('user' are like 'bucket' to some extent).
|
4
4
|
|
5
|
-
##
|
6
|
-
|
7
|
-
Every user can have `PUT/DELETE` permissions of files under its own `/<user>/` path.
|
8
|
-
The admin can have `PUT/DELETE` permissions of files of all users.
|
5
|
+
## File access with `GET` permission
|
6
|
+
The `GET` is used to access the file (if path is not ending with `/`), or to list the files under a path (if path is ending with `/`).
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
### Path-listing
|
14
|
-
- Non-login users cannot list any files.
|
15
|
-
- All users can list the files under their own path
|
16
|
-
- Admins can list the files under other users' path.
|
17
|
-
|
18
|
-
### File-access
|
19
|
-
For accessing file content, the user must have `GET` permission of the file, which is determined by the `permission` field of both the owner and the file.
|
8
|
+
### File access
|
9
|
+
For accessing file content, the user must have `GET` permission of the file, which is determined by the `permission` field of both the owner and the file.
|
20
10
|
(Note: The owner of the file is the user who created the file, may not necessarily be the user under whose path the file is stored.)
|
21
11
|
|
22
12
|
There are four types of permissions: `unset`, `public`, `protected`, `private`.
|
@@ -26,4 +16,22 @@ Non-admin users can access files based on:
|
|
26
16
|
- If the file is `protected`, then only the logged-in user can access it.
|
27
17
|
- If the file is `private`, then only the owner can access it.
|
28
18
|
- If the file is `unset`, then the file's permission is inherited from the owner's permission.
|
29
|
-
- If both the owner and the file have `unset` permission, then the file is `public`.
|
19
|
+
- If both the owner and the file have `unset` permission, then the file is `public`.
|
20
|
+
|
21
|
+
### Path-listing
|
22
|
+
- Non-login users cannot list any files.
|
23
|
+
- All users can list the files under their own path
|
24
|
+
- Admins can list the files under other users' path.
|
25
|
+
|
26
|
+
## File creation with `PUT` permission
|
27
|
+
The `PUT` is used to create a file.
|
28
|
+
- Non-login user don't have `PUT` permission.
|
29
|
+
- Every user can have `PUT` permission of files under its own `/<user>/` path.
|
30
|
+
- The admin can have `PUT` permission of files of all users.
|
31
|
+
|
32
|
+
## `DELETE` and moving permissions
|
33
|
+
- Non-login user don't have `DELETE`/move permission.
|
34
|
+
- Every user can have `DELETE`/move permission that they own.
|
35
|
+
- The admin can have `DELETE` permission of files of all users
|
36
|
+
(The admin can't move files of other users, because move does not change the owner of the file.
|
37
|
+
If move is allowed, then its equivalent to create file on behalf of other users.)
|
@@ -45,6 +45,7 @@ async def _main():
|
|
45
45
|
sp_list.add_argument("-l", "--long", action="store_true")
|
46
46
|
|
47
47
|
args = parser.parse_args()
|
48
|
+
db = await Database().init()
|
48
49
|
|
49
50
|
@asynccontextmanager
|
50
51
|
async def get_uconn():
|
@@ -65,7 +66,6 @@ async def _main():
|
|
65
66
|
print('User not found')
|
66
67
|
exit(1)
|
67
68
|
else:
|
68
|
-
db = await Database().init()
|
69
69
|
await db.delete_user(user.id)
|
70
70
|
print('User deleted')
|
71
71
|
|
@@ -17,7 +17,7 @@ class Connector:
|
|
17
17
|
"token": token
|
18
18
|
}
|
19
19
|
|
20
|
-
def
|
20
|
+
def _fetch_factory(
|
21
21
|
self, method: Literal['GET', 'POST', 'PUT', 'DELETE'],
|
22
22
|
path: str, search_params: dict = {}
|
23
23
|
):
|
@@ -46,7 +46,7 @@ class Connector:
|
|
46
46
|
else:
|
47
47
|
return {'status': 'skipped', 'path': path}
|
48
48
|
|
49
|
-
response = self.
|
49
|
+
response = self._fetch_factory('PUT', path, search_params={
|
50
50
|
'permission': int(permission),
|
51
51
|
'conflict': conflict
|
52
52
|
})(
|
@@ -68,7 +68,7 @@ class Connector:
|
|
68
68
|
else:
|
69
69
|
return {'status': 'skipped', 'path': path}
|
70
70
|
|
71
|
-
response = self.
|
71
|
+
response = self._fetch_factory('PUT', path, search_params={
|
72
72
|
'permission': int(permission),
|
73
73
|
'conflict': conflict
|
74
74
|
})(
|
@@ -79,7 +79,7 @@ class Connector:
|
|
79
79
|
|
80
80
|
def _get(self, path: str) -> Optional[requests.Response]:
|
81
81
|
try:
|
82
|
-
response = self.
|
82
|
+
response = self._fetch_factory('GET', path)()
|
83
83
|
except requests.exceptions.HTTPError as e:
|
84
84
|
if e.response.status_code == 404:
|
85
85
|
return None
|
@@ -100,12 +100,12 @@ class Connector:
|
|
100
100
|
|
101
101
|
def delete(self, path: str):
|
102
102
|
"""Deletes the file at the specified path."""
|
103
|
-
self.
|
103
|
+
self._fetch_factory('DELETE', path)()
|
104
104
|
|
105
105
|
def get_metadata(self, path: str) -> Optional[FileRecord | DirectoryRecord]:
|
106
106
|
"""Gets the metadata for the file at the specified path."""
|
107
107
|
try:
|
108
|
-
response = self.
|
108
|
+
response = self._fetch_factory('GET', '_api/meta', {'path': path})()
|
109
109
|
if path.endswith('/'):
|
110
110
|
return DirectoryRecord(**response.json())
|
111
111
|
else:
|
@@ -117,22 +117,24 @@ class Connector:
|
|
117
117
|
|
118
118
|
def list_path(self, path: str) -> PathContents:
|
119
119
|
assert path.endswith('/')
|
120
|
-
response = self.
|
121
|
-
|
120
|
+
response = self._fetch_factory('GET', path)()
|
121
|
+
dirs = [DirectoryRecord(**d) for d in response.json()['dirs']]
|
122
|
+
files = [FileRecord(**f) for f in response.json()['files']]
|
123
|
+
return PathContents(dirs=dirs, files=files)
|
122
124
|
|
123
125
|
def set_file_permission(self, path: str, permission: int | FileReadPermission):
|
124
126
|
"""Sets the file permission for the specified path."""
|
125
|
-
self.
|
127
|
+
self._fetch_factory('POST', '_api/meta', {'path': path, 'perm': int(permission)})(
|
126
128
|
headers={'Content-Type': 'application/www-form-urlencoded'}
|
127
129
|
)
|
128
130
|
|
129
131
|
def move(self, path: str, new_path: str):
|
130
132
|
"""Move file or directory to a new path."""
|
131
|
-
self.
|
133
|
+
self._fetch_factory('POST', '_api/meta', {'path': path, 'new_path': new_path})(
|
132
134
|
headers = {'Content-Type': 'application/www-form-urlencoded'}
|
133
135
|
)
|
134
136
|
|
135
137
|
def whoami(self) -> UserRecord:
|
136
138
|
"""Gets information about the current user."""
|
137
|
-
response = self.
|
139
|
+
response = self._fetch_factory('GET', '_api/whoami')()
|
138
140
|
return UserRecord(**response.json())
|
@@ -1,5 +1,5 @@
|
|
1
1
|
from pathlib import Path
|
2
|
-
import os
|
2
|
+
import os, hashlib
|
3
3
|
|
4
4
|
__default_dir = '.storage_data'
|
5
5
|
|
@@ -14,4 +14,7 @@ LARGE_BLOB_DIR.mkdir(exist_ok=True)
|
|
14
14
|
# https://sqlite.org/fasterthanfs.html
|
15
15
|
LARGE_FILE_BYTES = 8 * 1024 * 1024 # 8MB
|
16
16
|
MAX_FILE_BYTES = 512 * 1024 * 1024 # 512MB
|
17
|
-
MAX_BUNDLE_BYTES = 512 * 1024 * 1024 # 512MB
|
17
|
+
MAX_BUNDLE_BYTES = 512 * 1024 * 1024 # 512MB
|
18
|
+
|
19
|
+
def hash_credential(username, password):
|
20
|
+
return hashlib.sha256((username + password).encode()).hexdigest()
|
@@ -139,6 +139,7 @@ async def unique_cursor(is_write: bool = False):
|
|
139
139
|
finally:
|
140
140
|
await g_pool.release(connection_obj)
|
141
141
|
|
142
|
+
# todo: add exclusive transaction option
|
142
143
|
@asynccontextmanager
|
143
144
|
async def transaction():
|
144
145
|
async with unique_cursor(is_write=True) as cur:
|
@@ -3,7 +3,7 @@ from typing import Optional, overload, Literal, AsyncIterable
|
|
3
3
|
from abc import ABC
|
4
4
|
|
5
5
|
import urllib.parse
|
6
|
-
import
|
6
|
+
import uuid
|
7
7
|
import zipfile, io, asyncio
|
8
8
|
|
9
9
|
import aiosqlite, aiofiles
|
@@ -11,14 +11,11 @@ import aiofiles.os
|
|
11
11
|
|
12
12
|
from .connection_pool import execute_sql, unique_cursor, transaction
|
13
13
|
from .datatype import UserRecord, FileReadPermission, FileRecord, DirectoryRecord, PathContents
|
14
|
-
from .config import LARGE_BLOB_DIR
|
14
|
+
from .config import LARGE_BLOB_DIR, hash_credential
|
15
15
|
from .log import get_logger
|
16
16
|
from .utils import decode_uri_compnents
|
17
17
|
from .error import *
|
18
18
|
|
19
|
-
def hash_credential(username, password):
|
20
|
-
return hashlib.sha256((username + password).encode()).hexdigest()
|
21
|
-
|
22
19
|
class DBObjectBase(ABC):
|
23
20
|
logger = get_logger('database', global_instance=True)
|
24
21
|
_cur: aiosqlite.Cursor
|
@@ -153,16 +150,6 @@ class FileConn(DBObjectBase):
|
|
153
150
|
return []
|
154
151
|
return [self.parse_record(r) for r in res]
|
155
152
|
|
156
|
-
async def get_user_file_records(self, owner_id: int) -> list[FileRecord]:
|
157
|
-
await self.cur.execute("SELECT * FROM fmeta WHERE owner_id = ?", (owner_id, ))
|
158
|
-
res = await self.cur.fetchall()
|
159
|
-
return [self.parse_record(r) for r in res]
|
160
|
-
|
161
|
-
async def get_path_file_records(self, url: str) -> list[FileRecord]:
|
162
|
-
await self.cur.execute("SELECT * FROM fmeta WHERE url LIKE ?", (url + '%', ))
|
163
|
-
res = await self.cur.fetchall()
|
164
|
-
return [self.parse_record(r) for r in res]
|
165
|
-
|
166
153
|
async def list_root(self, *usernames: str) -> list[DirectoryRecord]:
|
167
154
|
"""
|
168
155
|
Efficiently list users' directories, if usernames is empty, list all users' directories.
|
@@ -340,25 +327,27 @@ class FileConn(DBObjectBase):
|
|
340
327
|
async def log_access(self, url: str):
|
341
328
|
await self.cur.execute("UPDATE fmeta SET access_time = CURRENT_TIMESTAMP WHERE url = ?", (url, ))
|
342
329
|
|
343
|
-
async def delete_file_record(self, url: str):
|
344
|
-
|
345
|
-
|
346
|
-
|
330
|
+
async def delete_file_record(self, url: str) -> Optional[FileRecord]:
|
331
|
+
res = await self.cur.execute("DELETE FROM fmeta WHERE url = ? RETURNING *", (url, ))
|
332
|
+
row = await res.fetchone()
|
333
|
+
if row is None:
|
334
|
+
raise FileNotFoundError(f"File {url} not found")
|
335
|
+
file_record = FileRecord(*row)
|
347
336
|
await self._user_size_dec(file_record.owner_id, file_record.file_size)
|
348
337
|
self.logger.info(f"Deleted fmeta {url}")
|
338
|
+
return file_record
|
349
339
|
|
350
|
-
async def delete_user_file_records(self, owner_id: int):
|
340
|
+
async def delete_user_file_records(self, owner_id: int) -> list[FileRecord]:
|
351
341
|
cursor = await self.cur.execute("SELECT * FROM fmeta WHERE owner_id = ?", (owner_id, ))
|
352
342
|
res = await cursor.fetchall()
|
353
|
-
await self.cur.execute("DELETE FROM fmeta WHERE owner_id = ?", (owner_id, ))
|
354
343
|
await self.cur.execute("DELETE FROM usize WHERE user_id = ?", (owner_id, ))
|
355
|
-
self.
|
344
|
+
res = await self.cur.execute("DELETE FROM fmeta WHERE owner_id = ? RETURNING *", (owner_id, ))
|
345
|
+
ret = [self.parse_record(r) for r in await res.fetchall()]
|
346
|
+
self.logger.info(f"Deleted {len(ret)} file(s) for user {owner_id}") # type: ignore
|
347
|
+
return ret
|
356
348
|
|
357
|
-
async def delete_path_records(self, path: str):
|
349
|
+
async def delete_path_records(self, path: str, under_user_id: Optional[int] = None) -> list[FileRecord]:
|
358
350
|
"""Delete all records with url starting with path"""
|
359
|
-
cursor = await self.cur.execute("SELECT * FROM fmeta WHERE url LIKE ?", (path + '%', ))
|
360
|
-
all_f_rec = await cursor.fetchall()
|
361
|
-
|
362
351
|
# update user size
|
363
352
|
cursor = await self.cur.execute("SELECT DISTINCT owner_id FROM fmeta WHERE url LIKE ?", (path + '%', ))
|
364
353
|
res = await cursor.fetchall()
|
@@ -368,8 +357,16 @@ class FileConn(DBObjectBase):
|
|
368
357
|
if size is not None:
|
369
358
|
await self._user_size_dec(r[0], size[0])
|
370
359
|
|
371
|
-
|
372
|
-
|
360
|
+
# if any new records are created here, the size update may be inconsistent
|
361
|
+
# but it's not a big deal...
|
362
|
+
|
363
|
+
if under_user_id is None:
|
364
|
+
res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? RETURNING *", (path + '%', ))
|
365
|
+
else:
|
366
|
+
res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? AND owner_id = ? RETURNING *", (path + '%', under_user_id))
|
367
|
+
all_f_rec = await res.fetchall()
|
368
|
+
self.logger.info(f"Deleted {len(all_f_rec)} file(s) for path {path}") # type: ignore
|
369
|
+
return [self.parse_record(r) for r in all_f_rec]
|
373
370
|
|
374
371
|
async def set_file_blob(self, file_id: str, blob: bytes):
|
375
372
|
await self.cur.execute("INSERT OR REPLACE INTO blobs.fdata (file_id, data) VALUES (?, ?)", (file_id, blob))
|
@@ -546,28 +543,37 @@ class Database:
|
|
546
543
|
|
547
544
|
return blob
|
548
545
|
|
549
|
-
async def delete_file(self, url: str) -> Optional[FileRecord]:
|
546
|
+
async def delete_file(self, url: str, assure_user: Optional[UserRecord] = None) -> Optional[FileRecord]:
|
550
547
|
validate_url(url)
|
551
548
|
|
552
549
|
async with transaction() as cur:
|
553
550
|
fconn = FileConn(cur)
|
554
|
-
r = await fconn.
|
551
|
+
r = await fconn.delete_file_record(url)
|
555
552
|
if r is None:
|
556
553
|
return None
|
554
|
+
if assure_user is not None:
|
555
|
+
if r.owner_id != assure_user.id:
|
556
|
+
# will rollback
|
557
|
+
raise PermissionDeniedError(f"Permission denied: {assure_user.username} cannot delete file {url}")
|
557
558
|
f_id = r.file_id
|
558
|
-
await fconn.delete_file_record(url)
|
559
559
|
if r.external:
|
560
560
|
await fconn.delete_file_blob_external(f_id)
|
561
561
|
else:
|
562
562
|
await fconn.delete_file_blob(f_id)
|
563
563
|
return r
|
564
564
|
|
565
|
-
async def move_file(self, old_url: str, new_url: str):
|
565
|
+
async def move_file(self, old_url: str, new_url: str, ensure_user: Optional[UserRecord] = None):
|
566
566
|
validate_url(old_url)
|
567
567
|
validate_url(new_url)
|
568
568
|
|
569
569
|
async with transaction() as cur:
|
570
570
|
fconn = FileConn(cur)
|
571
|
+
r = await fconn.get_file_record(old_url)
|
572
|
+
if r is None:
|
573
|
+
raise FileNotFoundError(f"File {old_url} not found")
|
574
|
+
if ensure_user is not None:
|
575
|
+
if r.owner_id != ensure_user.id:
|
576
|
+
raise PermissionDeniedError(f"Permission denied: {ensure_user.username} cannot move file {old_url}")
|
571
577
|
await fconn.move_file(old_url, new_url)
|
572
578
|
|
573
579
|
async def move_path(self, user: UserRecord, old_url: str, new_url: str):
|
@@ -615,16 +621,16 @@ class Database:
|
|
615
621
|
await fconn.delete_file_blob_external(external_ids[i])
|
616
622
|
|
617
623
|
|
618
|
-
async def delete_path(self, url: str):
|
624
|
+
async def delete_path(self, url: str, under_user: Optional[UserRecord] = None) -> Optional[list[FileRecord]]:
|
619
625
|
validate_url(url, is_file=False)
|
626
|
+
user_id = under_user.id if under_user is not None else None
|
620
627
|
|
621
628
|
async with transaction() as cur:
|
622
629
|
fconn = FileConn(cur)
|
623
|
-
records = await fconn.
|
630
|
+
records = await fconn.delete_path_records(url, user_id)
|
624
631
|
if not records:
|
625
632
|
return None
|
626
633
|
await self.__batch_delete_file_blobs(fconn, records)
|
627
|
-
await fconn.delete_path_records(url)
|
628
634
|
return records
|
629
635
|
|
630
636
|
async def delete_user(self, u: str | int):
|
@@ -633,12 +639,17 @@ class Database:
|
|
633
639
|
if user is None:
|
634
640
|
return
|
635
641
|
|
636
|
-
|
637
|
-
records = await fconn.get_user_file_records(user.id)
|
638
|
-
await self.__batch_delete_file_blobs(fconn, records)
|
639
|
-
await fconn.delete_user_file_records(user.id)
|
642
|
+
# no new files can be added since profile deletion
|
640
643
|
uconn = UserConn(cur)
|
641
644
|
await uconn.delete_user(user.username)
|
645
|
+
|
646
|
+
fconn = FileConn(cur)
|
647
|
+
records = await fconn.delete_user_file_records(user.id)
|
648
|
+
await self.__batch_delete_file_blobs(fconn, records)
|
649
|
+
|
650
|
+
# make sure the user's directory is deleted,
|
651
|
+
# may contain admin's files, but delete them all
|
652
|
+
await fconn.delete_path_records(user.username + '/')
|
642
653
|
|
643
654
|
async def iter_path(self, top_url: str, urls: Optional[list[str]]) -> AsyncIterable[tuple[FileRecord, bytes | AsyncIterable[bytes]]]:
|
644
655
|
async with unique_cursor() as cur:
|
@@ -76,6 +76,11 @@ async def get_current_user(
|
|
76
76
|
raise HTTPException(status_code=401, detail="Invalid token")
|
77
77
|
return user
|
78
78
|
|
79
|
+
async def registered_user(user: UserRecord = Depends(get_current_user)):
|
80
|
+
if user.id == 0:
|
81
|
+
raise HTTPException(status_code=401, detail="Permission denied")
|
82
|
+
return user
|
83
|
+
|
79
84
|
app = FastAPI(docs_url=None, redoc_url=None, lifespan=lifespan)
|
80
85
|
app.add_middleware(
|
81
86
|
CORSMiddleware,
|
@@ -186,11 +191,8 @@ async def put_file(
|
|
186
191
|
path: str,
|
187
192
|
conflict: Literal["overwrite", "skip", "abort"] = "abort",
|
188
193
|
permission: int = 0,
|
189
|
-
user: UserRecord = Depends(
|
194
|
+
user: UserRecord = Depends(registered_user)):
|
190
195
|
path = ensure_uri_compnents(path)
|
191
|
-
if user.id == 0:
|
192
|
-
logger.debug("Reject put request from DECOY_USER")
|
193
|
-
raise HTTPException(status_code=401, detail="Permission denied")
|
194
196
|
if not path.startswith(f"{user.username}/") and not user.is_admin:
|
195
197
|
logger.debug(f"Reject put request from {user.username} to {path}")
|
196
198
|
raise HTTPException(status_code=403, detail="Permission denied")
|
@@ -217,6 +219,8 @@ async def put_file(
|
|
217
219
|
}, content=json.dumps({"url": path}))
|
218
220
|
# remove the old file
|
219
221
|
exists_flag = True
|
222
|
+
if not user.is_admin and not file_record.owner_id == user.id:
|
223
|
+
raise HTTPException(status_code=403, detail="Permission denied, cannot overwrite other's file")
|
220
224
|
await db.delete_file(path)
|
221
225
|
|
222
226
|
# check content-type
|
@@ -267,19 +271,17 @@ async def put_file(
|
|
267
271
|
|
268
272
|
@router_fs.delete("/{path:path}")
|
269
273
|
@handle_exception
|
270
|
-
async def delete_file(path: str, user: UserRecord = Depends(
|
274
|
+
async def delete_file(path: str, user: UserRecord = Depends(registered_user)):
|
271
275
|
path = ensure_uri_compnents(path)
|
272
|
-
if user.id == 0:
|
273
|
-
raise HTTPException(status_code=401, detail="Permission denied")
|
274
276
|
if not path.startswith(f"{user.username}/") and not user.is_admin:
|
275
277
|
raise HTTPException(status_code=403, detail="Permission denied")
|
276
278
|
|
277
279
|
logger.info(f"DELETE {path}, user: {user.username}")
|
278
280
|
|
279
281
|
if path.endswith("/"):
|
280
|
-
res = await db.delete_path(path)
|
282
|
+
res = await db.delete_path(path, user if not user.is_admin else None)
|
281
283
|
else:
|
282
|
-
res = await db.delete_file(path)
|
284
|
+
res = await db.delete_file(path, user if not user.is_admin else None)
|
283
285
|
|
284
286
|
await db.record_user_activity(user.username)
|
285
287
|
if res:
|
@@ -291,21 +293,23 @@ router_api = APIRouter(prefix="/_api")
|
|
291
293
|
|
292
294
|
@router_api.get("/bundle")
|
293
295
|
@handle_exception
|
294
|
-
async def bundle_files(path: str, user: UserRecord = Depends(
|
296
|
+
async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
|
295
297
|
logger.info(f"GET bundle({path}), user: {user.username}")
|
296
|
-
if user.id == 0:
|
297
|
-
raise HTTPException(status_code=401, detail="Permission denied")
|
298
298
|
path = ensure_uri_compnents(path)
|
299
299
|
assert path.endswith("/") or path == ""
|
300
300
|
|
301
301
|
if not path == "" and path[0] == "/": # adapt to both /path and path
|
302
302
|
path = path[1:]
|
303
303
|
|
304
|
-
owner_records_cache = {} # cache owner records, ID -> UserRecord
|
304
|
+
owner_records_cache: dict[int, UserRecord] = {} # cache owner records, ID -> UserRecord
|
305
305
|
async def is_access_granted(file_record: FileRecord):
|
306
306
|
owner_id = file_record.owner_id
|
307
307
|
owner = owner_records_cache.get(owner_id, None)
|
308
308
|
if owner is None:
|
309
|
+
async with unique_cursor() as conn:
|
310
|
+
uconn = UserConn(conn)
|
311
|
+
owner = await uconn.get_user_by_id(owner_id)
|
312
|
+
assert owner is not None, "Owner not found"
|
309
313
|
owner_records_cache[owner_id] = owner
|
310
314
|
|
311
315
|
allow_access, _ = check_user_permission(user, owner, file_record)
|
@@ -334,7 +338,7 @@ async def bundle_files(path: str, user: UserRecord = Depends(get_current_user)):
|
|
334
338
|
|
335
339
|
@router_api.get("/meta")
|
336
340
|
@handle_exception
|
337
|
-
async def get_file_meta(path: str, user: UserRecord = Depends(
|
341
|
+
async def get_file_meta(path: str, user: UserRecord = Depends(registered_user)):
|
338
342
|
logger.info(f"GET meta({path}), user: {user.username}")
|
339
343
|
path = ensure_uri_compnents(path)
|
340
344
|
async with unique_cursor() as conn:
|
@@ -352,10 +356,8 @@ async def update_file_meta(
|
|
352
356
|
path: str,
|
353
357
|
perm: Optional[int] = None,
|
354
358
|
new_path: Optional[str] = None,
|
355
|
-
user: UserRecord = Depends(
|
359
|
+
user: UserRecord = Depends(registered_user)
|
356
360
|
):
|
357
|
-
if user.id == 0:
|
358
|
-
raise HTTPException(status_code=401, detail="Permission denied")
|
359
361
|
path = ensure_uri_compnents(path)
|
360
362
|
if path.startswith("/"):
|
361
363
|
path = path[1:]
|
@@ -374,7 +376,7 @@ async def update_file_meta(
|
|
374
376
|
if new_path is not None:
|
375
377
|
new_path = ensure_uri_compnents(new_path)
|
376
378
|
logger.info(f"Update path of {path} to {new_path}")
|
377
|
-
await db.move_file(path, new_path)
|
379
|
+
await db.move_file(path, new_path, user)
|
378
380
|
|
379
381
|
# directory
|
380
382
|
else:
|
@@ -389,9 +391,7 @@ async def update_file_meta(
|
|
389
391
|
|
390
392
|
@router_api.get("/whoami")
|
391
393
|
@handle_exception
|
392
|
-
async def whoami(user: UserRecord = Depends(
|
393
|
-
if user.id == 0:
|
394
|
-
raise HTTPException(status_code=401, detail="Login required")
|
394
|
+
async def whoami(user: UserRecord = Depends(registered_user)):
|
395
395
|
user.credential = "__HIDDEN__"
|
396
396
|
return user
|
397
397
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "lfss"
|
3
|
-
version = "0.7.
|
3
|
+
version = "0.7.3"
|
4
4
|
description = "Lightweight file storage service"
|
5
5
|
authors = ["li, mengxun <limengxun45@outlook.com>"]
|
6
6
|
readme = "Readme.md"
|
@@ -15,6 +15,10 @@ aiosqlite = "0.*"
|
|
15
15
|
aiofiles = "23.*"
|
16
16
|
mimesniff = "1.*"
|
17
17
|
|
18
|
+
[tool.poetry.dev-dependencies]
|
19
|
+
pytest = "*"
|
20
|
+
pytest-html = "*"
|
21
|
+
|
18
22
|
[tool.poetry.scripts]
|
19
23
|
lfss-serve = "lfss.cli.serve:main"
|
20
24
|
lfss-user = "lfss.cli.user:main"
|
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
|