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 +4 -2
- docs/Changelog.md +27 -0
- docs/Enviroment_variables.md +12 -0
- docs/Known_issues.md +3 -1
- docs/Webdav.md +3 -3
- frontend/api.js +21 -0
- frontend/scripts.js +49 -2
- lfss/api/connector.py +6 -0
- lfss/eng/connection_pool.py +1 -1
- lfss/eng/database.py +13 -16
- lfss/eng/error.py +6 -2
- lfss/eng/thumb.py +5 -1
- lfss/eng/utils.py +46 -24
- lfss/svc/app.py +2 -2
- lfss/svc/app_base.py +10 -8
- lfss/svc/app_dav.py +114 -96
- lfss/svc/app_native.py +15 -9
- lfss/svc/common_impl.py +98 -31
- {lfss-0.9.1.dist-info → lfss-0.9.4.dist-info}/METADATA +5 -3
- {lfss-0.9.1.dist-info → lfss-0.9.4.dist-info}/RECORD +22 -20
- {lfss-0.9.1.dist-info → lfss-0.9.4.dist-info}/WHEEL +0 -0
- {lfss-0.9.1.dist-info → lfss-0.9.4.dist-info}/entry_points.txt +0 -0
lfss/svc/app_dav.py
CHANGED
@@ -3,16 +3,17 @@
|
|
3
3
|
from fastapi import Request, Response, Depends, HTTPException
|
4
4
|
import time, uuid, os
|
5
5
|
import aiosqlite
|
6
|
+
import asyncio
|
6
7
|
from typing import Literal, Optional
|
7
8
|
import xml.etree.ElementTree as ET
|
8
9
|
from ..eng.connection_pool import unique_cursor
|
9
10
|
from ..eng.error import *
|
10
|
-
from ..eng.config import DATA_HOME
|
11
|
-
from ..eng.datatype import UserRecord, FileRecord, DirectoryRecord
|
12
|
-
from ..eng.database import FileConn
|
13
|
-
from ..eng.utils import ensure_uri_compnents, decode_uri_compnents, format_last_modified
|
11
|
+
from ..eng.config import DATA_HOME, DEBUG_MODE
|
12
|
+
from ..eng.datatype import UserRecord, FileRecord, DirectoryRecord, AccessLevel
|
13
|
+
from ..eng.database import FileConn, UserConn, check_path_permission
|
14
|
+
from ..eng.utils import ensure_uri_compnents, decode_uri_compnents, format_last_modified, static_vars
|
14
15
|
from .app_base import *
|
15
|
-
from .common_impl import
|
16
|
+
from .common_impl import copy_impl
|
16
17
|
|
17
18
|
LOCK_DB_PATH = DATA_HOME / "lock.db"
|
18
19
|
MKDIR_PLACEHOLDER = ".lfss_keep"
|
@@ -52,11 +53,27 @@ async def eval_path(path: str) -> tuple[ptype, str, Optional[FileRecord | Direct
|
|
52
53
|
# path now is url-safe and without leading slash
|
53
54
|
if path.endswith("/"):
|
54
55
|
lfss_path = path
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
dir_path_sp = path.split("/")
|
57
|
+
if len(dir_path_sp) > 2:
|
58
|
+
async with unique_cursor() as c:
|
59
|
+
fconn = FileConn(c)
|
60
|
+
if await fconn.count_path_files(path, flat=True) == 0:
|
61
|
+
return None, lfss_path, None
|
62
|
+
return "dir", lfss_path, await fconn.get_path_record(path)
|
63
|
+
else:
|
64
|
+
# test if its a user's root directory
|
65
|
+
assert len(dir_path_sp) == 2
|
66
|
+
username = path.split("/")[0]
|
67
|
+
async with unique_cursor() as c:
|
68
|
+
uconn = UserConn(c)
|
69
|
+
u = await uconn.get_user(username)
|
70
|
+
if u is None:
|
71
|
+
return None, lfss_path, None
|
72
|
+
return "dir", lfss_path, DirectoryRecord(lfss_path)
|
73
|
+
|
74
|
+
# may be root directory
|
75
|
+
if path == "":
|
76
|
+
return "dir", "", DirectoryRecord("")
|
60
77
|
|
61
78
|
# not end with /, check if it is a file
|
62
79
|
async with unique_cursor() as c:
|
@@ -65,11 +82,10 @@ async def eval_path(path: str) -> tuple[ptype, str, Optional[FileRecord | Direct
|
|
65
82
|
lfss_path = path
|
66
83
|
return "file", lfss_path, res
|
67
84
|
|
68
|
-
if path == "": return "dir", "", DirectoryRecord("")
|
69
85
|
async with unique_cursor() as c:
|
86
|
+
lfss_path = path + "/"
|
70
87
|
fconn = FileConn(c)
|
71
|
-
if await fconn.count_path_files(
|
72
|
-
lfss_path = path + "/"
|
88
|
+
if await fconn.count_path_files(lfss_path) > 0:
|
73
89
|
return "dir", lfss_path, await fconn.get_path_record(lfss_path)
|
74
90
|
|
75
91
|
return None, path, None
|
@@ -79,12 +95,14 @@ CREATE TABLE IF NOT EXISTS locks (
|
|
79
95
|
path TEXT PRIMARY KEY,
|
80
96
|
user TEXT,
|
81
97
|
token TEXT,
|
98
|
+
depth TEXT,
|
82
99
|
timeout float,
|
83
100
|
lock_time float
|
84
101
|
);
|
85
102
|
"""
|
86
|
-
async def lock_path(user: UserRecord, p: str, token: str, timeout: int =
|
103
|
+
async def lock_path(user: UserRecord, p: str, token: str, depth: str, timeout: int = 1800):
|
87
104
|
async with aiosqlite.connect(LOCK_DB_PATH) as conn:
|
105
|
+
await conn.execute("BEGIN EXCLUSIVE")
|
88
106
|
await conn.execute(lock_table_create_sql)
|
89
107
|
async with conn.execute("SELECT user, timeout, lock_time FROM locks WHERE path=?", (p,)) as cur:
|
90
108
|
row = await cur.fetchone()
|
@@ -93,10 +111,11 @@ async def lock_path(user: UserRecord, p: str, token: str, timeout: int = 600):
|
|
93
111
|
curr_time = time.time()
|
94
112
|
if timeout > 0 and curr_time - lock_time_ < timeout_:
|
95
113
|
raise FileLockedError(f"File is locked (by {user_}) [{p}]")
|
96
|
-
await cur.execute("INSERT OR REPLACE INTO locks VALUES (?, ?, ?, ?, ?)", (p, user.username, token, timeout, time.time()))
|
114
|
+
await cur.execute("INSERT OR REPLACE INTO locks VALUES (?, ?, ?, ?, ?, ?)", (p, user.username, token, depth, timeout, time.time()))
|
97
115
|
await conn.commit()
|
98
116
|
async def unlock_path(user: UserRecord, p: str, token: str):
|
99
117
|
async with aiosqlite.connect(LOCK_DB_PATH) as conn:
|
118
|
+
await conn.execute("BEGIN EXCLUSIVE")
|
100
119
|
await conn.execute(lock_table_create_sql)
|
101
120
|
async with conn.execute("SELECT user, token FROM locks WHERE path=?", (p,)) as cur:
|
102
121
|
row = await cur.fetchone()
|
@@ -106,31 +125,34 @@ async def unlock_path(user: UserRecord, p: str, token: str):
|
|
106
125
|
raise FileLockedError(f"Failed to unlock file [{p}] with token {token}")
|
107
126
|
await cur.execute("DELETE FROM locks WHERE path=?", (p,))
|
108
127
|
await conn.commit()
|
109
|
-
async def
|
128
|
+
async def query_lock_element(p: str, top_el_name: str = f"{{{DAV_NS}}}lockinfo") -> Optional[ET.Element]:
|
110
129
|
async with aiosqlite.connect(LOCK_DB_PATH) as conn:
|
130
|
+
await conn.execute("BEGIN EXCLUSIVE")
|
111
131
|
await conn.execute(lock_table_create_sql)
|
112
|
-
async with conn.execute("SELECT user, token, timeout, lock_time FROM locks WHERE path=?", (p,)) as cur:
|
132
|
+
async with conn.execute("SELECT user, token, depth, timeout, lock_time FROM locks WHERE path=?", (p,)) as cur:
|
113
133
|
row = await cur.fetchone()
|
114
134
|
if not row: return None
|
115
135
|
curr_time = time.time()
|
116
|
-
|
136
|
+
username, token, depth, timeout, lock_time = row
|
117
137
|
if timeout > 0 and curr_time - lock_time > timeout:
|
118
138
|
await cur.execute("DELETE FROM locks WHERE path=?", (p,))
|
119
139
|
await conn.commit()
|
120
140
|
return None
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
141
|
+
lock_info = ET.Element(top_el_name)
|
142
|
+
locktype = ET.SubElement(lock_info, f"{{{DAV_NS}}}locktype")
|
143
|
+
ET.SubElement(locktype, f"{{{DAV_NS}}}write")
|
144
|
+
lockscope = ET.SubElement(lock_info, f"{{{DAV_NS}}}lockscope")
|
145
|
+
ET.SubElement(lockscope, f"{{{DAV_NS}}}exclusive")
|
146
|
+
owner = ET.SubElement(lock_info, f"{{{DAV_NS}}}owner")
|
147
|
+
owner.text = username
|
148
|
+
depth_el = ET.SubElement(lock_info, f"{{{DAV_NS}}}depth")
|
149
|
+
depth_el.text = depth
|
150
|
+
timeout = ET.SubElement(lock_info, f"{{{DAV_NS}}}timeout")
|
151
|
+
timeout.text = f"Second-{timeout}"
|
152
|
+
locktoken = ET.SubElement(lock_info, f"{{{DAV_NS}}}locktoken")
|
153
|
+
href = ET.SubElement(locktoken, f"{{{DAV_NS}}}href")
|
154
|
+
href.text = f"{token}"
|
155
|
+
return lock_info
|
134
156
|
|
135
157
|
async def create_file_xml_element(frecord: FileRecord) -> ET.Element:
|
136
158
|
file_el = ET.Element(f"{{{DAV_NS}}}response")
|
@@ -138,15 +160,16 @@ async def create_file_xml_element(frecord: FileRecord) -> ET.Element:
|
|
138
160
|
href.text = f"/{frecord.url}"
|
139
161
|
propstat = ET.SubElement(file_el, f"{{{DAV_NS}}}propstat")
|
140
162
|
prop = ET.SubElement(propstat, f"{{{DAV_NS}}}prop")
|
141
|
-
ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = frecord.url.split("/")[-1]
|
163
|
+
ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = decode_uri_compnents(frecord.url.split("/")[-1])
|
142
164
|
ET.SubElement(prop, f"{{{DAV_NS}}}resourcetype")
|
143
165
|
ET.SubElement(prop, f"{{{DAV_NS}}}getcontentlength").text = str(frecord.file_size)
|
144
166
|
ET.SubElement(prop, f"{{{DAV_NS}}}getlastmodified").text = format_last_modified(frecord.create_time)
|
145
167
|
ET.SubElement(prop, f"{{{DAV_NS}}}getcontenttype").text = frecord.mime_type
|
146
|
-
|
147
|
-
lock_el = await query_lock_el(frecord.url, top_el_name=f"{{{DAV_NS}}}activelock")
|
168
|
+
lock_el = await query_lock_element(frecord.url, top_el_name=f"{{{DAV_NS}}}activelock")
|
148
169
|
if lock_el is not None:
|
170
|
+
lock_discovery = ET.SubElement(prop, f"{{{DAV_NS}}}lockdiscovery")
|
149
171
|
lock_discovery.append(lock_el)
|
172
|
+
ET.SubElement(propstat, f"{{{DAV_NS}}}status").text = "HTTP/1.1 200 OK"
|
150
173
|
return file_el
|
151
174
|
|
152
175
|
async def create_dir_xml_element(drecord: DirectoryRecord) -> ET.Element:
|
@@ -155,15 +178,16 @@ async def create_dir_xml_element(drecord: DirectoryRecord) -> ET.Element:
|
|
155
178
|
href.text = f"/{drecord.url}"
|
156
179
|
propstat = ET.SubElement(dir_el, f"{{{DAV_NS}}}propstat")
|
157
180
|
prop = ET.SubElement(propstat, f"{{{DAV_NS}}}prop")
|
158
|
-
ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = drecord.url.split("/")[-2]
|
181
|
+
ET.SubElement(prop, f"{{{DAV_NS}}}displayname").text = decode_uri_compnents(drecord.url.split("/")[-2])
|
159
182
|
ET.SubElement(prop, f"{{{DAV_NS}}}resourcetype").append(ET.Element(f"{{{DAV_NS}}}collection"))
|
160
183
|
if drecord.size >= 0:
|
161
184
|
ET.SubElement(prop, f"{{{DAV_NS}}}getlastmodified").text = format_last_modified(drecord.create_time)
|
162
185
|
ET.SubElement(prop, f"{{{DAV_NS}}}getcontentlength").text = str(drecord.size)
|
163
|
-
|
164
|
-
lock_el = await query_lock_el(drecord.url, top_el_name=f"{{{DAV_NS}}}activelock")
|
186
|
+
lock_el = await query_lock_element(drecord.url, top_el_name=f"{{{DAV_NS}}}activelock")
|
165
187
|
if lock_el is not None:
|
188
|
+
lock_discovery = ET.SubElement(prop, f"{{{DAV_NS}}}lockdiscovery")
|
166
189
|
lock_discovery.append(lock_el)
|
190
|
+
ET.SubElement(propstat, f"{{{DAV_NS}}}status").text = "HTTP/1.1 200 OK"
|
167
191
|
return dir_el
|
168
192
|
|
169
193
|
async def xml_request_body(request: Request) -> Optional[ET.Element]:
|
@@ -179,64 +203,59 @@ async def dav_options(request: Request, path: str):
|
|
179
203
|
return Response(headers={
|
180
204
|
"DAV": "1,2",
|
181
205
|
"MS-Author-Via": "DAV",
|
182
|
-
"Allow": "OPTIONS, GET, HEAD, POST, DELETE, TRACE, PROPFIND, PROPPATCH, COPY, MOVE, LOCK, UNLOCK",
|
206
|
+
"Allow": "OPTIONS, GET, HEAD, POST, DELETE, TRACE, PROPFIND, PROPPATCH, COPY, MOVE, LOCK, UNLOCK, MKCOL",
|
183
207
|
"Content-Length": "0"
|
184
208
|
})
|
185
209
|
|
186
|
-
@router_dav.get("/{path:path}")
|
187
|
-
@handle_exception
|
188
|
-
async def dav_get(request: Request, path: str, user: UserRecord = Depends(registered_user)):
|
189
|
-
ptype, path, _ = await eval_path(path)
|
190
|
-
if ptype is None: raise PathNotFoundError(path)
|
191
|
-
elif ptype == "dir": raise InvalidOptionsError("Directory should not be fetched")
|
192
|
-
else: return await get_file_impl(request, user=user, path=path)
|
193
|
-
|
194
|
-
@router_dav.head("/{path:path}")
|
195
|
-
@handle_exception
|
196
|
-
async def dav_head(request: Request, path: str, user: UserRecord = Depends(registered_user)):
|
197
|
-
ptype, path, _ = await eval_path(path)
|
198
|
-
# some clients may send HEAD request to check if the file exists
|
199
|
-
if ptype is None: raise PathNotFoundError(path)
|
200
|
-
elif ptype == "dir": return Response(status_code=200)
|
201
|
-
else: return await get_file_impl(request, user=user, path=path, is_head=True)
|
202
|
-
|
203
|
-
@router_dav.put("/{path:path}")
|
204
|
-
@handle_exception
|
205
|
-
async def dav_put(request: Request, path: str, user: UserRecord = Depends(registered_user)):
|
206
|
-
_, path, _ = await eval_path(path)
|
207
|
-
return await put_file_impl(request, user=user, path=path, conflict='overwrite')
|
208
|
-
|
209
|
-
@router_dav.delete("/{path:path}")
|
210
|
-
@handle_exception
|
211
|
-
async def dav_delete(path: str, user: UserRecord = Depends(registered_user)):
|
212
|
-
_, path, _ = await eval_path(path)
|
213
|
-
return await delete_file_impl(user=user, path=path)
|
214
|
-
|
215
210
|
@router_dav.api_route("/{path:path}", methods=["PROPFIND"])
|
216
211
|
@handle_exception
|
217
|
-
async def dav_propfind(request: Request, path: str, user: UserRecord = Depends(registered_user)):
|
212
|
+
async def dav_propfind(request: Request, path: str, user: UserRecord = Depends(registered_user), body: Optional[ET.Element] = Depends(xml_request_body)):
|
218
213
|
if path.startswith("/"): path = path[1:]
|
219
214
|
path = ensure_uri_compnents(path)
|
220
215
|
|
221
|
-
|
216
|
+
if body and DEBUG_MODE:
|
217
|
+
print("Propfind-body:", ET.tostring(body, encoding="utf-8", method="xml"))
|
218
|
+
|
219
|
+
depth = request.headers.get("Depth", "0")
|
222
220
|
# Generate XML response
|
223
221
|
multistatus = ET.Element(f"{{{DAV_NS}}}multistatus")
|
224
222
|
path_type, lfss_path, record = await eval_path(path)
|
225
|
-
logger.info(f"PROPFIND {lfss_path} (depth: {depth})")
|
226
|
-
|
223
|
+
logger.info(f"PROPFIND {lfss_path} (depth: {depth}), type: {path_type}, record: {record}")
|
224
|
+
|
225
|
+
if lfss_path and await check_path_permission(lfss_path, user) < AccessLevel.READ:
|
226
|
+
raise PermissionDeniedError(lfss_path)
|
227
|
+
|
227
228
|
if path_type == "dir" and depth == "0":
|
228
229
|
# query the directory itself
|
229
|
-
return_status = 200
|
230
230
|
assert isinstance(record, DirectoryRecord)
|
231
231
|
dir_el = await create_dir_xml_element(record)
|
232
232
|
multistatus.append(dir_el)
|
233
233
|
|
234
|
+
elif path_type == "dir" and lfss_path == "":
|
235
|
+
# query root directory content
|
236
|
+
async def user_path_record(user_name: str, cur) -> DirectoryRecord:
|
237
|
+
try:
|
238
|
+
return await FileConn(cur).get_path_record(user_name + "/")
|
239
|
+
except PathNotFoundError:
|
240
|
+
return DirectoryRecord(user_name + "/", size=0, n_files=0, create_time="1970-01-01 00:00:00", update_time="1970-01-01 00:00:00", access_time="1970-01-01 00:00:00")
|
241
|
+
|
242
|
+
async with unique_cursor() as c:
|
243
|
+
uconn = UserConn(c)
|
244
|
+
if not user.is_admin:
|
245
|
+
for u in [user] + await uconn.list_peer_users(user.id, AccessLevel.READ):
|
246
|
+
dir_el = await create_dir_xml_element(await user_path_record(u.username, c))
|
247
|
+
multistatus.append(dir_el)
|
248
|
+
else:
|
249
|
+
async for u in uconn.all():
|
250
|
+
dir_el = await create_dir_xml_element(await user_path_record(u.username, c))
|
251
|
+
multistatus.append(dir_el)
|
252
|
+
|
234
253
|
elif path_type == "dir":
|
235
|
-
|
254
|
+
# query directory content
|
236
255
|
async with unique_cursor() as c:
|
237
256
|
flist = await FileConn(c).list_path_files(lfss_path, flat = True if depth == "infinity" else False)
|
238
257
|
for frecord in flist:
|
239
|
-
if frecord.url.
|
258
|
+
if frecord.url.endswith(f"/{MKDIR_PLACEHOLDER}"): continue
|
240
259
|
file_el = await create_file_xml_element(frecord)
|
241
260
|
multistatus.append(file_el)
|
242
261
|
|
@@ -247,6 +266,7 @@ async def dav_propfind(request: Request, path: str, user: UserRecord = Depends(r
|
|
247
266
|
multistatus.append(dir_el)
|
248
267
|
|
249
268
|
elif path_type == "file":
|
269
|
+
# query file
|
250
270
|
assert isinstance(record, FileRecord)
|
251
271
|
file_el = await create_file_xml_element(record)
|
252
272
|
multistatus.append(file_el)
|
@@ -255,7 +275,7 @@ async def dav_propfind(request: Request, path: str, user: UserRecord = Depends(r
|
|
255
275
|
raise PathNotFoundError(path)
|
256
276
|
|
257
277
|
xml_response = ET.tostring(multistatus, encoding="utf-8", method="xml")
|
258
|
-
return Response(content=xml_response, media_type="application/xml", status_code=
|
278
|
+
return Response(content=xml_response, media_type="application/xml", status_code=207)
|
259
279
|
|
260
280
|
@router_dav.api_route("/{path:path}", methods=["MKCOL"])
|
261
281
|
@handle_exception
|
@@ -282,7 +302,7 @@ async def dav_move(request: Request, path: str, user: UserRecord = Depends(regis
|
|
282
302
|
ptype, lfss_path, _ = await eval_path(path)
|
283
303
|
if ptype is None:
|
284
304
|
raise PathNotFoundError(path)
|
285
|
-
dptype, dlfss_path,
|
305
|
+
dptype, dlfss_path, _ = await eval_path(destination)
|
286
306
|
if dptype is not None:
|
287
307
|
raise HTTPException(status_code=409, detail="Conflict")
|
288
308
|
|
@@ -308,25 +328,17 @@ async def dav_copy(request: Request, path: str, user: UserRecord = Depends(regis
|
|
308
328
|
ptype, lfss_path, _ = await eval_path(path)
|
309
329
|
if ptype is None:
|
310
330
|
raise PathNotFoundError(path)
|
311
|
-
dptype, dlfss_path,
|
331
|
+
dptype, dlfss_path, _ = await eval_path(destination)
|
312
332
|
if dptype is not None:
|
313
333
|
raise HTTPException(status_code=409, detail="Conflict")
|
314
334
|
|
315
335
|
logger.info(f"COPY {path} -> {destination}")
|
316
|
-
|
317
|
-
assert not lfss_path.endswith("/"), "File path should not end with /"
|
318
|
-
assert not dlfss_path.endswith("/"), "File path should not end with /"
|
319
|
-
await db.copy_file(lfss_path, dlfss_path, user)
|
320
|
-
else:
|
321
|
-
assert ptype == "dir", "Directory path should end with /"
|
322
|
-
assert lfss_path.endswith("/"), "Directory path should end with /"
|
323
|
-
assert dlfss_path.endswith("/"), "Directory path should end with /"
|
324
|
-
await db.copy_path(lfss_path, dlfss_path, user)
|
325
|
-
return Response(status_code=201)
|
336
|
+
return await copy_impl(op_user=user, src_path=lfss_path, dst_path=dlfss_path)
|
326
337
|
|
327
338
|
@router_dav.api_route("/{path:path}", methods=["LOCK"])
|
328
339
|
@handle_exception
|
329
|
-
|
340
|
+
@static_vars(lock = asyncio.Lock())
|
341
|
+
async def dav_lock(request: Request, path: str, user: UserRecord = Depends(registered_user), body: ET.Element = Depends(xml_request_body)):
|
330
342
|
raw_timeout = request.headers.get("Timeout", "Second-3600")
|
331
343
|
if raw_timeout == "Infinite": timeout = -1
|
332
344
|
else:
|
@@ -334,16 +346,20 @@ async def dav_lock(request: Request, path: str, user: UserRecord = Depends(regis
|
|
334
346
|
raise HTTPException(status_code=400, detail="Bad Request, invalid timeout: " + raw_timeout + ", expected Second-<seconds> or Infinite")
|
335
347
|
_, timeout_str = raw_timeout.split("-")
|
336
348
|
timeout = int(timeout_str)
|
337
|
-
|
349
|
+
|
350
|
+
lock_depth = request.headers.get("Depth", "0")
|
338
351
|
_, path, _ = await eval_path(path)
|
339
352
|
# lock_token = f"opaquelocktoken:{uuid.uuid4().hex}"
|
340
353
|
lock_token = f"urn:uuid:{uuid.uuid4()}"
|
341
|
-
logger.info(f"LOCK {path} (timeout: {timeout}), token: {lock_token}")
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
354
|
+
logger.info(f"LOCK {path} (timeout: {timeout}), token: {lock_token}, depth: {lock_depth}")
|
355
|
+
if DEBUG_MODE and body:
|
356
|
+
print("Lock-body:", ET.tostring(body, encoding="utf-8", method="xml"))
|
357
|
+
async with dav_lock.lock:
|
358
|
+
await lock_path(user, path, lock_token, lock_depth, timeout=timeout)
|
359
|
+
response_elem = ET.Element(f"{{{DAV_NS}}}prop")
|
360
|
+
lockdiscovery = ET.SubElement(response_elem, f"{{{DAV_NS}}}lockdiscovery")
|
361
|
+
activelock = await query_lock_element(path, top_el_name=f"{{{DAV_NS}}}activelock")
|
362
|
+
assert activelock is not None
|
347
363
|
lockdiscovery.append(activelock)
|
348
364
|
lock_response = ET.tostring(response_elem, encoding="utf-8", method="xml")
|
349
365
|
return Response(content=lock_response, media_type="application/xml", status_code=201, headers={
|
@@ -352,13 +368,15 @@ async def dav_lock(request: Request, path: str, user: UserRecord = Depends(regis
|
|
352
368
|
|
353
369
|
@router_dav.api_route("/{path:path}", methods=["UNLOCK"])
|
354
370
|
@handle_exception
|
355
|
-
async def dav_unlock(request: Request, path: str, user: UserRecord = Depends(registered_user)):
|
371
|
+
async def dav_unlock(request: Request, path: str, user: UserRecord = Depends(registered_user), body: ET.Element = Depends(xml_request_body)):
|
356
372
|
lock_token = request.headers.get("Lock-Token")
|
357
373
|
if not lock_token:
|
358
374
|
raise HTTPException(status_code=400, detail="Lock-Token header is required")
|
359
375
|
if lock_token.startswith("<") and lock_token.endswith(">"):
|
360
376
|
lock_token = lock_token[1:-1]
|
361
377
|
logger.info(f"UNLOCK {path}, token: {lock_token}")
|
378
|
+
if DEBUG_MODE and body:
|
379
|
+
print("Unlock-body:", ET.tostring(body, encoding="utf-8", method="xml"))
|
362
380
|
_, path, _ = await eval_path(path)
|
363
381
|
await unlock_path(user, path, lock_token)
|
364
382
|
return Response(status_code=204)
|
lfss/svc/app_native.py
CHANGED
@@ -13,7 +13,7 @@ from ..eng.datatype import (
|
|
13
13
|
)
|
14
14
|
|
15
15
|
from .app_base import *
|
16
|
-
from .common_impl import
|
16
|
+
from .common_impl import get_impl, put_file_impl, post_file_impl, delete_impl, copy_impl
|
17
17
|
|
18
18
|
@router_fs.get("/{path:path}")
|
19
19
|
@handle_exception
|
@@ -23,7 +23,7 @@ async def get_file(
|
|
23
23
|
download: bool = False, thumb: bool = False,
|
24
24
|
user: UserRecord = Depends(get_current_user)
|
25
25
|
):
|
26
|
-
return await
|
26
|
+
return await get_impl(
|
27
27
|
request = request,
|
28
28
|
user = user, path = path, download = download, thumb = thumb
|
29
29
|
)
|
@@ -38,9 +38,7 @@ async def head_file(
|
|
38
38
|
):
|
39
39
|
if path.startswith("_api/"):
|
40
40
|
raise HTTPException(status_code=405, detail="HEAD not supported for API")
|
41
|
-
|
42
|
-
raise HTTPException(status_code=405, detail="HEAD not supported for directory")
|
43
|
-
return await get_file_impl(
|
41
|
+
return await get_impl(
|
44
42
|
request = request,
|
45
43
|
user = user, path = path, download = download, thumb = thumb, is_head = True
|
46
44
|
)
|
@@ -50,7 +48,7 @@ async def head_file(
|
|
50
48
|
async def put_file(
|
51
49
|
request: Request,
|
52
50
|
path: str,
|
53
|
-
conflict: Literal["overwrite", "skip", "abort"] = "
|
51
|
+
conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
|
54
52
|
permission: int = 0,
|
55
53
|
user: UserRecord = Depends(registered_user)
|
56
54
|
):
|
@@ -64,7 +62,7 @@ async def put_file(
|
|
64
62
|
async def post_file(
|
65
63
|
path: str,
|
66
64
|
file: UploadFile,
|
67
|
-
conflict: Literal["overwrite", "skip", "abort"] = "
|
65
|
+
conflict: Literal["overwrite", "skip", "abort"] = "overwrite",
|
68
66
|
permission: int = 0,
|
69
67
|
user: UserRecord = Depends(registered_user)
|
70
68
|
):
|
@@ -75,7 +73,7 @@ async def post_file(
|
|
75
73
|
@router_fs.delete("/{path:path}")
|
76
74
|
@handle_exception
|
77
75
|
async def delete_file(path: str, user: UserRecord = Depends(registered_user)):
|
78
|
-
return await
|
76
|
+
return await delete_impl(path, user)
|
79
77
|
|
80
78
|
|
81
79
|
@router_api.get("/bundle")
|
@@ -181,11 +179,19 @@ async def update_file_meta(
|
|
181
179
|
if new_path is not None:
|
182
180
|
new_path = ensure_uri_compnents(new_path)
|
183
181
|
logger.info(f"Update path of {path} to {new_path}")
|
184
|
-
#
|
182
|
+
# will raise duplicate path error if same name path exists in the new path
|
185
183
|
await db.move_path(path, new_path, user)
|
186
184
|
|
187
185
|
return Response(status_code=200, content="OK")
|
188
186
|
|
187
|
+
@router_api.post("/copy")
|
188
|
+
@handle_exception
|
189
|
+
async def copy_file(
|
190
|
+
src: str, dst: str,
|
191
|
+
user: UserRecord = Depends(registered_user)
|
192
|
+
):
|
193
|
+
return await copy_impl(src_path = src, dst_path = dst, op_user = user)
|
194
|
+
|
189
195
|
async def validate_path_read_permission(path: str, user: UserRecord):
|
190
196
|
if not path.endswith("/"):
|
191
197
|
raise HTTPException(status_code=400, detail="Path must end with /")
|