lfss 0.9.1__py3-none-any.whl → 0.9.4__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.
Readme.md CHANGED
@@ -34,10 +34,12 @@ The API usage is simple, just `GET`, `PUT`, `DELETE` to the `/<username>/file/ur
34
34
  The authentication can be acheived through one of the following methods:
35
35
  1. `Authorization` header with the value `Bearer sha256(<username><password>)`.
36
36
  2. `token` query parameter with the value `sha256(<username><password>)`.
37
- 3. HTTP Basic Authentication with the username and password.
37
+ 3. HTTP Basic Authentication with the username and password (If WebDAV is enabled).
38
38
 
39
39
  You can refer to `frontend` as an application example, `lfss/api/connector.py` for more APIs.
40
40
 
41
41
  By default, the service exposes all files to the public for `GET` requests,
42
42
  but file-listing is restricted to the user's own files.
43
- Please refer to [docs/Permission.md](./docs/Permission.md) for more details on the permission system.
43
+ Please refer to [docs/Permission.md](./docs/Permission.md) for more details on the permission system.
44
+
45
+ More can be found in the [docs](./docs) directory.
docs/Changelog.md ADDED
@@ -0,0 +1,27 @@
1
+
2
+ ## 0.9
3
+
4
+ ### 0.9.4
5
+ - Decode WebDAV file name.
6
+ - Allow root-listing for WebDAV.
7
+ - Always return 207 status code for propfind.
8
+ - Refactor debounce utility.
9
+
10
+ ### 0.9.3
11
+ - Fix empty file getting.
12
+ - HTTP `PUT/POST` default to overwrite the file.
13
+ - Use shared implementations for `PUT`, `GET`, `DELETE` methods.
14
+ - Inherit permission on overwriting `unset` permission files.
15
+
16
+ ### 0.9.2
17
+ - Native copy function.
18
+ - Only enable basic authentication if WebDAV is enabled.
19
+ - `WWW-Authenticate` header is now added to the response when authentication fails.
20
+
21
+ ### 0.9.1
22
+ - Add WebDAV support.
23
+ - Code refactor, use `lfss.eng` and `lfss.svc`.
24
+
25
+ ### 0.9.0
26
+ - User peer access control, now user can share their path with other users.
27
+ - Fix high concurrency database locking on file getting.
@@ -0,0 +1,12 @@
1
+
2
+ # Enviroment variables
3
+
4
+ **Server**
5
+ - `LFSS_DATA`: The directory to store the data. Default is `.storage_data`.
6
+ - `LFSS_WEBDAV`: Enable WebDAV support. Default is `0`, set to `1` to enable.
7
+ - `LFSS_LARGE_FILE`: The size limit of the file to store in the database. Default is `8m`.
8
+ - `LFSS_DEBUG`: Enable debug mode for more verbose logging. Default is `0`, set to `1` to enable.
9
+
10
+ **Client**
11
+ - `LFSS_ENDPOINT`: The fallback server endpoint. Default is `http://localhost:8000`.
12
+ - `LFSS_TOKEN`: The fallback token to authenticate. Should be `sha256(<username><password>)`.
docs/Known_issues.md CHANGED
@@ -1 +1,3 @@
1
- [Safari 中文输入法回车捕获](https://github.com/anse-app/anse/issues/127)
1
+ [Safari 中文输入法回车捕获](https://github.com/anse-app/anse/issues/127)
2
+
3
+ [Word 临时文件](https://answers.microsoft.com/en-us/msoffice/forum/all/mac-os-word-temp-sb-folders-created-on-smb-share/40fda56c-c77c-4365-8fa3-eb87ac814207?page=1)
docs/Webdav.md CHANGED
@@ -15,8 +15,8 @@ Please note:
15
15
  2. LFSS not allow creating files in the root directory, however some client such as [Finder](https://sabre.io/dav/clients/finder/) will try to create files in the root directory. Thus, it is safer to mount the user directory only, e.g. `http://localhost:8000/<username>/`.
16
16
  3. LFSS not allow directory creation, instead it creates directoy implicitly when a file is uploaded to a non-exist directory.
17
17
  i.e. `PUT http://localhost:8000/<username>/dir/file.txt` will create the `dir` directory if it does not exist.
18
- However, the WebDAV `MKCOL` method requires the directory to be created explicitly, so WebDAV `MKCOL` method instead create a decoy file on the path (`.lfss-keep`), and hide the file from the file listing by `PROPFIND` method.
18
+ However, the WebDAV `MKCOL` method requires the directory to be created explicitly, so WebDAV `MKCOL` method instead create a decoy file on the path (`.lfss_keep`), and hide the file from the file listing by `PROPFIND` method.
19
19
  This leads to:
20
- 1) You may see a `.lfss-keep` file in the directory with native file listing (e.g. `/_api/list-files`), but it is hidden in WebDAV clients.
21
- 2) The directory may be deleted if there is no file in it and the `.lfss-keep` file is not created by WebDAV client.
20
+ 1) You may see a `.lfss_keep` file in the directory with native file listing (e.g. `/_api/list-files`), but it is hidden in WebDAV clients.
21
+ 2) The directory may be deleted if there is no file in it and the `.lfss_keep` file is not created by WebDAV client.
22
22
 
