lfss 0.11.3__py3-none-any.whl → 0.11.5__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.
- docs/Client.md +37 -0
- docs/changelog.md +16 -0
- lfss/api/__init__.py +3 -3
- lfss/api/connector.py +2 -2
- lfss/cli/cli.py +3 -3
- lfss/eng/database.py +57 -25
- lfss/eng/utils.py +4 -4
- lfss/svc/app_dav.py +6 -6
- lfss/svc/app_native.py +11 -11
- lfss/svc/common_impl.py +7 -7
- {lfss-0.11.3.dist-info → lfss-0.11.5.dist-info}/METADATA +1 -1
- {lfss-0.11.3.dist-info → lfss-0.11.5.dist-info}/RECORD +14 -13
- {lfss-0.11.3.dist-info → lfss-0.11.5.dist-info}/entry_points.txt +1 -0
- {lfss-0.11.3.dist-info → lfss-0.11.5.dist-info}/WHEEL +0 -0
docs/Client.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
# Client-side CLI tools
|
3
|
+
|
4
|
+
To install python CLI tools without dependencies (to avoid conflicts with your existing packages):
|
5
|
+
```sh
|
6
|
+
pip install requests
|
7
|
+
pip install lfss --no-deps
|
8
|
+
```
|
9
|
+
|
10
|
+
Then set the `LFSS_ENDPOINT`, `LFSS_TOKEN` environment variables,
|
11
|
+
then you can use the following commands:
|
12
|
+
```sh
|
13
|
+
# Query a path
|
14
|
+
lfss query remote/file[/or_dir/]
|
15
|
+
|
16
|
+
# List directories of a specified path
|
17
|
+
lfss list-dirs remote/dir/
|
18
|
+
|
19
|
+
# List files of a specified path,
|
20
|
+
# with pagination and sorting
|
21
|
+
lfss list-files --offset 0 --limit 100 --order access_time remote/dir/
|
22
|
+
|
23
|
+
# Upload a file
|
24
|
+
lfss upload local/file.txt remote/file.txt
|
25
|
+
|
26
|
+
# Upload a directory, note the ending slashes
|
27
|
+
lfss upload local/dir/ remote/dir/
|
28
|
+
|
29
|
+
# Download a file
|
30
|
+
lfss download remote/file.txt local/file.txt
|
31
|
+
|
32
|
+
# Download a directory, with verbose output and 8 concurrent jobs
|
33
|
+
# Overwrite existing files
|
34
|
+
lfss download -v -j 8 --conflict overwrite remote/dir/ local/dir/
|
35
|
+
```
|
36
|
+
|
37
|
+
More commands can be found using `lfss-cli --help`.
|
docs/changelog.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
## 0.11
|
2
2
|
|
3
|
+
### 0.11.5
|
4
|
+
- Script entry default to client CLI.
|
5
|
+
- Fix single file download name deduce with decoding.
|
6
|
+
- Fix code misspell (minor).
|
7
|
+
|
8
|
+
### 0.11.4
|
9
|
+
- Fix SQL query for LIKE clause to escape special characters in path.
|
10
|
+
|
11
|
+
### 0.11.3
|
12
|
+
- Add method to get multiple files, maybe with content, at once.
|
13
|
+
- Allow copy directory files that the user is not the owner of.
|
14
|
+
- Environment variables to set origin and disable file logging.
|
15
|
+
- Fix error handling for some endpoints.
|
16
|
+
- Redirect CLI error output to stderr.
|
17
|
+
- Increase thumb image size to 64x64.
|
18
|
+
|
3
19
|
### 0.11.2
|
4
20
|
- Improve frontend directory upload feedback.
|
5
21
|
- Set default large file threashold to 1M.
|
lfss/api/__init__.py
CHANGED
@@ -2,7 +2,7 @@ import os, time, pathlib
|
|
2
2
|
from threading import Lock
|
3
3
|
from .connector import Connector
|
4
4
|
from ..eng.datatype import FileRecord
|
5
|
-
from ..eng.utils import
|
5
|
+
from ..eng.utils import decode_uri_components
|
6
6
|
from ..eng.bounded_pool import BoundedThreadPoolExecutor
|
7
7
|
|
8
8
|
def upload_file(
|
@@ -105,7 +105,7 @@ def download_file(
|
|
105
105
|
assert not src_url.endswith('/'), "Source URL must not end with a slash."
|
106
106
|
while this_try <= n_retries:
|
107
107
|
if os.path.isdir(file_path):
|
108
|
-
fname = src_url.split('/')[-1]
|
108
|
+
fname = decode_uri_components(src_url.split('/')[-1])
|
109
109
|
file_path = os.path.join(file_path, fname)
|
110
110
|
|
111
111
|
if not overwrite and os.path.exists(file_path):
|
@@ -176,7 +176,7 @@ def download_directory(
|
|
176
176
|
with _counter_lock:
|
177
177
|
_counter += 1
|
178
178
|
this_count = _counter
|
179
|
-
dst_path = f"{directory}{os.path.relpath(
|
179
|
+
dst_path = f"{directory}{os.path.relpath(decode_uri_components(src_url), decode_uri_components(src_path))}"
|
180
180
|
if verbose:
|
181
181
|
print(f"[{this_count}/{file_count}] Downloading {src_url} to {dst_path}")
|
182
182
|
|
lfss/api/connector.py
CHANGED
@@ -11,7 +11,7 @@ from lfss.eng.datatype import (
|
|
11
11
|
FileReadPermission, FileRecord, DirectoryRecord, UserRecord, PathContents,
|
12
12
|
FileSortKey, DirSortKey
|
13
13
|
)
|
14
|
-
from lfss.eng.utils import
|
14
|
+
from lfss.eng.utils import ensure_uri_components
|
15
15
|
|
16
16
|
_default_endpoint = os.environ.get('LFSS_ENDPOINT', 'http://localhost:8000')
|
17
17
|
_default_token = os.environ.get('LFSS_TOKEN', '')
|
@@ -74,7 +74,7 @@ class Connector:
|
|
74
74
|
):
|
75
75
|
if path.startswith('/'):
|
76
76
|
path = path[1:]
|
77
|
-
path =
|
77
|
+
path = ensure_uri_components(path)
|
78
78
|
def f(**kwargs):
|
79
79
|
search_params_t = [
|
80
80
|
(k, str(v).lower() if isinstance(v, bool) else v)
|
lfss/cli/cli.py
CHANGED
@@ -2,7 +2,7 @@ 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
4
|
from lfss.eng.datatype import FileReadPermission, FileSortKey, DirSortKey
|
5
|
-
from lfss.eng.utils import
|
5
|
+
from lfss.eng.utils import decode_uri_components
|
6
6
|
from . import catch_request_error, line_sep
|
7
7
|
|
8
8
|
def parse_permission(s: str) -> FileReadPermission:
|
@@ -143,7 +143,7 @@ def main():
|
|
143
143
|
order_desc=args.reverse,
|
144
144
|
)
|
145
145
|
for i, f in enumerate(line_sep(res)):
|
146
|
-
f.url =
|
146
|
+
f.url = decode_uri_components(f.url)
|
147
147
|
print(f"[{i+1}] {f if args.long else f.url}")
|
148
148
|
|
149
149
|
if len(res) == args.limit:
|
@@ -160,7 +160,7 @@ def main():
|
|
160
160
|
order_desc=args.reverse,
|
161
161
|
)
|
162
162
|
for i, d in enumerate(line_sep(res)):
|
163
|
-
d.url =
|
163
|
+
d.url = decode_uri_components(d.url)
|
164
164
|
print(f"[{i+1}] {d if args.long else d.url}")
|
165
165
|
|
166
166
|
if len(res) == args.limit:
|
lfss/eng/database.py
CHANGED
@@ -21,7 +21,7 @@ from .datatype import (
|
|
21
21
|
)
|
22
22
|
from .config import LARGE_BLOB_DIR, CHUNK_SIZE, LARGE_FILE_BYTES, MAX_MEM_FILE_BYTES
|
23
23
|
from .log import get_logger
|
24
|
-
from .utils import
|
24
|
+
from .utils import decode_uri_components, hash_credential, concurrent_wrap, debounce_async, static_vars
|
25
25
|
from .error import *
|
26
26
|
|
27
27
|
class DBObjectBase(ABC):
|
@@ -197,6 +197,11 @@ class FileConn(DBObjectBase):
|
|
197
197
|
def parse_record(record) -> FileRecord:
|
198
198
|
return FileRecord(*record)
|
199
199
|
|
200
|
+
@staticmethod
|
201
|
+
def escape_sqlike(url: str) -> str:
|
202
|
+
""" Escape a url for use in SQL LIKE clause (The % and _ characters) """
|
203
|
+
return url.replace('%', r'\%').replace('_', r'\_')
|
204
|
+
|
200
205
|
@overload
|
201
206
|
async def get_file_record(self, url: str, throw: Literal[True]) -> FileRecord: ...
|
202
207
|
@overload
|
@@ -246,9 +251,9 @@ class FileConn(DBObjectBase):
|
|
246
251
|
url, LENGTH(?) + 1,
|
247
252
|
INSTR(SUBSTR(url, LENGTH(?) + 1), '/')
|
248
253
|
) AS dirname
|
249
|
-
FROM fmeta WHERE url LIKE ? AND dirname != ''
|
254
|
+
FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND dirname != ''
|
250
255
|
)
|
251
|
-
""", (url, url, url + '%'))
|
256
|
+
""", (url, url, self.escape_sqlike(url) + '%'))
|
252
257
|
res = await cursor.fetchone()
|
253
258
|
assert res is not None, "Error: count_path_dirs"
|
254
259
|
return res[0]
|
@@ -271,11 +276,11 @@ class FileConn(DBObjectBase):
|
|
271
276
|
1 + LENGTH(?),
|
272
277
|
INSTR(SUBSTR(url, 1 + LENGTH(?)), '/')
|
273
278
|
) AS dirname
|
274
|
-
FROM fmeta WHERE url LIKE ? AND dirname != ''
|
279
|
+
FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND dirname != ''
|
275
280
|
""" \
|
276
281
|
+ (f"ORDER BY {order_by} {'DESC' if order_desc else 'ASC'}" if order_by else '') \
|
277
282
|
+ " LIMIT ? OFFSET ?"
|
278
|
-
cursor = await self.cur.execute(sql_qury, (url, url, url + '%', limit, offset))
|
283
|
+
cursor = await self.cur.execute(sql_qury, (url, url, self.escape_sqlike(url) + '%', limit, offset))
|
279
284
|
res = await cursor.fetchall()
|
280
285
|
dirs_str = [r[0] for r in res]
|
281
286
|
async def get_dir(dir_url):
|
@@ -290,9 +295,15 @@ class FileConn(DBObjectBase):
|
|
290
295
|
if not url.endswith('/'): url += '/'
|
291
296
|
if url == '/': url = ''
|
292
297
|
if flat:
|
293
|
-
cursor = await self.cur.execute(
|
298
|
+
cursor = await self.cur.execute(
|
299
|
+
"SELECT COUNT(*) FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
|
300
|
+
(self.escape_sqlike(url) + '%', )
|
301
|
+
)
|
294
302
|
else:
|
295
|
-
cursor = await self.cur.execute(
|
303
|
+
cursor = await self.cur.execute(
|
304
|
+
"SELECT COUNT(*) FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND url NOT LIKE ? ESCAPE '\\'",
|
305
|
+
(self.escape_sqlike(url) + '%', self.escape_sqlike(url) + '%/%')
|
306
|
+
)
|
296
307
|
res = await cursor.fetchone()
|
297
308
|
assert res is not None, "Error: count_path_files"
|
298
309
|
return res[0]
|
@@ -309,14 +320,14 @@ class FileConn(DBObjectBase):
|
|
309
320
|
if not url.endswith('/'): url += '/'
|
310
321
|
if url == '/': url = ''
|
311
322
|
|
312
|
-
sql_query = "SELECT * FROM fmeta WHERE url LIKE ?"
|
313
|
-
if not flat: sql_query += " AND url NOT LIKE ?"
|
323
|
+
sql_query = "SELECT * FROM fmeta WHERE url LIKE ? ESCAPE '\\'"
|
324
|
+
if not flat: sql_query += " AND url NOT LIKE ? ESCAPE '\\'"
|
314
325
|
if order_by: sql_query += f" ORDER BY {order_by} {'DESC' if order_desc else 'ASC'}"
|
315
326
|
sql_query += " LIMIT ? OFFSET ?"
|
316
327
|
if flat:
|
317
|
-
cursor = await self.cur.execute(sql_query, (url + '%', limit, offset))
|
328
|
+
cursor = await self.cur.execute(sql_query, (self.escape_sqlike(url) + '%', limit, offset))
|
318
329
|
else:
|
319
|
-
cursor = await self.cur.execute(sql_query, (url + '%', url + '%/%', limit, offset))
|
330
|
+
cursor = await self.cur.execute(sql_query, (self.escape_sqlike(url) + '%', self.escape_sqlike(url) + '%/%', limit, offset))
|
320
331
|
res = await cursor.fetchall()
|
321
332
|
files = [self.parse_record(r) for r in res]
|
322
333
|
return files
|
@@ -351,8 +362,8 @@ class FileConn(DBObjectBase):
|
|
351
362
|
MAX(access_time) as access_time,
|
352
363
|
COUNT(*) as n_files
|
353
364
|
FROM fmeta
|
354
|
-
WHERE url LIKE ?
|
355
|
-
""", (url + '%', ))
|
365
|
+
WHERE url LIKE ? ESCAPE '\\'
|
366
|
+
""", (self.escape_sqlike(url) + '%', ))
|
356
367
|
result = await cursor.fetchone()
|
357
368
|
if result is None or any(val is None for val in result):
|
358
369
|
raise PathNotFoundError(f"Path {url} not found")
|
@@ -376,10 +387,16 @@ class FileConn(DBObjectBase):
|
|
376
387
|
if not url.endswith('/'):
|
377
388
|
url += '/'
|
378
389
|
if not include_subpath:
|
379
|
-
cursor = await self.cur.execute(
|
390
|
+
cursor = await self.cur.execute(
|
391
|
+
"SELECT SUM(file_size) FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND url NOT LIKE ? ESCAPE '\\'",
|
392
|
+
(self.escape_sqlike(url) + '%', self.escape_sqlike(url) + '%/%')
|
393
|
+
)
|
380
394
|
res = await cursor.fetchone()
|
381
395
|
else:
|
382
|
-
cursor = await self.cur.execute(
|
396
|
+
cursor = await self.cur.execute(
|
397
|
+
"SELECT SUM(file_size) FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
|
398
|
+
(self.escape_sqlike(url) + '%', )
|
399
|
+
)
|
383
400
|
res = await cursor.fetchone()
|
384
401
|
assert res is not None
|
385
402
|
return res[0] or 0
|
@@ -442,7 +459,10 @@ class FileConn(DBObjectBase):
|
|
442
459
|
"""
|
443
460
|
assert old_url.endswith('/'), "Old path must end with /"
|
444
461
|
assert new_url.endswith('/'), "New path must end with /"
|
445
|
-
cursor = await self.cur.execute(
|
462
|
+
cursor = await self.cur.execute(
|
463
|
+
"SELECT * FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
|
464
|
+
(self.escape_sqlike(old_url) + '%', )
|
465
|
+
)
|
446
466
|
res = await cursor.fetchall()
|
447
467
|
for r in res:
|
448
468
|
old_record = FileRecord(*r)
|
@@ -472,10 +492,16 @@ class FileConn(DBObjectBase):
|
|
472
492
|
assert old_url.endswith('/'), "Old path must end with /"
|
473
493
|
assert new_url.endswith('/'), "New path must end with /"
|
474
494
|
if user_id is None:
|
475
|
-
cursor = await self.cur.execute(
|
495
|
+
cursor = await self.cur.execute(
|
496
|
+
"SELECT * FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
|
497
|
+
(self.escape_sqlike(old_url) + '%', )
|
498
|
+
)
|
476
499
|
res = await cursor.fetchall()
|
477
500
|
else:
|
478
|
-
cursor = await self.cur.execute(
|
501
|
+
cursor = await self.cur.execute(
|
502
|
+
"SELECT * FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND owner_id = ?",
|
503
|
+
(self.escape_sqlike(old_url) + '%', user_id)
|
504
|
+
)
|
479
505
|
res = await cursor.fetchall()
|
480
506
|
for r in res:
|
481
507
|
new_r = new_url + r[0][len(old_url):]
|
@@ -510,10 +536,16 @@ class FileConn(DBObjectBase):
|
|
510
536
|
async def delete_records_by_prefix(self, path: str, under_owner_id: Optional[int] = None) -> list[FileRecord]:
|
511
537
|
"""Delete all records with url starting with path"""
|
512
538
|
# update user size
|
513
|
-
cursor = await self.cur.execute(
|
539
|
+
cursor = await self.cur.execute(
|
540
|
+
"SELECT DISTINCT owner_id FROM fmeta WHERE url LIKE ? ESCAPE '\\'",
|
541
|
+
(self.escape_sqlike(path) + '%', )
|
542
|
+
)
|
514
543
|
res = await cursor.fetchall()
|
515
544
|
for r in res:
|
516
|
-
cursor = await self.cur.execute(
|
545
|
+
cursor = await self.cur.execute(
|
546
|
+
"SELECT SUM(file_size) FROM fmeta WHERE owner_id = ? AND url LIKE ? ESCAPE '\\'",
|
547
|
+
(r[0], self.escape_sqlike(path) + '%')
|
548
|
+
)
|
517
549
|
size = await cursor.fetchone()
|
518
550
|
if size is not None:
|
519
551
|
await self._user_size_dec(r[0], size[0])
|
@@ -522,9 +554,9 @@ class FileConn(DBObjectBase):
|
|
522
554
|
# but it's not a big deal... we should have only one writer
|
523
555
|
|
524
556
|
if under_owner_id is None:
|
525
|
-
res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? RETURNING *", (path + '%', ))
|
557
|
+
res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? ESCAPE '\\' RETURNING *", (self.escape_sqlike(path) + '%', ))
|
526
558
|
else:
|
527
|
-
res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? AND owner_id = ? RETURNING *", (path + '%', under_owner_id))
|
559
|
+
res = await self.cur.execute("DELETE FROM fmeta WHERE url LIKE ? ESCAPE '\\' AND owner_id = ? RETURNING *", (self.escape_sqlike(path) + '%', under_owner_id))
|
528
560
|
all_f_rec = await res.fetchall()
|
529
561
|
self.logger.info(f"Deleted {len(all_f_rec)} file(s) for path {path}") # type: ignore
|
530
562
|
return [self.parse_record(r) for r in all_f_rec]
|
@@ -692,7 +724,7 @@ async def delayed_log_access(url: str):
|
|
692
724
|
prohibited_part_regex = re.compile(
|
693
725
|
"|".join([
|
694
726
|
r"^\s*\.+\s*$", # dot path
|
695
|
-
"[{}]".format("".join(re.escape(c) for c in ('/', "\\", "'", '"', "*"
|
727
|
+
"[{}]".format("".join(re.escape(c) for c in ('/', "\\", "'", '"', "*"))), # prohibited characters
|
696
728
|
])
|
697
729
|
),
|
698
730
|
)
|
@@ -1076,7 +1108,7 @@ class Database:
|
|
1076
1108
|
async def data_iter():
|
1077
1109
|
async for (r, blob) in self.iter_dir(top_url, None):
|
1078
1110
|
rel_path = r.url[len(top_url):]
|
1079
|
-
rel_path =
|
1111
|
+
rel_path = decode_uri_components(rel_path)
|
1080
1112
|
b_iter: AsyncIterable[bytes]
|
1081
1113
|
if isinstance(blob, bytes):
|
1082
1114
|
async def blob_iter(): yield blob
|
@@ -1106,7 +1138,7 @@ class Database:
|
|
1106
1138
|
with zipfile.ZipFile(buffer, 'w') as zf:
|
1107
1139
|
async for (r, blob) in self.iter_dir(top_url, None):
|
1108
1140
|
rel_path = r.url[len(top_url):]
|
1109
|
-
rel_path =
|
1141
|
+
rel_path = decode_uri_components(rel_path)
|
1110
1142
|
if r.external:
|
1111
1143
|
assert isinstance(blob, AsyncIterable)
|
1112
1144
|
zf.writestr(rel_path, b''.join([chunk async for chunk in blob]))
|
lfss/eng/utils.py
CHANGED
@@ -21,19 +21,19 @@ async def copy_file(source: str|pathlib.Path, destination: str|pathlib.Path):
|
|
21
21
|
def hash_credential(username: str, password: str):
|
22
22
|
return hashlib.sha256(f"{username}:{password}".encode()).hexdigest()
|
23
23
|
|
24
|
-
def
|
24
|
+
def encode_uri_components(path: str):
|
25
25
|
path_sp = path.split("/")
|
26
26
|
mapped = map(lambda x: urllib.parse.quote(x), path_sp)
|
27
27
|
return "/".join(mapped)
|
28
28
|
|
29
|
-
def
|
29
|
+
def decode_uri_components(path: str):
|
30
30
|
path_sp = path.split("/")
|
31
31
|
mapped = map(lambda x: urllib.parse.unquote(x), path_sp)
|
32
32
|
return "/".join(mapped)
|
33
33
|
|
34
|
-
def
|
34
|
+
def ensure_uri_components(path: str):
|
35
35
|
""" Ensure the path components are safe to use """
|
36
|
-
return
|
36
|
+
return encode_uri_components(decode_uri_components(path))
|
37
37
|
|
38
38
|
class TaskManager:
|
39
39
|
def __init__(self):
|
lfss/svc/app_dav.py
CHANGED
@@ -11,7 +11,7 @@ from ..eng.error import *
|
|
11
11
|
from ..eng.config import DATA_HOME, DEBUG_MODE
|
12
12
|
from ..eng.datatype import UserRecord, FileRecord, DirectoryRecord, AccessLevel
|
13
13
|
from ..eng.database import FileConn, UserConn, check_path_permission
|
14
|
-
from ..eng.utils import
|
14
|
+
from ..eng.utils import ensure_uri_components, decode_uri_components, format_last_modified, static_vars
|
15
15
|
from .app_base import *
|
16
16
|
from .common_impl import copy_impl
|
17
17
|
|
@@ -36,7 +36,7 @@ async def eval_path(path: str) -> tuple[ptype, str, Optional[FileRecord | Direct
|
|
36
36
|
and should end with / if it is a directory, otherwise it is a file
|
37
37
|
record is the FileRecord or DirectoryRecord object, it is None if the path does not exist
|
38
38
|
"""
|
39
|
-
path =
|
39
|
+
path = decode_uri_components(path)
|
40
40
|
if "://" in path:
|
41
41
|
if not path.startswith("http://") and not path.startswith("https://"):
|
42
42
|
raise HTTPException(status_code=400, detail="Bad Request, unsupported protocol")
|
@@ -47,7 +47,7 @@ async def eval_path(path: str) -> tuple[ptype, str, Optional[FileRecord | Direct
|
|
47
47
|
assert path.startswith(route_prefix), "Path should start with the route prefix, got: " + path
|
48
48
|
path = path[len(route_prefix):]
|
49
49
|
|
50
|
-
path =
|
50
|
+
path = ensure_uri_components(path)
|
51
51
|
if path.startswith("/"): path = path[1:]
|
52
52
|
|
53
53
|
# path now is url-safe and without leading slash
|
@@ -160,7 +160,7 @@ async def create_file_xml_element(frecord: FileRecord) -> ET.Element:
|
|
160
160
|
href.text = f"/{frecord.url}"
|
161
161
|
propstat = ET.SubElement(file_el, f"{{{DAV_NS}}}propstat")
|
162
162
|
prop = ET.SubElement(propstat, f"{{{DAV_NS}}}prop")
|
163
|
-
ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text =
|
163
|
+
ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = decode_uri_components(frecord.url.split("/")[-1])
|
164
164
|
ET.SubElement(prop, f"{{{DAV_NS}}}resourcetype")
|
165
165
|
ET.SubElement(prop, f"{{{DAV_NS}}}getcontentlength").text = str(frecord.file_size)
|
166
166
|
ET.SubElement(prop, f"{{{DAV_NS}}}getlastmodified").text = format_last_modified(frecord.create_time)
|
@@ -178,7 +178,7 @@ async def create_dir_xml_element(drecord: DirectoryRecord) -> ET.Element:
|
|
178
178
|
href.text = f"/{drecord.url}"
|
179
179
|
propstat = ET.SubElement(dir_el, f"{{{DAV_NS}}}propstat")
|
180
180
|
prop = ET.SubElement(propstat, f"{{{DAV_NS}}}prop")
|
181
|
-
ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text =
|
181
|
+
ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = decode_uri_components(drecord.url.split("/")[-2])
|
182
182
|
ET.SubElement(prop, f"{{{DAV_NS}}}resourcetype").append(ET.Element(f"{{{DAV_NS}}}collection"))
|
183
183
|
if drecord.size >= 0:
|
184
184
|
ET.SubElement(prop, f"{{{DAV_NS}}}getlastmodified").text = format_last_modified(drecord.create_time)
|
@@ -211,7 +211,7 @@ async def dav_options(request: Request, path: str):
|
|
211
211
|
@handle_exception
|
212
212
|
async def dav_propfind(request: Request, path: str, user: UserRecord = Depends(registered_user), body: Optional[ET.Element] = Depends(xml_request_body)):
|
213
213
|
if path.startswith("/"): path = path[1:]
|
214
|
-
path =
|
214
|
+
path = ensure_uri_components(path)
|
215
215
|
|
216
216
|
if body and DEBUG_MODE:
|
217
217
|
print("Propfind-body:", ET.tostring(body, encoding="utf-8", method="xml"))
|
lfss/svc/app_native.py
CHANGED
@@ -5,7 +5,7 @@ from fastapi import Depends, Request, Response, UploadFile, Query
|
|
5
5
|
from fastapi.responses import StreamingResponse, JSONResponse
|
6
6
|
from fastapi.exceptions import HTTPException
|
7
7
|
|
8
|
-
from ..eng.utils import
|
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
11
|
from ..eng.database import check_file_read_permission, check_path_permission, FileConn, delayed_log_access
|
@@ -83,7 +83,7 @@ async def delete_file(path: str, user: UserRecord = Depends(registered_user)):
|
|
83
83
|
@handle_exception
|
84
84
|
async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
|
85
85
|
logger.info(f"GET bundle({path}), user: {user.username}")
|
86
|
-
path =
|
86
|
+
path = ensure_uri_components(path)
|
87
87
|
if not path.endswith("/"):
|
88
88
|
raise HTTPException(status_code=400, detail="Path must end with /")
|
89
89
|
if path[0] == "/": # adapt to both /path and path
|
@@ -123,7 +123,7 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
|
|
123
123
|
@handle_exception
|
124
124
|
async def get_file_meta(path: str, user: UserRecord = Depends(registered_user)):
|
125
125
|
logger.info(f"GET meta({path}), user: {user.username}")
|
126
|
-
path =
|
126
|
+
path = ensure_uri_components(path)
|
127
127
|
is_file = not path.endswith("/")
|
128
128
|
async with unique_cursor() as cur:
|
129
129
|
fconn = FileConn(cur)
|
@@ -147,7 +147,7 @@ async def update_file_meta(
|
|
147
147
|
new_path: Optional[str] = None,
|
148
148
|
user: UserRecord = Depends(registered_user)
|
149
149
|
):
|
150
|
-
path =
|
150
|
+
path = ensure_uri_components(path)
|
151
151
|
if path.startswith("/"):
|
152
152
|
path = path[1:]
|
153
153
|
|
@@ -162,7 +162,7 @@ async def update_file_meta(
|
|
162
162
|
)
|
163
163
|
|
164
164
|
if new_path is not None:
|
165
|
-
new_path =
|
165
|
+
new_path = ensure_uri_components(new_path)
|
166
166
|
logger.info(f"Update path of {path} to {new_path}")
|
167
167
|
await db.move_file(path, new_path, user)
|
168
168
|
|
@@ -170,7 +170,7 @@ async def update_file_meta(
|
|
170
170
|
else:
|
171
171
|
assert perm is None, "Permission is not supported for directory"
|
172
172
|
if new_path is not None:
|
173
|
-
new_path =
|
173
|
+
new_path = ensure_uri_components(new_path)
|
174
174
|
logger.info(f"Update path of {path} to {new_path}")
|
175
175
|
# will raise duplicate path error if same name path exists in the new path
|
176
176
|
await db.move_dir(path, new_path, user)
|
@@ -194,7 +194,7 @@ async def validate_path_read_permission(path: str, user: UserRecord):
|
|
194
194
|
@handle_exception
|
195
195
|
async def count_files(path: str, flat: bool = False, user: UserRecord = Depends(registered_user)):
|
196
196
|
await validate_path_read_permission(path, user)
|
197
|
-
path =
|
197
|
+
path = ensure_uri_components(path)
|
198
198
|
async with unique_cursor() as conn:
|
199
199
|
fconn = FileConn(conn)
|
200
200
|
return { "count": await fconn.count_dir_files(url = path, flat = flat) }
|
@@ -206,7 +206,7 @@ async def list_files(
|
|
206
206
|
flat: bool = False, user: UserRecord = Depends(registered_user)
|
207
207
|
):
|
208
208
|
await validate_path_read_permission(path, user)
|
209
|
-
path =
|
209
|
+
path = ensure_uri_components(path)
|
210
210
|
async with unique_cursor() as conn:
|
211
211
|
fconn = FileConn(conn)
|
212
212
|
return await fconn.list_dir_files(
|
@@ -219,7 +219,7 @@ async def list_files(
|
|
219
219
|
@handle_exception
|
220
220
|
async def count_dirs(path: str, user: UserRecord = Depends(registered_user)):
|
221
221
|
await validate_path_read_permission(path, user)
|
222
|
-
path =
|
222
|
+
path = ensure_uri_components(path)
|
223
223
|
async with unique_cursor() as conn:
|
224
224
|
fconn = FileConn(conn)
|
225
225
|
return { "count": await fconn.count_path_dirs(url = path) }
|
@@ -231,7 +231,7 @@ async def list_dirs(
|
|
231
231
|
skim: bool = True, user: UserRecord = Depends(registered_user)
|
232
232
|
):
|
233
233
|
await validate_path_read_permission(path, user)
|
234
|
-
path =
|
234
|
+
path = ensure_uri_components(path)
|
235
235
|
async with unique_cursor() as conn:
|
236
236
|
fconn = FileConn(conn)
|
237
237
|
return await fconn.list_path_dirs(
|
@@ -263,7 +263,7 @@ async def get_multiple_files(
|
|
263
263
|
upath2path = OrderedDict[str, str]()
|
264
264
|
for p in path:
|
265
265
|
p_ = p if not p.startswith("/") else p[1:]
|
266
|
-
upath2path[
|
266
|
+
upath2path[ensure_uri_components(p_)] = p
|
267
267
|
upaths = list(upath2path.keys())
|
268
268
|
|
269
269
|
# get files
|
lfss/svc/common_impl.py
CHANGED
@@ -6,7 +6,7 @@ from ..eng.connection_pool import unique_cursor
|
|
6
6
|
from ..eng.datatype import UserRecord, FileRecord, PathContents, AccessLevel, FileReadPermission
|
7
7
|
from ..eng.database import FileConn, UserConn, delayed_log_access, check_file_read_permission, check_path_permission
|
8
8
|
from ..eng.thumb import get_thumb
|
9
|
-
from ..eng.utils import format_last_modified,
|
9
|
+
from ..eng.utils import format_last_modified, ensure_uri_components
|
10
10
|
from ..eng.config import CHUNK_SIZE, DEBUG_MODE
|
11
11
|
|
12
12
|
from .app_base import skip_request_log, db, logger
|
@@ -100,7 +100,7 @@ async def get_impl(
|
|
100
100
|
thumb: bool = False,
|
101
101
|
is_head = False,
|
102
102
|
):
|
103
|
-
path =
|
103
|
+
path = ensure_uri_components(path)
|
104
104
|
if path.startswith("/"): path = path[1:]
|
105
105
|
|
106
106
|
# handle directory query
|
@@ -194,7 +194,7 @@ async def put_file_impl(
|
|
194
194
|
conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
|
195
195
|
permission: int = 0,
|
196
196
|
):
|
197
|
-
path =
|
197
|
+
path = ensure_uri_components(path)
|
198
198
|
assert not path.endswith("/"), "Path must not end with /"
|
199
199
|
|
200
200
|
access_level = await check_path_permission(path, user)
|
@@ -249,7 +249,7 @@ async def post_file_impl(
|
|
249
249
|
conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
|
250
250
|
permission: int = 0,
|
251
251
|
):
|
252
|
-
path =
|
252
|
+
path = ensure_uri_components(path)
|
253
253
|
assert not path.endswith("/"), "Path must not end with /"
|
254
254
|
|
255
255
|
access_level = await check_path_permission(path, user)
|
@@ -288,7 +288,7 @@ async def post_file_impl(
|
|
288
288
|
}, content=json.dumps({"url": path}))
|
289
289
|
|
290
290
|
async def delete_impl(path: str, user: UserRecord):
|
291
|
-
path =
|
291
|
+
path = ensure_uri_components(path)
|
292
292
|
if await check_path_permission(path, user) < AccessLevel.WRITE:
|
293
293
|
raise HTTPException(status_code=403, detail="Permission denied")
|
294
294
|
|
@@ -307,8 +307,8 @@ async def delete_impl(path: str, user: UserRecord):
|
|
307
307
|
async def copy_impl(
|
308
308
|
op_user: UserRecord, src_path: str, dst_path: str,
|
309
309
|
):
|
310
|
-
src_path =
|
311
|
-
dst_path =
|
310
|
+
src_path = ensure_uri_components(src_path)
|
311
|
+
dst_path = ensure_uri_components(dst_path)
|
312
312
|
copy_type = "file" if not src_path[-1] == "/" else "directory"
|
313
313
|
if (src_path[-1] == "/") != (dst_path[-1] == "/"):
|
314
314
|
raise HTTPException(status_code=400, detail="Source and destination must be same type")
|
@@ -1,9 +1,10 @@
|
|
1
1
|
Readme.md,sha256=B-foESzFWoSI5MEd89AWUzKcVRrTwipM28TK8GN0o8c,1920
|
2
|
+
docs/Client.md,sha256=2GSKrcKkjYHxxA6Afa2vhHj7UFEQ-_EdmNRkZ06jPHU,975
|
2
3
|
docs/Enviroment_variables.md,sha256=CZ5DrrXSLU5RLBEVQ-gLMaOIuFthd7dEiTzO7ODrPRQ,788
|
3
4
|
docs/Known_issues.md,sha256=ZqETcWP8lzTOel9b2mxEgCnADFF8IxOrEtiVO1NoMAk,251
|
4
5
|
docs/Permission.md,sha256=thUJx7YRoU63Pb-eqo5l5450DrZN3QYZ36GCn8r66no,3152
|
5
6
|
docs/Webdav.md,sha256=-Ja-BTWSY1BEMAyZycvEMNnkNTPZ49gSPzmf3Lbib70,1547
|
6
|
-
docs/changelog.md,sha256=
|
7
|
+
docs/changelog.md,sha256=7Pa-yRqF2E0uU69e32bquAGmumgV3I-6irBiW-Jt5Nw,2120
|
7
8
|
frontend/api.js,sha256=F35jQjWF2LITkuO-wZJuEKyafLWFx_M4C2tEYJV8zak,22631
|
8
9
|
frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
|
9
10
|
frontend/info.css,sha256=Ny0N3GywQ3a9q1_Qph_QFEKB4fEnTe_2DJ1Y5OsLLmQ,595
|
@@ -18,11 +19,11 @@ frontend/styles.css,sha256=xcNLqI3KBsY5TLnku8UIP0Jfr7QLajr1_KNlZj9eheM,4935
|
|
18
19
|
frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
|
19
20
|
frontend/thumb.js,sha256=46ViD2TlTTWy0fx6wjoAs_5CQ4ajYB90vVzM7UO2IHw,6182
|
20
21
|
frontend/utils.js,sha256=XP5hM_mROYaxK5dqn9qZVwv7GdQuiDzByilFskbrnxA,6068
|
21
|
-
lfss/api/__init__.py,sha256=
|
22
|
-
lfss/api/connector.py,sha256=
|
22
|
+
lfss/api/__init__.py,sha256=MdRsQSldbV4tZpAdzgr0sws8ru9GPvqUWVcsT6iVRFY,6844
|
23
|
+
lfss/api/connector.py,sha256=sz2mvGsHOREhev0FCd9p9RdyrJxRwtt-X63Kt4_E_dQ,13933
|
23
24
|
lfss/cli/__init__.py,sha256=lPwPmqpa7EXQ4zlU7E7LOe6X2kw_xATGdwoHphUEirA,827
|
24
25
|
lfss/cli/balance.py,sha256=fUbKKAUyaDn74f7mmxMfBL4Q4voyBLHu6Lg_g8GfMOQ,4121
|
25
|
-
lfss/cli/cli.py,sha256=
|
26
|
+
lfss/cli/cli.py,sha256=l4_hU7DLJ_QqTG69oKZSVXSA46PjfpxiUc_8ruTY6n0,8196
|
26
27
|
lfss/cli/log.py,sha256=TBlt8mhHMouv8ZBUMHYfGZiV6-0yPdajJQ5mkGHEojI,3016
|
27
28
|
lfss/cli/panel.py,sha256=Xq3I_n-ctveym-Gh9LaUpzHiLlvt3a_nuDiwUS-MGrg,1597
|
28
29
|
lfss/cli/serve.py,sha256=vTo6_BiD7Dn3VLvHsC5RKRBC3lMu45JVr_0SqpgHdj0,1086
|
@@ -32,21 +33,21 @@ lfss/eng/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
33
|
lfss/eng/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
|
33
34
|
lfss/eng/config.py,sha256=0dncHYn3tYw4pKBwXuP_huz0u7ud23fJ6SUmUPfLmeM,967
|
34
35
|
lfss/eng/connection_pool.py,sha256=1aq7nSgd7hB9YNV4PjD1RDRyl_moDw3ubBtSLyfgGBs,6320
|
35
|
-
lfss/eng/database.py,sha256=
|
36
|
+
lfss/eng/database.py,sha256=obJU3rD_AvwVzIUiSB1gqsC2k7_E6p7dw8PW7eYvi84,57241
|
36
37
|
lfss/eng/datatype.py,sha256=27UB7-l9SICy5lAvKjdzpTL_GohZjzstQcr9PtAq7nM,2709
|
37
38
|
lfss/eng/error.py,sha256=JGf5NV-f4rL6tNIDSAx5-l9MG8dEj7F2w_MuOjj1d1o,732
|
38
39
|
lfss/eng/log.py,sha256=yciFQ7Utz1AItNekS4YtdP6bM7i1krA6qSAU2wVQv24,7698
|
39
40
|
lfss/eng/thumb.py,sha256=AFyWEkkpuCKGWOB9bLlaDwPKzQ9JtCSSmHMhX2Gu3CI,3096
|
40
|
-
lfss/eng/utils.py,sha256=
|
41
|
+
lfss/eng/utils.py,sha256=SlgiC5S0V42n9JczFF04jl5tKC7R6gN-FkuqqNoF7co,6688
|
41
42
|
lfss/sql/init.sql,sha256=FBmVzkNjYUnWjEELRFzf7xb50GngmzmeDVffT1Uk8u8,1625
|
42
43
|
lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
|
43
44
|
lfss/svc/app.py,sha256=r1KUO3sPaaJWbkJF0bcVTD7arPKLs2jFlq52Ixicomo,220
|
44
45
|
lfss/svc/app_base.py,sha256=s5ieQVI4BT0CBYavRx0dyBqwts7PYnjyCovHNYPHul8,7014
|
45
|
-
lfss/svc/app_dav.py,sha256=
|
46
|
-
lfss/svc/app_native.py,sha256=
|
47
|
-
lfss/svc/common_impl.py,sha256=
|
46
|
+
lfss/svc/app_dav.py,sha256=DRMgByUAQ3gD6wL9xmikV5kvVmATN7QkxGSttFTYxFU,18245
|
47
|
+
lfss/svc/app_native.py,sha256=K2vlNs6b4DE2bPgRuHxic9oDp8V3D5M5wNjIibGjQyg,10612
|
48
|
+
lfss/svc/common_impl.py,sha256=wlTQm8zEGAfyw9FJvK9zqgLQw47MzNq6IT3OgwdUaCw,13736
|
48
49
|
lfss/svc/request_log.py,sha256=v8yXEIzPjaksu76Oh5vgdbUEUrw8Kt4etLAXBWSGie8,3207
|
49
|
-
lfss-0.11.
|
50
|
-
lfss-0.11.
|
51
|
-
lfss-0.11.
|
52
|
-
lfss-0.11.
|
50
|
+
lfss-0.11.5.dist-info/METADATA,sha256=h1KBhb0MjCtgrlaUqfDSqLH_6Jxy1q4KaETLEMauIyY,2732
|
51
|
+
lfss-0.11.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
52
|
+
lfss-0.11.5.dist-info/entry_points.txt,sha256=M4ubn9oLYcTc9wxlLKWwljnluStPWpCDlCGuTVU8twg,255
|
53
|
+
lfss-0.11.5.dist-info/RECORD,,
|
File without changes
|