feat(cli): add private NanoShare sync client
Build and Push Docker Container / build-and-push (push) Successful in 1m3s
Build and Push Docker Container / build-and-push (push) Successful in 1m3s
- Add a separate installable NanoShare CLI package under cli/. - Implement browser login with local callback and refresh-token config. - Add upload, list, download, sync, and watch CLI commands. - Add private owner-only file download endpoint for CLI downloads. - Add CLI auth endpoints for browser login and token refresh. - Return file_id from ServiceLink uploads for reliable sync state. - Exclude the CLI package from NanoShare container builds. - Include tests for private downloads and sync state updates.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from my_modules.app.setup import LIMITER
|
||||
from my_modules.decoratory.header import token_required
|
||||
from quart_common.web.wide_event import add_wide_event_context
|
||||
|
||||
from quart import Blueprint, abort, current_app, send_file
|
||||
|
||||
api_download_bp = Blueprint('api_download', __name__)
|
||||
|
||||
def _last_modified_from_file(file_data: dict):
|
||||
uploaded_at = file_data.get('uploaded_at')
|
||||
if uploaded_at is None:
|
||||
return None
|
||||
try:
|
||||
return int(uploaded_at) / 1000
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@api_download_bp.get('/api/files/<path:file_id>/download')
|
||||
@LIMITER.limit('60 per minute;1000 per hour;')
|
||||
@token_required(['files', 'mesh'])
|
||||
async def api_file_download(file_id: str, user: dict):
|
||||
add_wide_event_context(nanoshare={'operation': 'api_file_download', 'file_id': file_id})
|
||||
|
||||
file_data = await current_app.convex.get_file_informations(file_id=file_id, user_id=user['sub'])
|
||||
if not file_data:
|
||||
add_wide_event_context(nanoshare={'operation_status': 'not_found'})
|
||||
abort(404)
|
||||
|
||||
if file_data.get('expired'):
|
||||
add_wide_event_context(nanoshare={'operation_status': 'expired'})
|
||||
abort(410)
|
||||
|
||||
storage_id = file_data.get('db_image_url')
|
||||
if not storage_id:
|
||||
add_wide_event_context(nanoshare={'operation_status': 'missing_storage'})
|
||||
abort(404)
|
||||
|
||||
content_type = file_data.get('content_type') or 'application/octet-stream'
|
||||
file_name = file_data.get('file_name') or file_id
|
||||
add_wide_event_context(nanoshare={'operation_status': 'served', 'content_type': content_type})
|
||||
|
||||
response = await send_file(
|
||||
filename_or_io=await current_app.convex.get_from_storage(storage_id),
|
||||
mimetype=content_type,
|
||||
as_attachment=True,
|
||||
attachment_filename=file_name,
|
||||
conditional=True,
|
||||
cache_timeout=0,
|
||||
last_modified=_last_modified_from_file(file_data),
|
||||
)
|
||||
response.headers['Cache-Control'] = 'private, no-store'
|
||||
return response
|
||||
Reference in New Issue
Block a user