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.
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 get_file_impl, put_file_impl, delete_file_impl
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
- async with unique_cursor() as c:
56
- fconn = FileConn(c)
57
- if await fconn.count_path_files(path, flat=True) == 0:
58
- return None, lfss_path, None
59
- return "dir", lfss_path, await fconn.get_path_record(path)
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(path + "/") > 0:
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 = 600):
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 query_lock_el(p: str, top_el_name: str = f"{{{DAV_NS}}}lockinfo") -> Optional[ET.Element]:
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
- user_, token, timeout, lock_time = row
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
- lock_info = ET.Element(top_el_name)
122
- locktype = ET.SubElement(lock_info, f"{{{DAV_NS}}}locktype")
123
- ET.SubElement(locktype, f"{{{DAV_NS}}}write")
124
- lockscope = ET.SubElement(lock_info, f"{{{DAV_NS}}}lockscope")
125
- ET.SubElement(lockscope, f"{{{DAV_NS}}}exclusive")
126
- owner = ET.SubElement(lock_info, f"{{{DAV_NS}}}owner")
127
- owner.text = user_
128
- timeout = ET.SubElement(lock_info, f"{{{DAV_NS}}}timeout")
129
- timeout.text = f"Second-{timeout}"
130
- locktoken = ET.SubElement(lock_info, f"{{{DAV_NS}}}locktoken")
131
- href = ET.SubElement(locktoken, f"{{{DAV_NS}}}href")
132
- href.text = f"{token}"
133
- return lock_info
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
- lock_discovery = ET.SubElement(prop, f"{{{DAV_NS}}}lockdiscovery")
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
- lock_discovery = ET.SubElement(prop, f"{{{DAV_NS}}}lockdiscovery")
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
- depth = request.headers.get("Depth", "1")
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
- return_status = 200
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
- return_status = 207
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.split("/")[-1] == MKDIR_PLACEHOLDER: continue
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=return_status)
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, ddav_path = await eval_path(destination)
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, ddav_path = await eval_path(destination)
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
- if ptype == "file":
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
- async def dav_lock(request: Request, path: str, user: UserRecord = Depends(registered_user)):
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
- await lock_path(user, path, lock_token, timeout=timeout)
343
- response_elem = ET.Element(f"{{{DAV_NS}}}prop")
344
- lockdiscovery = ET.SubElement(response_elem, f"{{{DAV_NS}}}lockdiscovery")
345
- activelock = await query_lock_el(path, top_el_name=f"{{{DAV_NS}}}activelock")
346
- assert activelock is not None, "Lock info should not be None"
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 get_file_impl, put_file_impl, post_file_impl, delete_file_impl
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 get_file_impl(
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
- if path.endswith("/"):
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"] = "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"] = "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 delete_file_impl(path, user)
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
- # currently only move own file, with overwrite
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 /")