frontend/api.js CHANGED
@@ -384,6 +384,27 @@ export default class Connector {
384
384
  }
385
385
  }
386
386
 
387
+ /**
388
+ * @param {string} srcPath - file path(url)
389
+ * @param {string} dstPath - new file path(url)
390
+ */
391
+ async copy(srcPath, dstPath){
392
+ if (srcPath.startsWith('/')){ srcPath = srcPath.slice(1); }
393
+ if (dstPath.startsWith('/')){ dstPath = dstPath.slice(1); }
394
+ const dst = new URL(this.config.endpoint + '/_api/copy');
395
+ dst.searchParams.append('src', srcPath);
396
+ dst.searchParams.append('dst', dstPath);
397
+ const res = await fetch(dst.toString(), {
398
+ method: 'POST',
399
+ headers: {
400
+ 'Authorization': 'Bearer ' + this.config.token,
401
+ 'Content-Type': 'application/www-form-urlencoded'
402
+ },
403
+ });
404
+ if (!(res.status == 200 || res.status == 201)){
405
+ throw new Error(`Failed to copy file, status code: ${res.status}, message: ${await fmtFailedResponse(res)}`);
406
+ }
407
+ }
387
408
  }
388
409
 
389
410
  /**
frontend/scripts.js CHANGED
@@ -347,7 +347,7 @@ async function refreshFileList(){
347
347
  }
348
348
  );
349
349
  }, {
350
- text: 'Enter the destination path: ',
350
+ text: 'Enter the destination path (Move): ',
351
351
  placeholder: 'Destination path',
352
352
  value: decodePathURI(dirurl),
353
353
  select: "last-pathname"
@@ -355,6 +355,30 @@ async function refreshFileList(){
355
355
  });
356
356
  actContainer.appendChild(moveButton);
357
357
 
358
+ const copyButton = document.createElement('a');
359
+ copyButton.textContent = 'Copy';
360
+ copyButton.style.cursor = 'pointer';
361
+ copyButton.addEventListener('click', () => {
362
+ showFloatingWindowLineInput((dstPath) => {
363
+ dstPath = encodePathURI(dstPath);
364
+ console.log("Copying", dirurl, "to", dstPath);
365
+ conn.copy(dirurl, dstPath)
366
+ .then(() => {
367
+ refreshFileList();
368
+ },
369
+ (err) => {
370
+ showPopup('Failed to copy path: ' + err, {level: 'error'});
371
+ }
372
+ );
373
+ }, {
374
+ text: 'Enter the destination path (Copy): ',
375
+ placeholder: 'Destination path',
376
+ value: decodePathURI(dirurl),
377
+ select: "last-pathname"
378
+ });
379
+ });
380
+ actContainer.appendChild(copyButton);
381
+
358
382
  const downloadButton = document.createElement('a');
359
383
  downloadButton.textContent = 'Download';
360
384
  downloadButton.href = conn.config.endpoint + '/_api/bundle?' +
@@ -478,7 +502,7 @@ async function refreshFileList(){
478
502
  }
479
503
  );
480
504
  }, {
481
- text: 'Enter the destination path: ',
505
+ text: 'Enter the destination path (Move): ',
482
506
  placeholder: 'Destination path',
483
507
  value: decodePathURI(file.url),
484
508
  select: "last-filename"
@@ -486,6 +510,29 @@ async function refreshFileList(){
486
510
  });
487
511
  actContainer.appendChild(moveButton);
488
512
 
513
+ const copyButton = document.createElement('a');
514
+ copyButton.textContent = 'Copy';
515
+ copyButton.style.cursor = 'pointer';
516
+ copyButton.addEventListener('click', () => {
517
+ showFloatingWindowLineInput((dstPath) => {
518
+ dstPath = encodePathURI(dstPath);
519
+ conn.copy(file.url, dstPath)
520
+ .then(() => {
521
+ refreshFileList();
522
+ },
523
+ (err) => {
524
+ showPopup('Failed to copy file: ' + err, {level: 'error'});
525
+ }
526
+ );
527
+ }, {
528
+ text: 'Enter the destination path (Copy): ',
529
+ placeholder: 'Destination path',
530
+ value: decodePathURI(file.url),
531
+ select: "last-filename"
532
+ });
533
+ });
534
+ actContainer.appendChild(copyButton);
535
+
489
536
  const downloadBtn = document.createElement('a');
490
537
  downloadBtn.textContent = 'Download';
491
538
  downloadBtn.href = conn.config.endpoint + '/' + file.url + '?download=true&token=' + conn.config.token;
lfss/api/connector.py CHANGED
@@ -270,6 +270,12 @@ class Connector:
270
270
  self._fetch_factory('POST', '_api/meta', {'path': path, 'new_path': new_path})(
271
271
  headers = {'Content-Type': 'application/www-form-urlencoded'}
272
272
  )
273
+
274
+ def copy(self, src: str, dst: str):
275
+ """Copy file from src to dst."""
276
+ self._fetch_factory('POST', '_api/copy', {'src': src, 'dst': dst})(
277
+ headers = {'Content-Type': 'application/www-form-urlencoded'}
278
+ )
273
279
 
274
280
  def whoami(self) -> UserRecord:
275
281
  """Gets information about the current user."""
@@ -29,7 +29,7 @@ async def get_connection(read_only: bool = False) -> aiosqlite.Connection:
29
29
 
30
30
  conn = await aiosqlite.connect(
31
31
  get_db_uri(DATA_HOME / 'index.db', read_only=read_only),
32
- timeout = 20, uri = True
32
+ timeout = 10, uri = True
33
33
  )
34
34
  async with conn.cursor() as c:
35
35
  await c.execute(
lfss/eng/database.py CHANGED
@@ -161,7 +161,7 @@ class UserConn(DBObjectBase):
161
161
  async def list_peer_users(self, src_user: int | str, level: AccessLevel) -> list[UserRecord]:
162
162
  """
163
163
  List all users that src_user can do [AliasLevel] to, with level >= level,
164
- Note: the returned list does not include src_user and admin users
164
+ Note: the returned list does not include src_user and is not apporiate for admin (who has all permissions for all users)
165
165
  """
166
166
  assert int(level) > AccessLevel.NONE, f"Invalid level, {level}"
167
167
  match src_user:
@@ -427,8 +427,7 @@ class FileConn(DBObjectBase):
427
427
  await self._user_size_inc(user_id, old.file_size)
428
428
  self.logger.info(f"Copied file {old_url} to {new_url}")
429
429
 
430
- # not tested
431
- async def copy_path(self, old_url: str, new_url: str, conflict_handler: Literal['skip', 'overwrite'] = 'overwrite', user_id: Optional[int] = None):
430
+ async def copy_path(self, old_url: str, new_url: str, user_id: Optional[int] = None):
432
431
  assert old_url.endswith('/'), "Old path must end with /"
433
432
  assert new_url.endswith('/'), "New path must end with /"
434
433
  if user_id is None:
@@ -440,11 +439,8 @@ class FileConn(DBObjectBase):
440
439
  for r in res:
441
440
  old_record = FileRecord(*r)
442
441
  new_r = new_url + old_record.url[len(old_url):]
443
- if conflict_handler == 'overwrite':
444
- await self.cur.execute("DELETE FROM fmeta WHERE url = ?", (new_r, ))
445
- elif conflict_handler == 'skip':
446
- if (await self.cur.execute("SELECT url FROM fmeta WHERE url = ?", (new_r, ))) is not None:
447
- continue
442
+ if await (await self.cur.execute("SELECT url FROM fmeta WHERE url = ?", (new_r, ))).fetchone() is not None:
443
+ raise FileExistsError(f"File {new_r} already exists")
448
444
  new_fid = str(uuid.uuid4())
449
445
  user_id = old_record.owner_id if user_id is None else user_id
450
446
  await self.cur.execute(
@@ -456,6 +452,7 @@ class FileConn(DBObjectBase):
456
452
  else:
457
453
  await copy_file(LARGE_BLOB_DIR / old_record.file_id, LARGE_BLOB_DIR / new_fid)
458
454
  await self._user_size_inc(user_id, old_record.file_size)
455
+ self.logger.info(f"Copied path {old_url} to {new_url}")
459
456
 
460
457
  async def move_file(self, old_url: str, new_url: str):
461
458
  old = await self.get_file_record(old_url)
@@ -467,7 +464,7 @@ class FileConn(DBObjectBase):
467
464
  await self.cur.execute("UPDATE fmeta SET url = ?, create_time = CURRENT_TIMESTAMP WHERE url = ?", (new_url, old_url))
468
465
  self.logger.info(f"Moved file {old_url} to {new_url}")
469
466
 
470
- async def move_path(self, old_url: str, new_url: str, conflict_handler: Literal['skip', 'overwrite'] = 'overwrite', user_id: Optional[int] = None):
467
+ async def move_path(self, old_url: str, new_url: str, user_id: Optional[int] = None):
471
468
  assert old_url.endswith('/'), "Old path must end with /"
472
469
  assert new_url.endswith('/'), "New path must end with /"
473
470
  if user_id is None:
@@ -478,11 +475,9 @@ class FileConn(DBObjectBase):
478
475
  res = await cursor.fetchall()
479
476
  for r in res:
480
477
  new_r = new_url + r[0][len(old_url):]
481
- if conflict_handler == 'overwrite':
482
- await self.cur.execute("DELETE FROM fmeta WHERE url = ?", (new_r, ))
483
- elif conflict_handler == 'skip':
484
- if (await self.cur.execute("SELECT url FROM fmeta WHERE url = ?", (new_r, ))) is not None:
485
- continue
478
+ if await (await self.cur.execute("SELECT url FROM fmeta WHERE url = ?", (new_r, ))).fetchone():
479
+ self.logger.error(f"File {new_r} already exists on move path: {old_url} -> {new_url}")
480
+ raise FileDuplicateError(f"File {new_r} already exists")
486
481
  await self.cur.execute("UPDATE fmeta SET url = ?, create_time = CURRENT_TIMESTAMP WHERE url = ?", (new_r, r[0]))
487
482
 
488
483
  async def log_access(self, url: str):
@@ -790,6 +785,8 @@ class Database:
790
785
  if op_user is not None:
791
786
  if await check_path_permission(old_url, op_user, cursor=cur) < AccessLevel.WRITE:
792
787
  raise PermissionDeniedError(f"Permission denied: {op_user.username} cannot move file {old_url}")
788
+ if await check_path_permission(new_url, op_user, cursor=cur) < AccessLevel.WRITE:
789
+ raise PermissionDeniedError(f"Permission denied: {op_user.username} cannot move file to {new_url}")
793
790
  await fconn.move_file(old_url, new_url)
794
791
 
795
792
  new_mime, _ = mimetypes.guess_type(new_url)
@@ -834,7 +831,7 @@ class Database:
834
831
 
835
832
  async with transaction() as cur:
836
833
  fconn = FileConn(cur)
837
- await fconn.move_path(old_url, new_url, 'overwrite', op_user.id)
834
+ await fconn.move_path(old_url, new_url, op_user.id)
838
835
 
839
836
  # not tested
840
837
  async def copy_path(self, old_url: str, new_url: str, op_user: UserRecord):
@@ -858,7 +855,7 @@ class Database:
858
855
 
859
856
  async with transaction() as cur:
860
857
  fconn = FileConn(cur)
861
- await fconn.copy_path(old_url, new_url, 'overwrite', op_user.id)
858
+ await fconn.copy_path(old_url, new_url, op_user.id)
862
859
 
863
860
  async def __batch_delete_file_blobs(self, fconn: FileConn, file_records: list[FileRecord], batch_size: int = 512):
864
861
  # https://github.com/langchain-ai/langchain/issues/10321
lfss/eng/error.py CHANGED
@@ -6,13 +6,17 @@ class FileLockedError(LFSSExceptionBase):...
6
6
 
7
7
  class InvalidOptionsError(LFSSExceptionBase, ValueError):...
8
8
 
9
+ class InvalidDataError(LFSSExceptionBase, ValueError):...
10
+
11
+ class InvalidPathError(LFSSExceptionBase, ValueError):...
12
+
9
13
  class DatabaseLockedError(LFSSExceptionBase, sqlite3.DatabaseError):...
10
14
 
11
15
  class PathNotFoundError(LFSSExceptionBase, FileNotFoundError):...
12
16
 
13
- class PermissionDeniedError(LFSSExceptionBase, PermissionError):...
17
+ class FileDuplicateError(LFSSExceptionBase, FileExistsError):...
14
18
 
15
- class InvalidPathError(LFSSExceptionBase, ValueError):...
19
+ class PermissionDeniedError(LFSSExceptionBase, PermissionError):...
16
20
 
17
21
  class StorageExceededError(LFSSExceptionBase):...
18
22
 
lfss/eng/thumb.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from lfss.eng.config import THUMB_DB, THUMB_SIZE
2
2
  from lfss.eng.database import FileConn
3
+ from lfss.eng.error import *
3
4
  from lfss.eng.connection_pool import unique_cursor
4
5
  from typing import Optional
5
6
  from PIL import Image
@@ -32,7 +33,10 @@ async def _get_cache_thumb(c: aiosqlite.Cursor, path: str, ctime: str) -> Option
32
33
  return blob
33
34
 
34
35
  async def _save_cache_thumb(c: aiosqlite.Cursor, path: str, ctime: str, raw_bytes: bytes) -> bytes:
35
- raw_img = Image.open(BytesIO(raw_bytes))
36
+ try:
37
+ raw_img = Image.open(BytesIO(raw_bytes))
38
+ except Exception:
39
+ raise InvalidDataError('Invalid image data for thumbnail: ' + path)
36
40
  raw_img.thumbnail(THUMB_SIZE)
37
41
  img = raw_img.convert('RGB')
38
42
  bio = BytesIO()
lfss/eng/utils.py CHANGED
@@ -36,17 +36,41 @@ def ensure_uri_compnents(path: str):
36
36
  """ Ensure the path components are safe to use """
37
37
  return encode_uri_compnents(decode_uri_compnents(path))
38
38
 
39
- g_debounce_tasks: OrderedDict[str, asyncio.Task] = OrderedDict()
40
- lock_debounce_task_queue = Lock()
39
+ class TaskManager:
40
+ def __init__(self):
41
+ self._tasks: OrderedDict[str, asyncio.Task] = OrderedDict()
42
+
43
+ def push(self, task: asyncio.Task) -> str:
44
+ tid = uuid4().hex
45
+ if tid in self._tasks:
46
+ raise ValueError("Task ID collision")
47
+ self._tasks[tid] = task
48
+ return tid
49
+
50
+ def cancel(self, task_id: str):
51
+ task = self._tasks.pop(task_id, None)
52
+ if task is not None:
53
+ task.cancel()
54
+
55
+ def truncate(self):
56
+ new_tasks = OrderedDict()
57
+ for tid, task in self._tasks.items():
58
+ if not task.done():
59
+ new_tasks[tid] = task
60
+ self._tasks = new_tasks
61
+
62
+ async def wait_all(self):
63
+ async def stop_task(task: asyncio.Task):
64
+ if not task.done():
65
+ await task
66
+ await asyncio.gather(*map(stop_task, self._tasks.values()))
67
+ self._tasks.clear()
68
+
69
+ def __len__(self): return len(self._tasks)
70
+
71
+ g_debounce_tasks: TaskManager = TaskManager()
41
72
  async def wait_for_debounce_tasks():
42
- async def stop_task(task: asyncio.Task):
43
- task.cancel()
44
- try:
45
- await task
46
- except asyncio.CancelledError:
47
- pass
48
- await asyncio.gather(*map(stop_task, g_debounce_tasks.values()))
49
- g_debounce_tasks.clear()
73
+ await g_debounce_tasks.wait_all()
50
74
 
51
75
  def debounce_async(delay: float = 0.1, max_wait: float = 1.):
52
76
  """
@@ -54,7 +78,8 @@ def debounce_async(delay: float = 0.1, max_wait: float = 1.):
54
78
  ensuring execution at least once every `max_wait` seconds.
55
79
  """
56
80
  def debounce_wrap(func):
57
- task_record: tuple[str, asyncio.Task] | None = None
81
+ # task_record: tuple[str, asyncio.Task] | None = None
82
+ prev_task_id = None
58
83
  fn_execution_lock = Lock()
59
84
  last_execution_time = 0
60
85
 
@@ -67,12 +92,11 @@ def debounce_async(delay: float = 0.1, max_wait: float = 1.):
67
92
 
68
93
  @functools.wraps(func)
69
94
  async def wrapper(*args, **kwargs):
70
- nonlocal task_record, last_execution_time
95
+ nonlocal prev_task_id, last_execution_time
71
96
 
72
- async with lock_debounce_task_queue:
73
- if task_record is not None:
74
- task_record[1].cancel()
75
- g_debounce_tasks.pop(task_record[0], None)
97
+ if prev_task_id is not None:
98
+ g_debounce_tasks.cancel(prev_task_id)
99
+ prev_task_id = None
76
100
 
77
101
  async with fn_execution_lock:
78
102
  if time.monotonic() - last_execution_time > max_wait:
@@ -81,14 +105,12 @@ def debounce_async(delay: float = 0.1, max_wait: float = 1.):
81
105
  return
82
106
 
83
107
  task = asyncio.create_task(delayed_func(*args, **kwargs))
84
- task_uid = uuid4().hex
85
- task_record = (task_uid, task)
86
- async with lock_debounce_task_queue:
87
- g_debounce_tasks[task_uid] = task
88
- if len(g_debounce_tasks) > 2048:
89
- # finished tasks are not removed from the dict
90
- # so we need to clear it periodically
91
- await wait_for_debounce_tasks()
108
+ prev_task_id = g_debounce_tasks.push(task)
109
+ if len(g_debounce_tasks) > 1024:
110
+ # finished tasks are not removed from the dict
111
+ # so we need to clear it periodically
112
+ g_debounce_tasks.truncate()
113
+
92
114
  return wrapper
93
115
  return debounce_wrap
94
116
 
lfss/svc/app.py CHANGED
@@ -1,9 +1,9 @@
1
+ from .app_base import ENABLE_WEBDAV
1
2
  from .app_native import *
2
- import os
3
3
 
4
4
  # order matters
5
5
  app.include_router(router_api)
6
- if os.environ.get("LFSS_WEBDAV", "0") == "1":
6
+ if ENABLE_WEBDAV:
7
7
  from .app_dav import *
8
8
  app.include_router(router_dav)
9
9
  app.include_router(router_fs)
lfss/svc/app_base.py CHANGED
@@ -1,4 +1,4 @@
1
- import asyncio, time
1
+ import asyncio, time, os
2
2
  from contextlib import asynccontextmanager
3
3
  from typing import Optional
4
4
  from functools import wraps
@@ -17,6 +17,7 @@ from ..eng.error import *
17
17
  from ..eng.config import DEBUG_MODE
18
18
  from .request_log import RequestDB
19
19
 
20
+ ENABLE_WEBDAV = os.environ.get("LFSS_WEBDAV", "0") == "1"
20
21
  logger = get_logger("server", term_level="DEBUG")
21
22
  logger_failed_request = get_logger("failed_requests", term_level="INFO")
22
23
  db = Database()
@@ -46,12 +47,14 @@ def handle_exception(fn):
46
47
  if isinstance(e, StorageExceededError): raise HTTPException(status_code=413, detail=str(e))
47
48
  if isinstance(e, PermissionError): raise HTTPException(status_code=403, detail=str(e))
48
49
  if isinstance(e, InvalidPathError): raise HTTPException(status_code=400, detail=str(e))
50
+ if isinstance(e, InvalidOptionsError): raise HTTPException(status_code=400, detail=str(e))
51
+ if isinstance(e, InvalidDataError): raise HTTPException(status_code=400, detail=str(e))
49
52
  if isinstance(e, FileNotFoundError): raise HTTPException(status_code=404, detail=str(e))
53
+ if isinstance(e, FileDuplicateError): raise HTTPException(status_code=409, detail=str(e))
50
54
  if isinstance(e, FileExistsError): raise HTTPException(status_code=409, detail=str(e))
51
55
  if isinstance(e, TooManyItemsError): raise HTTPException(status_code=400, detail=str(e))
52
56
  if isinstance(e, DatabaseLockedError): raise HTTPException(status_code=503, detail=str(e))
53
57
  if isinstance(e, FileLockedError): raise HTTPException(status_code=423, detail=str(e))
54
- if isinstance(e, InvalidOptionsError): raise HTTPException(status_code=400, detail=str(e))
55
58
  logger.error(f"Uncaptured error in {fn.__name__}: {e}")
56
59
  raise
57
60
  return wrapper
@@ -119,13 +122,13 @@ async def get_current_user(
119
122
  uconn = UserConn(conn)
120
123
  if h_token:
121
124
  user = await uconn.get_user_by_credential(h_token.credentials)
122
- if not user: raise HTTPException(status_code=401, detail="Invalid token", headers={"WWW-Authenticate": "Basic"})
123
- elif b_token:
125
+ if not user: raise HTTPException(status_code=401, detail="Invalid token", headers={"WWW-Authenticate": "Basic" if ENABLE_WEBDAV else "Bearer"})
126
+ elif ENABLE_WEBDAV and b_token:
124
127
  user = await uconn.get_user_by_credential(hash_credential(b_token.username, b_token.password))
125
- if not user: raise HTTPException(status_code=401, detail="Invalid token", headers={"WWW-Authenticate": "Basic"})
128
+ if not user: raise HTTPException(status_code=401, detail="Invalid token", headers={"WWW-Authenticate": "Basic" if ENABLE_WEBDAV else "Bearer"})
126
129
  elif q_token:
127
130
  user = await uconn.get_user_by_credential(q_token)
128
- if not user: raise HTTPException(status_code=401, detail="Invalid token", headers={"WWW-Authenticate": "Basic"})
131
+ if not user: raise HTTPException(status_code=401, detail="Invalid token", headers={"WWW-Authenticate": "Basic" if ENABLE_WEBDAV else "Bearer"})
129
132
  else:
130
133
  return DECOY_USER
131
134
 
@@ -136,10 +139,9 @@ async def get_current_user(
136
139
 
137
140
  async def registered_user(user: UserRecord = Depends(get_current_user)):
138
141
  if user.id == 0:
139
- raise HTTPException(status_code=401, detail="Permission denied", headers={"WWW-Authenticate": "Basic"})
142
+ raise HTTPException(status_code=401, detail="Permission denied", headers={"WWW-Authenticate": "Basic" if ENABLE_WEBDAV else "Bearer"})
140
143
  return user
141
144
 
142
-
143
145
  router_api = APIRouter(prefix="/_api")
144
146
  router_dav = APIRouter(prefix="")
145
147
  router_fs = APIRouter(prefix="")