lfss 0.9.2__py3-none-any.whl → 0.11.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 +4 -4
- docs/Enviroment_variables.md +4 -2
- docs/Permission.md +4 -4
- docs/Webdav.md +3 -3
- docs/changelog.md +58 -0
- frontend/api.js +66 -4
- frontend/login.js +0 -1
- frontend/popup.js +18 -3
- frontend/scripts.js +46 -39
- frontend/utils.js +98 -1
- lfss/api/__init__.py +7 -4
- lfss/api/connector.py +47 -11
- lfss/cli/cli.py +9 -9
- lfss/cli/log.py +77 -0
- lfss/cli/vacuum.py +69 -19
- lfss/eng/config.py +7 -5
- lfss/eng/connection_pool.py +12 -8
- lfss/eng/database.py +350 -140
- lfss/eng/error.py +6 -2
- lfss/eng/log.py +91 -21
- lfss/eng/thumb.py +20 -23
- lfss/eng/utils.py +50 -29
- lfss/sql/init.sql +9 -4
- lfss/svc/app.py +1 -1
- lfss/svc/app_base.py +8 -3
- lfss/svc/app_dav.py +74 -61
- lfss/svc/app_native.py +95 -59
- lfss/svc/common_impl.py +72 -37
- {lfss-0.9.2.dist-info → lfss-0.11.4.dist-info}/METADATA +10 -8
- lfss-0.11.4.dist-info/RECORD +52 -0
- {lfss-0.9.2.dist-info → lfss-0.11.4.dist-info}/entry_points.txt +1 -0
- lfss-0.9.2.dist-info/RECORD +0 -50
- {lfss-0.9.2.dist-info → lfss-0.11.4.dist-info}/WHEEL +0 -0
lfss/eng/error.py
CHANGED
@@ -6,16 +6,20 @@ 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
|
|
15
|
+
class DatabaseTransactionError(LFSSExceptionBase, sqlite3.DatabaseError):...
|
16
|
+
|
11
17
|
class PathNotFoundError(LFSSExceptionBase, FileNotFoundError):...
|
12
18
|
|
13
19
|
class FileDuplicateError(LFSSExceptionBase, FileExistsError):...
|
14
20
|
|
15
21
|
class PermissionDeniedError(LFSSExceptionBase, PermissionError):...
|
16
22
|
|
17
|
-
class InvalidPathError(LFSSExceptionBase, ValueError):...
|
18
|
-
|
19
23
|
class StorageExceededError(LFSSExceptionBase):...
|
20
24
|
|
21
25
|
class TooManyItemsError(LFSSExceptionBase):...
|
lfss/eng/log.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
from .config import
|
1
|
+
from .config import LOG_DIR, DISABLE_LOGGING
|
2
|
+
import time, sqlite3, dataclasses
|
2
3
|
from typing import TypeVar, Callable, Literal, Optional
|
3
4
|
from concurrent.futures import ThreadPoolExecutor
|
4
5
|
from functools import wraps
|
5
|
-
import logging,
|
6
|
+
import logging, asyncio
|
6
7
|
from logging import handlers
|
7
8
|
|
8
9
|
class BCOLORS:
|
@@ -57,15 +58,81 @@ class BaseLogger(logging.Logger):
|
|
57
58
|
@thread_wrap
|
58
59
|
def error(self, *args, **kwargs): super().error(*args, **kwargs)
|
59
60
|
|
60
|
-
|
61
|
+
class SQLiteFileHandler(logging.FileHandler):
|
62
|
+
def __init__(self, filename, *args, **kwargs):
|
63
|
+
super().__init__(filename, *args, **kwargs)
|
64
|
+
self._db_file = filename
|
65
|
+
self._buffer: list[logging.LogRecord] = []
|
66
|
+
self._buffer_size = 100
|
67
|
+
self._flush_interval = 10
|
68
|
+
self._last_flush = time.time()
|
69
|
+
conn = sqlite3.connect(self._db_file, check_same_thread=False)
|
70
|
+
conn.execute('PRAGMA journal_mode=WAL')
|
71
|
+
conn.execute('''
|
72
|
+
CREATE TABLE IF NOT EXISTS log (
|
73
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
74
|
+
created TIMESTAMP,
|
75
|
+
created_epoch FLOAT,
|
76
|
+
name TEXT,
|
77
|
+
levelname VARCHAR(16),
|
78
|
+
level INTEGER,
|
79
|
+
message TEXT
|
80
|
+
)
|
81
|
+
''')
|
82
|
+
conn.commit()
|
83
|
+
conn.close()
|
84
|
+
|
85
|
+
def flush(self):
|
86
|
+
def format_time(self, record: logging.LogRecord):
|
87
|
+
""" Create a time stamp """
|
88
|
+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created))
|
89
|
+
self.acquire()
|
90
|
+
try:
|
91
|
+
conn = sqlite3.connect(self._db_file, check_same_thread=False)
|
92
|
+
conn.executemany('''
|
93
|
+
INSERT INTO log (created, created_epoch, name, levelname, level, message)
|
94
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
95
|
+
''', [
|
96
|
+
(format_time(self, record), record.created, record.name, record.levelname, record.levelno, record.getMessage())
|
97
|
+
for record in self._buffer
|
98
|
+
])
|
99
|
+
conn.commit()
|
100
|
+
conn.close()
|
101
|
+
self._buffer.clear()
|
102
|
+
self._last_flush = time.time()
|
103
|
+
finally:
|
104
|
+
self.release()
|
105
|
+
|
106
|
+
def emit(self, record: logging.LogRecord):
|
107
|
+
self._buffer.append(record)
|
108
|
+
if len(self._buffer) > self._buffer_size or time.time() - self._last_flush > self._flush_interval:
|
109
|
+
self.flush()
|
110
|
+
|
111
|
+
def close(self):
|
112
|
+
self.flush()
|
113
|
+
return super().close()
|
114
|
+
|
115
|
+
def eval_logline(row: sqlite3.Row):
|
116
|
+
@dataclasses.dataclass
|
117
|
+
class DBLogRecord:
|
118
|
+
id: int
|
119
|
+
created: str
|
120
|
+
created_epoch: float
|
121
|
+
name: str
|
122
|
+
levelname: str
|
123
|
+
level: int
|
124
|
+
message: str
|
125
|
+
return DBLogRecord(*row)
|
126
|
+
|
127
|
+
_fh_T = Literal['rotate', 'simple', 'daily', 'sqlite']
|
61
128
|
|
62
129
|
__g_logger_dict: dict[str, BaseLogger] = {}
|
63
130
|
def get_logger(
|
64
131
|
name = 'default',
|
65
|
-
log_home =
|
132
|
+
log_home = LOG_DIR,
|
66
133
|
level = 'DEBUG',
|
67
134
|
term_level = 'INFO',
|
68
|
-
file_handler_type: _fh_T = '
|
135
|
+
file_handler_type: _fh_T = 'sqlite',
|
69
136
|
global_instance = True
|
70
137
|
)->BaseLogger:
|
71
138
|
if global_instance and name in __g_logger_dict:
|
@@ -87,22 +154,25 @@ def get_logger(
|
|
87
154
|
if isinstance(color, str) and color.startswith('\033'):
|
88
155
|
format_str_plain = format_str_plain.replace(color, '')
|
89
156
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
157
|
+
if not DISABLE_LOGGING:
|
158
|
+
formatter_plain = logging.Formatter(format_str_plain)
|
159
|
+
log_home.mkdir(exist_ok=True)
|
160
|
+
log_file = log_home / f'{name}.log'
|
161
|
+
if file_handler_type == 'simple':
|
162
|
+
file_handler = logging.FileHandler(log_file)
|
163
|
+
elif file_handler_type == 'daily':
|
164
|
+
file_handler = handlers.TimedRotatingFileHandler(
|
165
|
+
log_file, when='midnight', interval=1, backupCount=30
|
166
|
+
)
|
167
|
+
elif file_handler_type == 'rotate':
|
168
|
+
file_handler = handlers.RotatingFileHandler(
|
169
|
+
log_file, maxBytes=1024*1024, backupCount=5
|
170
|
+
)
|
171
|
+
elif file_handler_type == 'sqlite':
|
172
|
+
file_handler = SQLiteFileHandler(log_file if log_file.suffix == '.db' else log_file.with_suffix('.log.db'))
|
173
|
+
|
174
|
+
file_handler.setFormatter(formatter_plain)
|
175
|
+
logger.addHandler(file_handler)
|
106
176
|
|
107
177
|
logger = BaseLogger(name)
|
108
178
|
setupLogger(logger)
|
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
|
@@ -10,44 +11,42 @@ from contextlib import asynccontextmanager
|
|
10
11
|
async def _maybe_init_thumb(c: aiosqlite.Cursor):
|
11
12
|
await c.execute('''
|
12
13
|
CREATE TABLE IF NOT EXISTS thumbs (
|
13
|
-
|
14
|
-
ctime TEXT,
|
14
|
+
file_id CHAR(32) PRIMARY KEY,
|
15
15
|
thumb BLOB
|
16
16
|
)
|
17
17
|
''')
|
18
|
-
await c.execute('CREATE INDEX IF NOT EXISTS thumbs_path_idx ON thumbs (
|
18
|
+
await c.execute('CREATE INDEX IF NOT EXISTS thumbs_path_idx ON thumbs (file_id)')
|
19
19
|
|
20
|
-
async def _get_cache_thumb(c: aiosqlite.Cursor,
|
20
|
+
async def _get_cache_thumb(c: aiosqlite.Cursor, file_id: str) -> Optional[bytes]:
|
21
21
|
res = await c.execute('''
|
22
|
-
SELECT
|
23
|
-
''', (
|
22
|
+
SELECT thumb FROM thumbs WHERE file_id = ?
|
23
|
+
''', (file_id, ))
|
24
24
|
row = await res.fetchone()
|
25
25
|
if row is None:
|
26
26
|
return None
|
27
|
-
|
28
|
-
if row[0] != ctime:
|
29
|
-
await _delete_cache_thumb(c, path)
|
30
|
-
return None
|
31
|
-
blob: bytes = row[1]
|
27
|
+
blob: bytes = row[0]
|
32
28
|
return blob
|
33
29
|
|
34
|
-
async def _save_cache_thumb(c: aiosqlite.Cursor,
|
35
|
-
|
30
|
+
async def _save_cache_thumb(c: aiosqlite.Cursor, file_id: str, raw_bytes: bytes) -> bytes:
|
31
|
+
try:
|
32
|
+
raw_img = Image.open(BytesIO(raw_bytes))
|
33
|
+
except Exception:
|
34
|
+
raise InvalidDataError('Invalid image data for thumbnail: ' + file_id)
|
36
35
|
raw_img.thumbnail(THUMB_SIZE)
|
37
36
|
img = raw_img.convert('RGB')
|
38
37
|
bio = BytesIO()
|
39
38
|
img.save(bio, 'JPEG')
|
40
39
|
blob = bio.getvalue()
|
41
40
|
await c.execute('''
|
42
|
-
INSERT OR REPLACE INTO thumbs (
|
43
|
-
''', (
|
41
|
+
INSERT OR REPLACE INTO thumbs (file_id, thumb) VALUES (?, ?)
|
42
|
+
''', (file_id, blob))
|
44
43
|
await c.execute('COMMIT') # commit immediately
|
45
44
|
return blob
|
46
45
|
|
47
|
-
async def _delete_cache_thumb(c: aiosqlite.Cursor,
|
46
|
+
async def _delete_cache_thumb(c: aiosqlite.Cursor, file_id: str):
|
48
47
|
await c.execute('''
|
49
|
-
DELETE FROM thumbs WHERE
|
50
|
-
''', (
|
48
|
+
DELETE FROM thumbs WHERE file_id = ?
|
49
|
+
''', (file_id, ))
|
51
50
|
await c.execute('COMMIT')
|
52
51
|
|
53
52
|
@asynccontextmanager
|
@@ -71,15 +70,13 @@ async def get_thumb(path: str) -> Optional[tuple[bytes, str]]:
|
|
71
70
|
r = await fconn.get_file_record(path)
|
72
71
|
|
73
72
|
if r is None:
|
74
|
-
async with cache_cursor() as cur:
|
75
|
-
await _delete_cache_thumb(cur, path)
|
76
73
|
raise FileNotFoundError(f'File not found: {path}')
|
77
74
|
if not r.mime_type.startswith('image/'):
|
78
75
|
return None
|
79
76
|
|
77
|
+
file_id = r.file_id
|
80
78
|
async with cache_cursor() as cur:
|
81
|
-
|
82
|
-
thumb_blob = await _get_cache_thumb(cur, path, c_time)
|
79
|
+
thumb_blob = await _get_cache_thumb(cur, file_id)
|
83
80
|
if thumb_blob is not None:
|
84
81
|
return thumb_blob, "image/jpeg"
|
85
82
|
|
@@ -94,5 +91,5 @@ async def get_thumb(path: str) -> Optional[tuple[bytes, str]]:
|
|
94
91
|
data = await fconn.get_file_blob(r.file_id)
|
95
92
|
assert data is not None
|
96
93
|
|
97
|
-
thumb_blob = await _save_cache_thumb(cur,
|
94
|
+
thumb_blob = await _save_cache_thumb(cur, file_id, data)
|
98
95
|
return thumb_blob, "image/jpeg"
|
lfss/eng/utils.py
CHANGED
@@ -11,7 +11,6 @@ from concurrent.futures import ThreadPoolExecutor
|
|
11
11
|
from typing import TypeVar, Callable, Awaitable
|
12
12
|
from functools import wraps, partial
|
13
13
|
from uuid import uuid4
|
14
|
-
import os
|
15
14
|
|
16
15
|
async def copy_file(source: str|pathlib.Path, destination: str|pathlib.Path):
|
17
16
|
async with aiofiles.open(source, mode='rb') as src:
|
@@ -20,7 +19,7 @@ async def copy_file(source: str|pathlib.Path, destination: str|pathlib.Path):
|
|
20
19
|
await dest.write(chunk)
|
21
20
|
|
22
21
|
def hash_credential(username: str, password: str):
|
23
|
-
return hashlib.sha256(
|
22
|
+
return hashlib.sha256(f"{username}:{password}".encode()).hexdigest()
|
24
23
|
|
25
24
|
def encode_uri_compnents(path: str):
|
26
25
|
path_sp = path.split("/")
|
@@ -36,17 +35,41 @@ def ensure_uri_compnents(path: str):
|
|
36
35
|
""" Ensure the path components are safe to use """
|
37
36
|
return encode_uri_compnents(decode_uri_compnents(path))
|
38
37
|
|
39
|
-
|
40
|
-
|
38
|
+
class TaskManager:
|
39
|
+
def __init__(self):
|
40
|
+
self._tasks: OrderedDict[str, asyncio.Task] = OrderedDict()
|
41
|
+
|
42
|
+
def push(self, task: asyncio.Task) -> str:
|
43
|
+
tid = uuid4().hex
|
44
|
+
if tid in self._tasks:
|
45
|
+
raise ValueError("Task ID collision")
|
46
|
+
self._tasks[tid] = task
|
47
|
+
return tid
|
48
|
+
|
49
|
+
def cancel(self, task_id: str):
|
50
|
+
task = self._tasks.pop(task_id, None)
|
51
|
+
if task is not None:
|
52
|
+
task.cancel()
|
53
|
+
|
54
|
+
def truncate(self):
|
55
|
+
new_tasks = OrderedDict()
|
56
|
+
for tid, task in self._tasks.items():
|
57
|
+
if not task.done():
|
58
|
+
new_tasks[tid] = task
|
59
|
+
self._tasks = new_tasks
|
60
|
+
|
61
|
+
async def wait_all(self):
|
62
|
+
async def stop_task(task: asyncio.Task):
|
63
|
+
if not task.done():
|
64
|
+
await task
|
65
|
+
await asyncio.gather(*map(stop_task, self._tasks.values()))
|
66
|
+
self._tasks.clear()
|
67
|
+
|
68
|
+
def __len__(self): return len(self._tasks)
|
69
|
+
|
70
|
+
g_debounce_tasks: TaskManager = TaskManager()
|
41
71
|
async def wait_for_debounce_tasks():
|
42
|
-
|
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()
|
72
|
+
await g_debounce_tasks.wait_all()
|
50
73
|
|
51
74
|
def debounce_async(delay: float = 0.1, max_wait: float = 1.):
|
52
75
|
"""
|
@@ -54,7 +77,8 @@ def debounce_async(delay: float = 0.1, max_wait: float = 1.):
|
|
54
77
|
ensuring execution at least once every `max_wait` seconds.
|
55
78
|
"""
|
56
79
|
def debounce_wrap(func):
|
57
|
-
task_record: tuple[str, asyncio.Task] | None = None
|
80
|
+
# task_record: tuple[str, asyncio.Task] | None = None
|
81
|
+
prev_task_id = None
|
58
82
|
fn_execution_lock = Lock()
|
59
83
|
last_execution_time = 0
|
60
84
|
|
@@ -67,12 +91,11 @@ def debounce_async(delay: float = 0.1, max_wait: float = 1.):
|
|
67
91
|
|
68
92
|
@functools.wraps(func)
|
69
93
|
async def wrapper(*args, **kwargs):
|
70
|
-
nonlocal
|
94
|
+
nonlocal prev_task_id, last_execution_time
|
71
95
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
g_debounce_tasks.pop(task_record[0], None)
|
96
|
+
if prev_task_id is not None:
|
97
|
+
g_debounce_tasks.cancel(prev_task_id)
|
98
|
+
prev_task_id = None
|
76
99
|
|
77
100
|
async with fn_execution_lock:
|
78
101
|
if time.monotonic() - last_execution_time > max_wait:
|
@@ -81,14 +104,12 @@ def debounce_async(delay: float = 0.1, max_wait: float = 1.):
|
|
81
104
|
return
|
82
105
|
|
83
106
|
task = asyncio.create_task(delayed_func(*args, **kwargs))
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
# so we need to clear it periodically
|
91
|
-
await wait_for_debounce_tasks()
|
107
|
+
prev_task_id = g_debounce_tasks.push(task)
|
108
|
+
if len(g_debounce_tasks) > 1024:
|
109
|
+
# finished tasks are not removed from the dict
|
110
|
+
# so we need to clear it periodically
|
111
|
+
g_debounce_tasks.truncate()
|
112
|
+
|
92
113
|
return wrapper
|
93
114
|
return debounce_wrap
|
94
115
|
|
@@ -133,12 +154,12 @@ def fmt_storage_size(size: int) -> str:
|
|
133
154
|
return f"{size/1024**4:.2f}T"
|
134
155
|
|
135
156
|
_FnReturnT = TypeVar('_FnReturnT')
|
136
|
-
_AsyncReturnT = Awaitable
|
157
|
+
_AsyncReturnT = TypeVar('_AsyncReturnT', bound=Awaitable)
|
137
158
|
_g_executor = None
|
138
159
|
def get_global_executor():
|
139
160
|
global _g_executor
|
140
161
|
if _g_executor is None:
|
141
|
-
_g_executor = ThreadPoolExecutor(
|
162
|
+
_g_executor = ThreadPoolExecutor()
|
142
163
|
return _g_executor
|
143
164
|
def async_wrap(executor=None):
|
144
165
|
if executor is None:
|
@@ -157,7 +178,7 @@ def concurrent_wrap(executor=None):
|
|
157
178
|
def sync_fn(*args, **kwargs):
|
158
179
|
loop = asyncio.new_event_loop()
|
159
180
|
return loop.run_until_complete(func(*args, **kwargs))
|
160
|
-
return sync_fn
|
181
|
+
return sync_fn # type: ignore
|
161
182
|
return _concurrent_wrap
|
162
183
|
|
163
184
|
# https://stackoverflow.com/a/279586/6775765
|
lfss/sql/init.sql
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
CREATE TABLE IF NOT EXISTS user (
|
1
|
+
CREATE TABLE IF NOT EXISTS main.user (
|
2
2
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
3
3
|
username VARCHAR(256) UNIQUE NOT NULL,
|
4
4
|
credential VARCHAR(256) NOT NULL,
|
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS user (
|
|
9
9
|
permission INTEGER DEFAULT 0
|
10
10
|
);
|
11
11
|
|
12
|
-
CREATE TABLE IF NOT EXISTS fmeta (
|
12
|
+
CREATE TABLE IF NOT EXISTS main.fmeta (
|
13
13
|
url VARCHAR(1024) PRIMARY KEY,
|
14
14
|
owner_id INTEGER NOT NULL,
|
15
15
|
file_id CHAR(32) NOT NULL,
|
@@ -22,12 +22,17 @@ CREATE TABLE IF NOT EXISTS fmeta (
|
|
22
22
|
FOREIGN KEY(owner_id) REFERENCES user(id)
|
23
23
|
);
|
24
24
|
|
25
|
-
CREATE TABLE IF NOT EXISTS
|
25
|
+
CREATE TABLE IF NOT EXISTS main.dupcount (
|
26
|
+
file_id CHAR(32) PRIMARY KEY,
|
27
|
+
count INTEGER DEFAULT 0
|
28
|
+
);
|
29
|
+
|
30
|
+
CREATE TABLE IF NOT EXISTS main.usize (
|
26
31
|
user_id INTEGER PRIMARY KEY,
|
27
32
|
size INTEGER DEFAULT 0
|
28
33
|
);
|
29
34
|
|
30
|
-
CREATE TABLE IF NOT EXISTS upeer (
|
35
|
+
CREATE TABLE IF NOT EXISTS main.upeer (
|
31
36
|
src_user_id INTEGER NOT NULL,
|
32
37
|
dst_user_id INTEGER NOT NULL,
|
33
38
|
access_level INTEGER DEFAULT 0,
|
lfss/svc/app.py
CHANGED
lfss/svc/app_base.py
CHANGED
@@ -27,7 +27,7 @@ req_conn = RequestDB()
|
|
27
27
|
async def lifespan(app: FastAPI):
|
28
28
|
global db
|
29
29
|
try:
|
30
|
-
await global_connection_init(n_read =
|
30
|
+
await global_connection_init(n_read = 8 if not DEBUG_MODE else 1)
|
31
31
|
await asyncio.gather(db.init(), req_conn.init())
|
32
32
|
yield
|
33
33
|
await req_conn.commit()
|
@@ -47,21 +47,26 @@ def handle_exception(fn):
|
|
47
47
|
if isinstance(e, StorageExceededError): raise HTTPException(status_code=413, detail=str(e))
|
48
48
|
if isinstance(e, PermissionError): raise HTTPException(status_code=403, detail=str(e))
|
49
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))
|
50
52
|
if isinstance(e, FileNotFoundError): raise HTTPException(status_code=404, detail=str(e))
|
51
53
|
if isinstance(e, FileDuplicateError): raise HTTPException(status_code=409, detail=str(e))
|
52
54
|
if isinstance(e, FileExistsError): raise HTTPException(status_code=409, detail=str(e))
|
53
55
|
if isinstance(e, TooManyItemsError): raise HTTPException(status_code=400, detail=str(e))
|
54
56
|
if isinstance(e, DatabaseLockedError): raise HTTPException(status_code=503, detail=str(e))
|
57
|
+
if isinstance(e, DatabaseTransactionError): raise HTTPException(status_code=503, detail=str(e))
|
55
58
|
if isinstance(e, FileLockedError): raise HTTPException(status_code=423, detail=str(e))
|
56
|
-
if isinstance(e, InvalidOptionsError): raise HTTPException(status_code=400, detail=str(e))
|
57
59
|
logger.error(f"Uncaptured error in {fn.__name__}: {e}")
|
58
60
|
raise
|
59
61
|
return wrapper
|
60
62
|
|
63
|
+
env_origins = os.environ.get("LFSS_ORIGINS", "*")
|
64
|
+
logger.debug(f"LFSS_ORIGINS: {env_origins}")
|
65
|
+
origins = [x.strip() for x in env_origins.split(",") if x.strip()]
|
61
66
|
app = FastAPI(docs_url=None, redoc_url=None, lifespan=lifespan)
|
62
67
|
app.add_middleware(
|
63
68
|
CORSMiddleware,
|
64
|
-
allow_origins=
|
69
|
+
allow_origins=origins,
|
65
70
|
allow_credentials=True,
|
66
71
|
allow_methods=["*"],
|
67
72
|
allow_headers=["*"],
|