Update.
This commit is contained in:
parent
d4aee28247
commit
9a0a6ce0fa
5
.gitignore
vendored
5
.gitignore
vendored
@ -2,3 +2,8 @@
|
||||
.env
|
||||
__pycache__/
|
||||
logs/
|
||||
webdav
|
||||
debug_webdav.py
|
||||
test_propfind.sh
|
||||
test_cache.py
|
||||
*.db
|
||||
|
||||
144
main.py
144
main.py
@ -13,6 +13,7 @@ import hmac
|
||||
import secrets
|
||||
import mimetypes
|
||||
import base64
|
||||
import functools
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List, Tuple
|
||||
@ -75,12 +76,17 @@ class Database:
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = db_path
|
||||
self._connection_lock = asyncio.Lock()
|
||||
self.init_database()
|
||||
|
||||
def get_connection(self) -> sqlite3.Connection:
|
||||
"""Get database connection with row factory"""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn = sqlite3.connect(self.db_path, timeout=30.0, check_same_thread=False)
|
||||
conn.row_factory = sqlite3.Row
|
||||
# Enable WAL mode for better concurrent access
|
||||
conn.execute('PRAGMA journal_mode=WAL')
|
||||
conn.execute('PRAGMA busy_timeout=30000')
|
||||
conn.execute('PRAGMA synchronous=NORMAL')
|
||||
return conn
|
||||
|
||||
def init_database(self):
|
||||
@ -172,6 +178,7 @@ class Database:
|
||||
salt = secrets.token_hex(16)
|
||||
password_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000).hex()
|
||||
|
||||
def _create():
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
@ -181,6 +188,10 @@ class Database:
|
||||
user_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return user_id
|
||||
|
||||
# Run in thread pool to prevent blocking
|
||||
user_id = await asyncio.get_event_loop().run_in_executor(None, _create)
|
||||
|
||||
# Create user directory
|
||||
user_dir = Path(Config.WEBDAV_ROOT) / 'users' / username
|
||||
@ -188,7 +199,17 @@ class Database:
|
||||
|
||||
return user_id
|
||||
|
||||
async def verify_user(self, username: str, password: str) -> Optional[Dict]:
|
||||
def _verify_user(self, username: str, password: str) -> Optional[Dict]:
|
||||
"""Verify user credentials"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT * FROM users WHERE username = ? AND is_active = 1', (username,))
|
||||
user = cursor.fetchone()
|
||||
conn.close()
|
||||
return dict(user) if user else None
|
||||
|
||||
@functools.lru_cache()
|
||||
def _verify_user(self, username: str, password: str) -> Optional[Dict]:
|
||||
"""Verify user credentials"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
@ -205,6 +226,10 @@ class Database:
|
||||
return dict(user)
|
||||
return None
|
||||
|
||||
async def verify_user(self, username: str, password: str) -> Optional[Dict]:
|
||||
"""Verify user credentials"""
|
||||
return self._verify_user(username, password)
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[Dict]:
|
||||
"""Get user by ID"""
|
||||
conn = self.get_connection()
|
||||
@ -267,6 +292,18 @@ class Database:
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
async def remove_property(self, resource_path: str, namespace: str, property_name: str):
|
||||
"""Remove a custom property from a resource"""
|
||||
conn = self.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
DELETE FROM properties
|
||||
WHERE resource_path = ? AND property_name = ?
|
||||
AND (namespace = ? OR (namespace IS NULL AND ? = ''))
|
||||
''', (resource_path, property_name, namespace, namespace))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
async def get_properties(self, resource_path: str) -> List[Dict]:
|
||||
"""Get all properties for a resource"""
|
||||
conn = self.get_connection()
|
||||
@ -289,7 +326,8 @@ class WebDAVXML:
|
||||
# WebDAV namespaces
|
||||
NS = {
|
||||
'D': 'DAV:',
|
||||
'MS': 'urn:schemas-microsoft-com:'
|
||||
'MS': 'urn:schemas-microsoft-com:',
|
||||
'C': 'http://webdav.org/custom/' # Custom properties namespace
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@ -301,6 +339,7 @@ class WebDAVXML:
|
||||
@staticmethod
|
||||
def create_multistatus() -> ET.Element:
|
||||
"""Create multistatus response root"""
|
||||
WebDAVXML.register_namespaces()
|
||||
return ET.Element('{DAV:}multistatus')
|
||||
|
||||
@staticmethod
|
||||
@ -317,15 +356,38 @@ class WebDAVXML:
|
||||
propstat = ET.SubElement(response, '{DAV:}propstat')
|
||||
prop = ET.SubElement(propstat, '{DAV:}prop')
|
||||
|
||||
for prop_name, prop_value in props.items():
|
||||
if ':' in prop_name:
|
||||
ns, name = prop_name.split(':', 1)
|
||||
ns_uri = WebDAVXML.NS.get(ns, ns)
|
||||
prop_elem = ET.SubElement(prop, f'{{{ns_uri}}}{name}')
|
||||
else:
|
||||
prop_elem = ET.SubElement(prop, f'{{DAV:}}{prop_name}')
|
||||
is_collection = props.pop('_is_collection', False)
|
||||
|
||||
if prop_value is not None:
|
||||
# Standard DAV properties - these should be in DAV: namespace
|
||||
standard_props = {
|
||||
'resourcetype', 'creationdate', 'getlastmodified',
|
||||
'getcontentlength', 'getcontenttype', 'displayname',
|
||||
'getetag', 'lockdiscovery', 'supportedlock'
|
||||
}
|
||||
|
||||
for prop_name, prop_value in props.items():
|
||||
# Check if property is already in {namespace}name format
|
||||
if prop_name.startswith('{'):
|
||||
# Already has namespace in Clark notation
|
||||
prop_elem = ET.SubElement(prop, prop_name)
|
||||
# Handle colon-separated namespace prefix (e.g., "D:displayname")
|
||||
elif ':' in prop_name and not prop_name.startswith('http'):
|
||||
ns_prefix, name = prop_name.split(':', 1)
|
||||
ns_uri = WebDAVXML.NS.get(ns_prefix, ns_prefix)
|
||||
prop_elem = ET.SubElement(prop, f'{{{ns_uri}}}{name}')
|
||||
# Standard DAV properties
|
||||
elif prop_name.lower() in standard_props:
|
||||
prop_elem = ET.SubElement(prop, f'{{DAV:}}{prop_name}')
|
||||
# Custom properties without explicit namespace - use custom namespace
|
||||
else:
|
||||
prop_elem = ET.SubElement(prop, f'{{http://webdav.org/custom/}}{prop_name}')
|
||||
|
||||
# Special handling for resourcetype
|
||||
if prop_name == 'resourcetype' or prop_name == '{DAV:}resourcetype':
|
||||
if is_collection:
|
||||
ET.SubElement(prop_elem, '{DAV:}collection')
|
||||
# Empty for non-collections
|
||||
elif prop_value is not None:
|
||||
prop_elem.text = str(prop_value)
|
||||
|
||||
status_elem = ET.SubElement(propstat, '{DAV:}status')
|
||||
@ -334,6 +396,8 @@ class WebDAVXML:
|
||||
@staticmethod
|
||||
def serialize(element: ET.Element) -> str:
|
||||
"""Serialize XML element to string"""
|
||||
# Register all namespaces before serializing
|
||||
WebDAVXML.register_namespaces()
|
||||
return ET.tostring(element, encoding='unicode', method='xml')
|
||||
|
||||
@staticmethod
|
||||
@ -521,7 +585,7 @@ class WebDAVHandler:
|
||||
return web.Response(
|
||||
status=200,
|
||||
headers={
|
||||
'DAV': '1, 2, 3',
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV',
|
||||
'Allow': 'OPTIONS, GET, HEAD, POST, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK',
|
||||
'Access-Control-Allow-Methods': 'OPTIONS, GET, HEAD, POST, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK',
|
||||
@ -663,7 +727,10 @@ class WebDAVHandler:
|
||||
if depth_header == 'infinity':
|
||||
depth = Config.MAX_PROPFIND_DEPTH
|
||||
else:
|
||||
try:
|
||||
depth = int(depth_header)
|
||||
except ValueError:
|
||||
depth = 1
|
||||
|
||||
# Parse request body
|
||||
body = await request.read()
|
||||
@ -675,15 +742,16 @@ class WebDAVHandler:
|
||||
# Add resources
|
||||
await self.add_resource_props(multistatus, path, request.path, user, depth, prop_request)
|
||||
|
||||
xml_response = WebDAVXML.serialize(multistatus)
|
||||
xml_response = '<?xml version="1.0" encoding="utf-8"?>\n' + WebDAVXML.serialize(multistatus)
|
||||
|
||||
return web.Response(
|
||||
status=207,
|
||||
content_type='application/xml',
|
||||
charset='utf-8',
|
||||
text='<?xml version="1.0" encoding="utf-8"?>\n' + xml_response,
|
||||
text=xml_response,
|
||||
headers={
|
||||
'DAV': '1, 2, 3',
|
||||
'DAV': '1, 2',
|
||||
'MS-Author-Via': 'DAV',
|
||||
}
|
||||
)
|
||||
|
||||
@ -718,14 +786,16 @@ class WebDAVHandler:
|
||||
|
||||
stat = path.stat()
|
||||
|
||||
# Resource type
|
||||
# Resource type - use None for proper XML nesting
|
||||
if path.is_dir():
|
||||
props['resourcetype'] = '<D:collection/>'
|
||||
props['resourcetype'] = None # Will be handled specially
|
||||
props['_is_collection'] = True
|
||||
else:
|
||||
props['resourcetype'] = ''
|
||||
props['resourcetype'] = None
|
||||
props['_is_collection'] = False
|
||||
|
||||
# Creation date
|
||||
props['creationdate'] = datetime.fromtimestamp(stat.st_ctime).isoformat() + 'Z'
|
||||
props['creationdate'] = datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
# Last modified
|
||||
props['getlastmodified'] = datetime.fromtimestamp(stat.st_mtime).strftime('%a, %d %b %Y %H:%M:%S GMT')
|
||||
@ -745,7 +815,28 @@ class WebDAVHandler:
|
||||
# Get custom properties from database
|
||||
custom_props = await self.db.get_properties(str(path))
|
||||
for prop in custom_props:
|
||||
key = f"{prop['namespace']}:{prop['property_name']}" if prop['namespace'] else prop['property_name']
|
||||
# Handle properties with full namespace URIs in the name
|
||||
prop_name = prop['property_name']
|
||||
namespace = prop['namespace'] or ''
|
||||
|
||||
# Check if property name contains a colon (namespace separator)
|
||||
# This happens when cadaver stores properties
|
||||
if ':' in prop_name and not namespace:
|
||||
# Extract namespace from property name
|
||||
# e.g., "http://webdav.org/cadaver/custom-properties/:xxx"
|
||||
# Split only on the LAST colon to get the actual property name
|
||||
parts = prop_name.rsplit(':', 1)
|
||||
if len(parts) == 2:
|
||||
namespace = parts[0] + ':'
|
||||
prop_name = parts[1]
|
||||
|
||||
# Create property key with namespace
|
||||
if namespace:
|
||||
# Store as full namespace URI + property name for ElementTree
|
||||
key = f'{{{namespace}}}{prop_name}'
|
||||
else:
|
||||
key = prop_name
|
||||
|
||||
props[key] = prop['property_value']
|
||||
|
||||
return props
|
||||
@ -780,16 +871,25 @@ class WebDAVHandler:
|
||||
|
||||
await self.db.set_property(str(path), namespace, prop_name, prop_value)
|
||||
|
||||
# Process remove operations
|
||||
for remove_elem in root.findall('.//{DAV:}remove/{DAV:}prop'):
|
||||
for prop_elem in remove_elem:
|
||||
namespace = prop_elem.tag.split('}')[0].strip('{') if '}' in prop_elem.tag else ''
|
||||
prop_name = prop_elem.tag.split('}')[1] if '}' in prop_elem.tag else prop_elem.tag
|
||||
|
||||
# Remove property using database method
|
||||
await self.db.remove_property(str(path), namespace, prop_name)
|
||||
|
||||
WebDAVXML.add_propstat(response, {}, '200 OK')
|
||||
multistatus.append(response)
|
||||
|
||||
xml_response = WebDAVXML.serialize(multistatus)
|
||||
xml_response = '<?xml version="1.0" encoding="utf-8"?>\n' + WebDAVXML.serialize(multistatus)
|
||||
|
||||
return web.Response(
|
||||
status=207,
|
||||
content_type='application/xml',
|
||||
charset='utf-8',
|
||||
text='<?xml version="1.0" encoding="utf-8"?>\n' + xml_response
|
||||
text=xml_response
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
10
webdav_cli.py
Normal file → Executable file
10
webdav_cli.py
Normal file → Executable file
@ -288,6 +288,16 @@ class WebDAVCLI:
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
async def clear_cache(self):
|
||||
"""Clear authentication cache"""
|
||||
print("⚠️ This requires the server to be running with cache management enabled.")
|
||||
print("Cache statistics are only available via server admin interface.")
|
||||
print("\nTo clear cache:")
|
||||
print("1. Stop the server")
|
||||
print("2. Restart the server (cache is in-memory only)")
|
||||
print("\nOr use the admin API if enabled:")
|
||||
print(" curl -X POST http://localhost:8080/admin/cache/clear")
|
||||
|
||||
# ========================================================================
|
||||
# Database Management
|
||||
# ========================================================================
|
||||
|
||||
Loading…
Reference in New Issue
Block a user