lfss 0.11.5__tar.gz → 0.11.6__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.11.5 → lfss-0.11.6}/PKG-INFO +2 -2
- {lfss-0.11.5 → lfss-0.11.6}/docs/Client.md +3 -0
- {lfss-0.11.5 → lfss-0.11.6}/docs/changelog.md +5 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/api.js +1 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/scripts.js +2 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/api/connector.py +8 -2
- {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/cli.py +40 -6
- {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/user.py +6 -3
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/database.py +16 -10
- {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/app_native.py +11 -1
- {lfss-0.11.5 → lfss-0.11.6}/pyproject.toml +2 -2
- {lfss-0.11.5 → lfss-0.11.6}/Readme.md +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/docs/Enviroment_variables.md +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/docs/Known_issues.md +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/docs/Permission.md +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/docs/Webdav.md +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/index.html +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/info.css +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/info.js +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/login.css +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/login.js +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/popup.css +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/popup.js +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/state.js +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/styles.css +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/thumb.css +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/thumb.js +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/frontend/utils.js +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/api/__init__.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/__init__.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/balance.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/log.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/panel.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/serve.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/cli/vacuum.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/__init__.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/bounded_pool.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/config.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/connection_pool.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/datatype.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/error.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/log.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/thumb.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/eng/utils.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/sql/init.sql +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/sql/pragma.sql +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/app.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/app_base.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/app_dav.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/common_impl.py +0 -0
- {lfss-0.11.5 → lfss-0.11.6}/lfss/svc/request_log.py +0 -0
@@ -10,6 +10,9 @@ pip install lfss --no-deps
|
|
10
10
|
Then set the `LFSS_ENDPOINT`, `LFSS_TOKEN` environment variables,
|
11
11
|
then you can use the following commands:
|
12
12
|
```sh
|
13
|
+
# Check current user information
|
14
|
+
lfss whoami
|
15
|
+
|
13
16
|
# Query a path
|
14
17
|
lfss query remote/file[/or_dir/]
|
15
18
|
|
@@ -1,5 +1,10 @@
|
|
1
1
|
## 0.11
|
2
2
|
|
3
|
+
### 0.11.6
|
4
|
+
- Hint copy and move success for frontend.
|
5
|
+
- Add query user info and list peers api.
|
6
|
+
- Add user with random password if not specified.
|
7
|
+
|
3
8
|
### 0.11.5
|
4
9
|
- Script entry default to client CLI.
|
5
10
|
- Fix single file download name deduce with decoding.
|
@@ -249,6 +249,7 @@ export default class Connector {
|
|
249
249
|
/**
|
250
250
|
* @param {string} path - the path to the file directory, should ends with '/'
|
251
251
|
* @returns {Promise<PathListResponse>} - the promise of the request
|
252
|
+
* NOTE: will deprecated in future
|
252
253
|
*/
|
253
254
|
async listPath(path){
|
254
255
|
path = this._sanitizeDirPath(path);
|
@@ -347,6 +347,7 @@ async function refreshFileList(){
|
|
347
347
|
console.log("Moving", dirurl, "to", dstPath);
|
348
348
|
conn.move(dirurl, dstPath)
|
349
349
|
.then(() => {
|
350
|
+
showPopup('Successfully moved path.', {level: 'success', timeout: 3000});
|
350
351
|
refreshFileList();
|
351
352
|
},
|
352
353
|
(err) => {
|
@@ -371,6 +372,7 @@ async function refreshFileList(){
|
|
371
372
|
console.log("Copying", dirurl, "to", dstPath);
|
372
373
|
conn.copy(dirurl, dstPath)
|
373
374
|
.then(() => {
|
375
|
+
showPopup('Successfully copied path.', {level: 'success', timeout: 3000});
|
374
376
|
refreshFileList();
|
375
377
|
},
|
376
378
|
(err) => {
|
@@ -8,7 +8,7 @@ import urllib.parse
|
|
8
8
|
from tempfile import SpooledTemporaryFile
|
9
9
|
from lfss.eng.error import PathNotFoundError
|
10
10
|
from lfss.eng.datatype import (
|
11
|
-
FileReadPermission, FileRecord, DirectoryRecord, UserRecord, PathContents,
|
11
|
+
FileReadPermission, FileRecord, DirectoryRecord, UserRecord, PathContents, AccessLevel,
|
12
12
|
FileSortKey, DirSortKey
|
13
13
|
)
|
14
14
|
from lfss.eng.utils import ensure_uri_components
|
@@ -316,4 +316,10 @@ class Connector:
|
|
316
316
|
def whoami(self) -> UserRecord:
|
317
317
|
"""Gets information about the current user."""
|
318
318
|
response = self._fetch_factory('GET', '_api/whoami')()
|
319
|
-
return UserRecord(**response.json())
|
319
|
+
return UserRecord(**response.json())
|
320
|
+
|
321
|
+
def list_peers(self, level: AccessLevel = AccessLevel.READ, incoming: bool = False) -> list[UserRecord]:
|
322
|
+
"""List all users that have at least the given access level to the current user."""
|
323
|
+
response = self._fetch_factory('GET', '_api/list-peers', {'level': int(level), 'incoming': incoming})()
|
324
|
+
users = [UserRecord(**u) for u in response.json()]
|
325
|
+
return users
|
@@ -1,8 +1,8 @@
|
|
1
1
|
from pathlib import Path
|
2
2
|
import argparse, typing, sys
|
3
3
|
from lfss.api import Connector, upload_directory, upload_file, download_file, download_directory
|
4
|
-
from lfss.eng.datatype import FileReadPermission, FileSortKey, DirSortKey
|
5
|
-
from lfss.eng.utils import decode_uri_components
|
4
|
+
from lfss.eng.datatype import FileReadPermission, FileSortKey, DirSortKey, AccessLevel
|
5
|
+
from lfss.eng.utils import decode_uri_components, fmt_storage_size
|
6
6
|
from . import catch_request_error, line_sep
|
7
7
|
|
8
8
|
def parse_permission(s: str) -> FileReadPermission:
|
@@ -10,14 +10,27 @@ def parse_permission(s: str) -> FileReadPermission:
|
|
10
10
|
if p.name.lower() == s.lower():
|
11
11
|
return p
|
12
12
|
raise ValueError(f"Invalid permission {s}")
|
13
|
+
def parse_access_level(s: str) -> AccessLevel:
|
14
|
+
for p in AccessLevel:
|
15
|
+
if p.name.lower() == s.lower():
|
16
|
+
return p
|
17
|
+
raise ValueError(f"Invalid access level {s}")
|
13
18
|
|
14
19
|
def parse_arguments():
|
15
20
|
parser = argparse.ArgumentParser(description="Client-side command line interface, set LFSS_ENDPOINT and LFSS_TOKEN environment variables for authentication.")
|
16
21
|
|
17
22
|
sp = parser.add_subparsers(dest="command", required=True)
|
18
23
|
|
24
|
+
# whoami
|
25
|
+
sp_whoami = sp.add_parser("whoami", help="Show current user information")
|
26
|
+
|
27
|
+
# list peers
|
28
|
+
sp_peers = sp.add_parser("peers", help="Query users that you have access to or users that have access to you")
|
29
|
+
sp_peers.add_argument('-l', "--level", type=parse_access_level, default=AccessLevel.READ, help="Access level filter")
|
30
|
+
sp_peers.add_argument('-i', '--incoming', action='store_true', help="List users that have access to you (rather than you have access to them")
|
31
|
+
|
19
32
|
# upload
|
20
|
-
sp_upload = sp.add_parser("upload", help="Upload
|
33
|
+
sp_upload = sp.add_parser("upload", help="Upload file(s)")
|
21
34
|
sp_upload.add_argument("src", help="Source file or directory", type=str)
|
22
35
|
sp_upload.add_argument("dst", help="Destination url path", type=str)
|
23
36
|
sp_upload.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
@@ -28,7 +41,7 @@ def parse_arguments():
|
|
28
41
|
sp_upload.add_argument("--retries", type=int, default=0, help="Number of retries")
|
29
42
|
|
30
43
|
# download
|
31
|
-
sp_download = sp.add_parser("download", help="Download
|
44
|
+
sp_download = sp.add_parser("download", help="Download file(s)")
|
32
45
|
sp_download.add_argument("src", help="Source url path", type=str)
|
33
46
|
sp_download.add_argument("dst", help="Destination file or directory", type=str)
|
34
47
|
sp_download.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
@@ -38,7 +51,7 @@ def parse_arguments():
|
|
38
51
|
sp_download.add_argument("--retries", type=int, default=0, help="Number of retries")
|
39
52
|
|
40
53
|
# query
|
41
|
-
sp_query = sp.add_parser("query", help="Query
|
54
|
+
sp_query = sp.add_parser("query", help="Query file or directories metadata from the server")
|
42
55
|
sp_query.add_argument("path", help="Path to query", nargs="*", type=str)
|
43
56
|
|
44
57
|
# list directories
|
@@ -65,7 +78,28 @@ def parse_arguments():
|
|
65
78
|
def main():
|
66
79
|
args = parse_arguments()
|
67
80
|
connector = Connector()
|
68
|
-
if args.command == "
|
81
|
+
if args.command == "whoami":
|
82
|
+
with catch_request_error():
|
83
|
+
user = connector.whoami()
|
84
|
+
print("Username:", user.username)
|
85
|
+
print("User ID:", user.id)
|
86
|
+
print("Is Admin:", bool(user.is_admin))
|
87
|
+
print("Max Storage:", fmt_storage_size(user.max_storage))
|
88
|
+
print("Default Permission:", user.permission.name)
|
89
|
+
print("Created At:", user.create_time)
|
90
|
+
print("Last Active:", user.last_active)
|
91
|
+
|
92
|
+
elif args.command == "peers":
|
93
|
+
with catch_request_error():
|
94
|
+
users = connector.list_peers(level=args.level, incoming=args.incoming)
|
95
|
+
if not args.incoming:
|
96
|
+
print(f"Peers that you have {args.level.name} access to:")
|
97
|
+
else:
|
98
|
+
print(f"Peers that have {args.level.name} access to you:")
|
99
|
+
for i, u in enumerate(line_sep(users)):
|
100
|
+
print(f"[{i+1}] {u.username} (id={u.id})")
|
101
|
+
|
102
|
+
elif args.command == "upload":
|
69
103
|
src_path = Path(args.src)
|
70
104
|
if src_path.is_dir():
|
71
105
|
failed_upload = upload_directory(
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import argparse, asyncio, os
|
1
|
+
import argparse, asyncio, os, secrets
|
2
2
|
from contextlib import asynccontextmanager
|
3
3
|
from .cli import parse_permission, FileReadPermission
|
4
4
|
from ..eng.utils import parse_storage_size, fmt_storage_size
|
@@ -18,10 +18,10 @@ async def _main():
|
|
18
18
|
sp = parser.add_subparsers(dest='subparser_name', required=True)
|
19
19
|
sp_add = sp.add_parser('add')
|
20
20
|
sp_add.add_argument('username', type=str)
|
21
|
-
sp_add.add_argument('password', type=str)
|
21
|
+
sp_add.add_argument('password', nargs='?', type=str, default=None)
|
22
22
|
sp_add.add_argument('--admin', action='store_true', help='Set user as admin')
|
23
23
|
sp_add.add_argument("--permission", type=parse_permission, default=FileReadPermission.UNSET, help="File permission, can be public, protected, private, or unset")
|
24
|
-
sp_add.add_argument('--max-storage', type=parse_storage_size, default="
|
24
|
+
sp_add.add_argument('--max-storage', type=parse_storage_size, default="10G", help="Maximum storage size, e.g. 1G, 100M, 10K, default is 10G")
|
25
25
|
|
26
26
|
sp_delete = sp.add_parser('delete')
|
27
27
|
sp_delete.add_argument('username', type=str)
|
@@ -58,6 +58,9 @@ async def _main():
|
|
58
58
|
|
59
59
|
if args.subparser_name == 'add':
|
60
60
|
async with get_uconn() as uconn:
|
61
|
+
if args.password is None:
|
62
|
+
passwd = secrets.token_urlsafe(16)
|
63
|
+
args.password = passwd
|
61
64
|
await uconn.create_user(args.username, args.password, args.admin, max_storage=args.max_storage, permission=args.permission)
|
62
65
|
user = await uconn.get_user(args.username)
|
63
66
|
assert user is not None
|
@@ -163,25 +163,31 @@ class UserConn(DBObjectBase):
|
|
163
163
|
return AccessLevel.NONE
|
164
164
|
return AccessLevel(res[0])
|
165
165
|
|
166
|
-
async def list_peer_users(self,
|
166
|
+
async def list_peer_users(self, user: int | str, level: AccessLevel, incoming = False) -> list[UserRecord]:
|
167
167
|
"""
|
168
|
-
|
169
|
-
|
168
|
+
if not incoming:
|
169
|
+
List all users that user can do [AliasLevel] to, with level >= level,
|
170
|
+
else:
|
171
|
+
List all users that can do [AliasLevel] to user, with level >= level
|
172
|
+
Note: the returned list does not include user and is not apporiate for admin (who has all permissions for all users)
|
170
173
|
"""
|
171
174
|
assert int(level) > AccessLevel.NONE, f"Invalid level, {level}"
|
172
|
-
|
175
|
+
aim_field = 'src_user_id' if incoming else 'dst_user_id'
|
176
|
+
query_field = 'dst_user_id' if incoming else 'src_user_id'
|
177
|
+
|
178
|
+
match user:
|
173
179
|
case int():
|
174
|
-
await self.cur.execute("""
|
180
|
+
await self.cur.execute(f"""
|
175
181
|
SELECT * FROM user WHERE id IN (
|
176
|
-
SELECT
|
182
|
+
SELECT {aim_field} FROM upeer WHERE {query_field} = ? AND access_level >= ?
|
177
183
|
)
|
178
|
-
""", (
|
184
|
+
""", (user, int(level)))
|
179
185
|
case str():
|
180
|
-
await self.cur.execute("""
|
186
|
+
await self.cur.execute(f"""
|
181
187
|
SELECT * FROM user WHERE id IN (
|
182
|
-
SELECT
|
188
|
+
SELECT {aim_field} FROM upeer WHERE {query_field} = (SELECT id FROM user WHERE username = ?) AND access_level >= ?
|
183
189
|
)
|
184
|
-
""", (
|
190
|
+
""", (user, int(level)))
|
185
191
|
case _:
|
186
192
|
raise ValueError("Invalid arguments")
|
187
193
|
res = await self.cur.fetchall()
|
@@ -8,7 +8,7 @@ from fastapi.exceptions import HTTPException
|
|
8
8
|
from ..eng.utils import ensure_uri_components
|
9
9
|
from ..eng.config import MAX_MEM_FILE_BYTES
|
10
10
|
from ..eng.connection_pool import unique_cursor
|
11
|
-
from ..eng.database import check_file_read_permission, check_path_permission, FileConn, delayed_log_access
|
11
|
+
from ..eng.database import check_file_read_permission, check_path_permission, FileConn, delayed_log_access, UserConn
|
12
12
|
from ..eng.datatype import (
|
13
13
|
FileReadPermission, UserRecord, AccessLevel,
|
14
14
|
FileSortKey, DirSortKey
|
@@ -185,6 +185,16 @@ async def copy_file(
|
|
185
185
|
):
|
186
186
|
return await copy_impl(src_path = src, dst_path = dst, op_user = user)
|
187
187
|
|
188
|
+
@router_api.get("/list-peers")
|
189
|
+
@handle_exception
|
190
|
+
async def list_peers(user: UserRecord = Depends(registered_user), level: AccessLevel = AccessLevel.READ, incoming: bool = False):
|
191
|
+
async with unique_cursor() as conn:
|
192
|
+
uconn = UserConn(conn)
|
193
|
+
peer_users = await uconn.list_peer_users(user.id, level, incoming=incoming)
|
194
|
+
for u in peer_users:
|
195
|
+
u.credential = "__HIDDEN__"
|
196
|
+
return peer_users
|
197
|
+
|
188
198
|
async def validate_path_read_permission(path: str, user: UserRecord):
|
189
199
|
if not path.endswith("/"):
|
190
200
|
raise HTTPException(status_code=400, detail="Path must end with /")
|
@@ -1,7 +1,7 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "lfss"
|
3
|
-
version = "0.11.
|
4
|
-
description = "
|
3
|
+
version = "0.11.6"
|
4
|
+
description = "Lite file storage service"
|
5
5
|
authors = ["Li, Mengxun <mengxunli@whu.edu.cn>"]
|
6
6
|
readme = "Readme.md"
|
7
7
|
homepage = "https://github.com/MenxLi/lfss"
|
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
|
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
|