Add NextSnap PWA with photo gallery viewer and continuous capture
Offline-first photo capture app for Nextcloud with: - Camera capture with continuous mode (auto-reopens after each photo) - File browser with fullscreen image gallery, swipe navigation, and rename - Upload queue with background sync engine - Admin panel for Nextcloud user management - Service worker for offline-first caching (v13) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
186
app/routes/webdav_proxy.py
Normal file
186
app/routes/webdav_proxy.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from flask import Blueprint, request, jsonify, send_file
|
||||
from app.services.auth import require_auth, get_current_user
|
||||
from app.services.nextcloud import NextcloudClient
|
||||
from config import Config
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
bp = Blueprint('webdav', __name__, url_prefix='/api/files')
|
||||
|
||||
def _get_nc_client():
|
||||
"""Get Nextcloud client for current user."""
|
||||
user = get_current_user()
|
||||
if not user:
|
||||
return None
|
||||
return NextcloudClient(Config.NEXTCLOUD_URL, user['username'], user['password'])
|
||||
|
||||
@bp.route('/list', methods=['GET'])
|
||||
@require_auth
|
||||
def list_files():
|
||||
"""List files and directories at the given path."""
|
||||
path = request.args.get('path', '/')
|
||||
|
||||
nc_client = _get_nc_client()
|
||||
if not nc_client:
|
||||
return jsonify({'error': 'Not authenticated'}), 401
|
||||
|
||||
try:
|
||||
result = nc_client.propfind(path, depth=1)
|
||||
return jsonify(result), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Failed to list files: {str(e)}'}), 500
|
||||
|
||||
@bp.route('/upload', methods=['PUT', 'POST'])
|
||||
@require_auth
|
||||
def upload_file():
|
||||
"""Upload a file to Nextcloud."""
|
||||
nc_client = _get_nc_client()
|
||||
if not nc_client:
|
||||
return jsonify({'error': 'Not authenticated'}), 401
|
||||
|
||||
# Get target path from query parameter or form data
|
||||
target_path = request.args.get('path') or request.form.get('path')
|
||||
if not target_path:
|
||||
return jsonify({'error': 'Target path is required'}), 400
|
||||
|
||||
# Get file data
|
||||
if 'file' in request.files:
|
||||
# Multipart upload
|
||||
file = request.files['file']
|
||||
file_data = file.read()
|
||||
filename = file.filename
|
||||
else:
|
||||
# Raw binary upload
|
||||
file_data = request.get_data()
|
||||
filename = request.args.get('filename', 'upload.jpg')
|
||||
|
||||
if not file_data:
|
||||
return jsonify({'error': 'No file data provided'}), 400
|
||||
|
||||
# Construct full path
|
||||
full_path = f"{target_path.rstrip('/')}/{filename}"
|
||||
|
||||
try:
|
||||
result = nc_client.put_file(full_path, file_data)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'path': full_path,
|
||||
'url': result.get('url', '')
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Upload failed: {str(e)}'}), 500
|
||||
|
||||
@bp.route('/verify', methods=['HEAD', 'GET'])
|
||||
@require_auth
|
||||
def verify_file():
|
||||
"""Verify that a file exists on Nextcloud."""
|
||||
path = request.args.get('path')
|
||||
if not path:
|
||||
return jsonify({'error': 'Path is required'}), 400
|
||||
|
||||
nc_client = _get_nc_client()
|
||||
if not nc_client:
|
||||
return jsonify({'error': 'Not authenticated'}), 401
|
||||
|
||||
try:
|
||||
exists = nc_client.head(path)
|
||||
if exists:
|
||||
return jsonify({'exists': True}), 200
|
||||
else:
|
||||
return jsonify({'exists': False}), 404
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Verification failed: {str(e)}'}), 500
|
||||
|
||||
@bp.route('/mkdir', methods=['POST'])
|
||||
@require_auth
|
||||
def make_directory():
|
||||
"""Create a new directory on Nextcloud."""
|
||||
data = request.get_json()
|
||||
if not data or 'path' not in data:
|
||||
return jsonify({'error': 'Path is required'}), 400
|
||||
|
||||
path = data['path']
|
||||
|
||||
nc_client = _get_nc_client()
|
||||
if not nc_client:
|
||||
return jsonify({'error': 'Not authenticated'}), 401
|
||||
|
||||
try:
|
||||
nc_client.mkcol(path)
|
||||
return jsonify({'success': True, 'path': path}), 201
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Failed to create directory: {str(e)}'}), 500
|
||||
|
||||
@bp.route('/rename', methods=['POST'])
|
||||
@require_auth
|
||||
def rename_file():
|
||||
"""Rename or move a file on Nextcloud."""
|
||||
data = request.get_json()
|
||||
if not data or 'source' not in data or 'destination' not in data:
|
||||
return jsonify({'error': 'Source and destination paths are required'}), 400
|
||||
|
||||
source = data['source']
|
||||
destination = data['destination']
|
||||
|
||||
nc_client = _get_nc_client()
|
||||
if not nc_client:
|
||||
return jsonify({'error': 'Not authenticated'}), 401
|
||||
|
||||
try:
|
||||
nc_client.move(source, destination)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'source': source,
|
||||
'destination': destination
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Failed to rename file: {str(e)}'}), 500
|
||||
|
||||
@bp.route('/thumbnail', methods=['GET'])
|
||||
@require_auth
|
||||
def get_thumbnail():
|
||||
"""Generate and return a thumbnail of an image file."""
|
||||
path = request.args.get('path')
|
||||
size = request.args.get('size', '256')
|
||||
|
||||
if not path:
|
||||
return jsonify({'error': 'Path is required'}), 400
|
||||
|
||||
try:
|
||||
size = int(size)
|
||||
if size < 32 or size > 1024:
|
||||
size = 256
|
||||
except ValueError:
|
||||
size = 256
|
||||
|
||||
nc_client = _get_nc_client()
|
||||
if not nc_client:
|
||||
return jsonify({'error': 'Not authenticated'}), 401
|
||||
|
||||
try:
|
||||
# Download the file from Nextcloud
|
||||
file_data = nc_client.get_file(path)
|
||||
|
||||
# Open image and create thumbnail
|
||||
image = Image.open(BytesIO(file_data))
|
||||
|
||||
# Convert to RGB if necessary (for PNG with transparency, etc.)
|
||||
if image.mode not in ('RGB', 'L'):
|
||||
image = image.convert('RGB')
|
||||
|
||||
# Create thumbnail maintaining aspect ratio
|
||||
image.thumbnail((size, size), Image.Resampling.LANCZOS)
|
||||
|
||||
# Save to BytesIO
|
||||
output = BytesIO()
|
||||
image.save(output, format='JPEG', quality=85)
|
||||
output.seek(0)
|
||||
|
||||
return send_file(
|
||||
output,
|
||||
mimetype='image/jpeg',
|
||||
as_attachment=False,
|
||||
download_name=f'thumbnail_{size}.jpg'
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Failed to generate thumbnail: {str(e)}'}), 500
|
||||
Reference in New Issue
Block a user