Update.
This commit is contained in:
parent
adc861d4b4
commit
d90b7ba852
@ -10,14 +10,17 @@ services:
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
restart: unless-stopped
|
||||
|
||||
network_mode: host
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
|
||||
app:
|
||||
network_mode: host
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
||||
@ -128,10 +128,23 @@ class FileRequest(models.Model):
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
expires_at = fields.DatetimeField(null=True)
|
||||
is_active = fields.BooleanField(default=True)
|
||||
# TODO: Add fields for custom form configuration
|
||||
|
||||
class Meta:
|
||||
table = "file_requests"
|
||||
|
||||
class WebDAVProperty(models.Model):
|
||||
id = fields.IntField(pk=True)
|
||||
resource_type = fields.CharField(max_length=10)
|
||||
resource_id = fields.IntField()
|
||||
namespace = fields.CharField(max_length=255)
|
||||
name = fields.CharField(max_length=255)
|
||||
value = fields.TextField()
|
||||
created_at = fields.DatetimeField(auto_now_add=True)
|
||||
updated_at = fields.DatetimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
table = "webdav_properties"
|
||||
unique_together = (("resource_type", "resource_id", "namespace", "name"),)
|
||||
|
||||
User_Pydantic = pydantic_model_creator(User, name="User_Pydantic")
|
||||
UserIn_Pydantic = pydantic_model_creator(User, name="UserIn_Pydantic", exclude_readonly=True)
|
||||
|
||||
204
rbox/webdav.py
204
rbox/webdav.py
@ -1,4 +1,5 @@
|
||||
from fastapi import APIRouter, Request, Response, Depends, HTTPException, status, Header
|
||||
from fastapi.responses import StreamingResponse
|
||||
from typing import Optional
|
||||
from xml.etree import ElementTree as ET
|
||||
from datetime import datetime
|
||||
@ -9,7 +10,7 @@ import base64
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from .auth import get_current_user, verify_password
|
||||
from .models import User, File, Folder
|
||||
from .models import User, File, Folder, WebDAVProperty
|
||||
from .storage import storage_manager
|
||||
from .activity import log_activity
|
||||
|
||||
@ -126,7 +127,14 @@ def build_href(base_path: str, name: str, is_collection: bool):
|
||||
path += '/'
|
||||
return path
|
||||
|
||||
def create_propstat_element(props: dict, status: str = "HTTP/1.1 200 OK"):
|
||||
async def get_custom_properties(resource_type: str, resource_id: int):
|
||||
props = await WebDAVProperty.filter(
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id
|
||||
)
|
||||
return {(prop.namespace, prop.name): prop.value for prop in props}
|
||||
|
||||
def create_propstat_element(props: dict, custom_props: dict = None, status: str = "HTTP/1.1 200 OK"):
|
||||
propstat = ET.Element("D:propstat")
|
||||
prop = ET.SubElement(propstat, "D:prop")
|
||||
|
||||
@ -154,11 +162,46 @@ def create_propstat_element(props: dict, status: str = "HTTP/1.1 200 OK"):
|
||||
elem = ET.SubElement(prop, f"D:{key}")
|
||||
elem.text = value
|
||||
|
||||
if custom_props:
|
||||
for (namespace, name), value in custom_props.items():
|
||||
if namespace == "DAV:":
|
||||
continue
|
||||
elem = ET.SubElement(prop, f"{{{namespace}}}{name}")
|
||||
elem.text = value
|
||||
|
||||
status_elem = ET.SubElement(propstat, "D:status")
|
||||
status_elem.text = status
|
||||
|
||||
return propstat
|
||||
|
||||
def parse_propfind_body(body: bytes):
|
||||
if not body:
|
||||
return None
|
||||
|
||||
try:
|
||||
root = ET.fromstring(body)
|
||||
|
||||
allprop = root.find(".//{DAV:}allprop")
|
||||
if allprop is not None:
|
||||
return "allprop"
|
||||
|
||||
propname = root.find(".//{DAV:}propname")
|
||||
if propname is not None:
|
||||
return "propname"
|
||||
|
||||
prop = root.find(".//{DAV:}prop")
|
||||
if prop is not None:
|
||||
requested_props = []
|
||||
for child in prop:
|
||||
ns = child.tag.split('}')[0][1:] if '}' in child.tag else "DAV:"
|
||||
name = child.tag.split('}')[1] if '}' in child.tag else child.tag
|
||||
requested_props.append((ns, name))
|
||||
return requested_props
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@router.api_route("/{full_path:path}", methods=["OPTIONS"])
|
||||
async def webdav_options(full_path: str):
|
||||
return Response(
|
||||
@ -174,6 +217,8 @@ async def webdav_options(full_path: str):
|
||||
async def handle_propfind(request: Request, full_path: str, current_user: User = Depends(webdav_auth)):
|
||||
depth = request.headers.get("Depth", "1")
|
||||
full_path = unquote(full_path).strip('/')
|
||||
body = await request.body()
|
||||
requested_props = parse_propfind_body(body)
|
||||
|
||||
resource, parent, exists = await resolve_path(full_path, current_user)
|
||||
|
||||
@ -212,7 +257,8 @@ async def handle_propfind(request: Request, full_path: str, current_user: User =
|
||||
"creationdate": folder.created_at,
|
||||
"getlastmodified": folder.updated_at
|
||||
}
|
||||
response.append(create_propstat_element(props))
|
||||
custom_props = await get_custom_properties("folder", folder.id) if requested_props == "allprop" or (isinstance(requested_props, list) and any(ns != "DAV:" for ns, _ in requested_props)) else None
|
||||
response.append(create_propstat_element(props, custom_props))
|
||||
|
||||
for file in files:
|
||||
response = ET.SubElement(multistatus, "D:response")
|
||||
@ -228,7 +274,8 @@ async def handle_propfind(request: Request, full_path: str, current_user: User =
|
||||
"getlastmodified": file.updated_at,
|
||||
"getetag": f'"{file.file_hash}"'
|
||||
}
|
||||
response.append(create_propstat_element(props))
|
||||
custom_props = await get_custom_properties("file", file.id) if requested_props == "allprop" or (isinstance(requested_props, list) and any(ns != "DAV:" for ns, _ in requested_props)) else None
|
||||
response.append(create_propstat_element(props, custom_props))
|
||||
|
||||
elif isinstance(resource, Folder):
|
||||
response = ET.SubElement(multistatus, "D:response")
|
||||
@ -241,7 +288,8 @@ async def handle_propfind(request: Request, full_path: str, current_user: User =
|
||||
"creationdate": resource.created_at,
|
||||
"getlastmodified": resource.updated_at
|
||||
}
|
||||
response.append(create_propstat_element(props))
|
||||
custom_props = await get_custom_properties("folder", resource.id) if requested_props == "allprop" or (isinstance(requested_props, list) and any(ns != "DAV:" for ns, _ in requested_props)) else None
|
||||
response.append(create_propstat_element(props, custom_props))
|
||||
|
||||
if depth in ["1", "infinity"]:
|
||||
folders = await Folder.filter(owner=current_user, parent=resource, is_deleted=False)
|
||||
@ -258,7 +306,8 @@ async def handle_propfind(request: Request, full_path: str, current_user: User =
|
||||
"creationdate": folder.created_at,
|
||||
"getlastmodified": folder.updated_at
|
||||
}
|
||||
response.append(create_propstat_element(props))
|
||||
custom_props = await get_custom_properties("folder", folder.id) if requested_props == "allprop" or (isinstance(requested_props, list) and any(ns != "DAV:" for ns, _ in requested_props)) else None
|
||||
response.append(create_propstat_element(props, custom_props))
|
||||
|
||||
for file in files:
|
||||
response = ET.SubElement(multistatus, "D:response")
|
||||
@ -274,7 +323,8 @@ async def handle_propfind(request: Request, full_path: str, current_user: User =
|
||||
"getlastmodified": file.updated_at,
|
||||
"getetag": f'"{file.file_hash}"'
|
||||
}
|
||||
response.append(create_propstat_element(props))
|
||||
custom_props = await get_custom_properties("file", file.id) if requested_props == "allprop" or (isinstance(requested_props, list) and any(ns != "DAV:" for ns, _ in requested_props)) else None
|
||||
response.append(create_propstat_element(props, custom_props))
|
||||
|
||||
elif isinstance(resource, File):
|
||||
response = ET.SubElement(multistatus, "D:response")
|
||||
@ -290,7 +340,8 @@ async def handle_propfind(request: Request, full_path: str, current_user: User =
|
||||
"getlastmodified": resource.updated_at,
|
||||
"getetag": f'"{resource.file_hash}"'
|
||||
}
|
||||
response.append(create_propstat_element(props))
|
||||
custom_props = await get_custom_properties("file", resource.id) if requested_props == "allprop" or (isinstance(requested_props, list) and any(ns != "DAV:" for ns, _ in requested_props)) else None
|
||||
response.append(create_propstat_element(props, custom_props))
|
||||
|
||||
xml_content = ET.tostring(multistatus, encoding="utf-8", xml_declaration=True)
|
||||
return Response(content=xml_content, media_type="application/xml; charset=utf-8", status_code=207)
|
||||
@ -320,7 +371,7 @@ async def handle_get(request: Request, full_path: str, current_user: User = Depe
|
||||
async for chunk in storage_manager.get_file(current_user.id, resource.path):
|
||||
yield chunk
|
||||
|
||||
return Response(
|
||||
return StreamingResponse(
|
||||
content=file_iterator(),
|
||||
media_type=resource.mime_type,
|
||||
headers={
|
||||
@ -652,3 +703,138 @@ async def handle_unlock(request: Request, full_path: str, current_user: User = D
|
||||
|
||||
WebDAVLock.remove_lock(full_path)
|
||||
return Response(status_code=204)
|
||||
|
||||
@router.api_route("/{full_path:path}", methods=["PROPPATCH"])
|
||||
async def handle_proppatch(request: Request, full_path: str, current_user: User = Depends(webdav_auth)):
|
||||
full_path = unquote(full_path).strip('/')
|
||||
|
||||
resource, parent, exists = await resolve_path(full_path, current_user)
|
||||
|
||||
if not resource:
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
|
||||
body = await request.body()
|
||||
if not body:
|
||||
raise HTTPException(status_code=400, detail="Request body required")
|
||||
|
||||
try:
|
||||
root = ET.fromstring(body)
|
||||
except:
|
||||
raise HTTPException(status_code=400, detail="Invalid XML")
|
||||
|
||||
resource_type = "file" if isinstance(resource, File) else "folder"
|
||||
resource_id = resource.id
|
||||
|
||||
set_props = []
|
||||
remove_props = []
|
||||
failed_props = []
|
||||
|
||||
set_element = root.find(".//{DAV:}set")
|
||||
if set_element is not None:
|
||||
prop_element = set_element.find(".//{DAV:}prop")
|
||||
if prop_element is not None:
|
||||
for child in prop_element:
|
||||
ns = child.tag.split('}')[0][1:] if '}' in child.tag else "DAV:"
|
||||
name = child.tag.split('}')[1] if '}' in child.tag else child.tag
|
||||
value = child.text or ""
|
||||
|
||||
if ns == "DAV:":
|
||||
live_props = ["creationdate", "getcontentlength", "getcontenttype",
|
||||
"getetag", "getlastmodified", "resourcetype"]
|
||||
if name in live_props:
|
||||
failed_props.append((ns, name, "409 Conflict"))
|
||||
continue
|
||||
|
||||
try:
|
||||
existing_prop = await WebDAVProperty.get_or_none(
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
namespace=ns,
|
||||
name=name
|
||||
)
|
||||
|
||||
if existing_prop:
|
||||
existing_prop.value = value
|
||||
await existing_prop.save()
|
||||
else:
|
||||
await WebDAVProperty.create(
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
namespace=ns,
|
||||
name=name,
|
||||
value=value
|
||||
)
|
||||
set_props.append((ns, name))
|
||||
except Exception as e:
|
||||
failed_props.append((ns, name, "500 Internal Server Error"))
|
||||
|
||||
remove_element = root.find(".//{DAV:}remove")
|
||||
if remove_element is not None:
|
||||
prop_element = remove_element.find(".//{DAV:}prop")
|
||||
if prop_element is not None:
|
||||
for child in prop_element:
|
||||
ns = child.tag.split('}')[0][1:] if '}' in child.tag else "DAV:"
|
||||
name = child.tag.split('}')[1] if '}' in child.tag else child.tag
|
||||
|
||||
if ns == "DAV:":
|
||||
failed_props.append((ns, name, "409 Conflict"))
|
||||
continue
|
||||
|
||||
try:
|
||||
existing_prop = await WebDAVProperty.get_or_none(
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
namespace=ns,
|
||||
name=name
|
||||
)
|
||||
|
||||
if existing_prop:
|
||||
await existing_prop.delete()
|
||||
remove_props.append((ns, name))
|
||||
else:
|
||||
failed_props.append((ns, name, "404 Not Found"))
|
||||
except Exception as e:
|
||||
failed_props.append((ns, name, "500 Internal Server Error"))
|
||||
|
||||
multistatus = ET.Element("D:multistatus", {"xmlns:D": "DAV:"})
|
||||
response_elem = ET.SubElement(multistatus, "D:response")
|
||||
href = ET.SubElement(response_elem, "D:href")
|
||||
href.text = f"/webdav/{full_path}"
|
||||
|
||||
if set_props or remove_props:
|
||||
propstat = ET.SubElement(response_elem, "D:propstat")
|
||||
prop = ET.SubElement(propstat, "D:prop")
|
||||
|
||||
for ns, name in set_props + remove_props:
|
||||
if ns == "DAV:":
|
||||
ET.SubElement(prop, f"D:{name}")
|
||||
else:
|
||||
ET.SubElement(prop, f"{{{ns}}}{name}")
|
||||
|
||||
status_elem = ET.SubElement(propstat, "D:status")
|
||||
status_elem.text = "HTTP/1.1 200 OK"
|
||||
|
||||
if failed_props:
|
||||
prop_by_status = {}
|
||||
for ns, name, status_text in failed_props:
|
||||
if status_text not in prop_by_status:
|
||||
prop_by_status[status_text] = []
|
||||
prop_by_status[status_text].append((ns, name))
|
||||
|
||||
for status_text, props_list in prop_by_status.items():
|
||||
propstat = ET.SubElement(response_elem, "D:propstat")
|
||||
prop = ET.SubElement(propstat, "D:prop")
|
||||
|
||||
for ns, name in props_list:
|
||||
if ns == "DAV:":
|
||||
ET.SubElement(prop, f"D:{name}")
|
||||
else:
|
||||
ET.SubElement(prop, f"{{{ns}}}{name}")
|
||||
|
||||
status_elem = ET.SubElement(propstat, "D:status")
|
||||
status_elem.text = f"HTTP/1.1 {status_text}"
|
||||
|
||||
await log_activity(current_user, "properties_modified", resource_type, resource_id)
|
||||
|
||||
xml_content = ET.tostring(multistatus, encoding="utf-8", xml_declaration=True)
|
||||
return Response(content=xml_content, media_type="application/xml; charset=utf-8", status_code=207)
|
||||
|
||||
@ -9,7 +9,7 @@ if [ ! -f .env ]; then
|
||||
fi
|
||||
|
||||
echo "Starting database services..."
|
||||
docker-compose up -d db redis
|
||||
docker compose up -d db redis
|
||||
|
||||
echo "Waiting for database to be ready..."
|
||||
sleep 5
|
||||
|
||||
Loading…
Reference in New Issue
Block a user