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>
187 lines
5.7 KiB
Python
187 lines
5.7 KiB
Python
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
|