From 033b8a82be65f68f5c3b759a95b7905b9e68b795 Mon Sep 17 00:00:00 2001 From: kqjy Date: Thu, 5 Feb 2026 20:44:11 +0800 Subject: [PATCH] Fix error handlers for API mode; distinguish files from directories in object lookup; Fix UI not showing newly uploaded objects by adding Cache-Control headers --- app/__init__.py | 30 ++++++++++++++++++++++++++++-- app/s3_api.py | 28 +++++++++++++++++++++------- app/storage.py | 2 +- app/ui.py | 4 +++- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index ef13ad4..636dc7c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -263,11 +263,37 @@ def create_app( @app.errorhandler(500) def internal_error(error): - return render_template('500.html'), 500 + wants_html = request.accept_mimetypes.accept_html + path = request.path or "" + if include_ui and wants_html and (path.startswith("/ui") or path == "/"): + return render_template('500.html'), 500 + error_xml = ( + '' + '' + 'InternalError' + 'An internal server error occurred' + f'{path}' + f'{getattr(g, "request_id", "-")}' + '' + ) + return error_xml, 500, {'Content-Type': 'application/xml'} @app.errorhandler(CSRFError) def handle_csrf_error(e): - return render_template('csrf_error.html', reason=e.description), 400 + wants_html = request.accept_mimetypes.accept_html + path = request.path or "" + if include_ui and wants_html and (path.startswith("/ui") or path == "/"): + return render_template('csrf_error.html', reason=e.description), 400 + error_xml = ( + '' + '' + 'CSRFError' + f'{e.description}' + f'{path}' + f'{getattr(g, "request_id", "-")}' + '' + ) + return error_xml, 400, {'Content-Type': 'application/xml'} @app.template_filter("filesizeformat") def filesizeformat(value: int) -> str: diff --git a/app/s3_api.py b/app/s3_api.py index f2cfa0b..12a72d1 100644 --- a/app/s3_api.py +++ b/app/s3_api.py @@ -2776,9 +2776,14 @@ def object_handler(bucket_name: str, object_key: str): except StorageError as exc: return _error_response("InternalError", str(exc), 500) else: - stat = path.stat() - file_size = stat.st_size - etag = storage._compute_etag(path) + try: + stat = path.stat() + file_size = stat.st_size + etag = storage._compute_etag(path) + except PermissionError: + return _error_response("AccessDenied", "Permission denied accessing object", 403) + except OSError as exc: + return _error_response("InternalError", f"Failed to access object: {exc}", 500) if range_header: try: @@ -2819,13 +2824,22 @@ def object_handler(bucket_name: str, object_key: str): except StorageError as exc: return _error_response("InternalError", str(exc), 500) else: - stat = path.stat() - response = Response(status=200) - etag = storage._compute_etag(path) + try: + stat = path.stat() + response = Response(status=200) + etag = storage._compute_etag(path) + except PermissionError: + return _error_response("AccessDenied", "Permission denied accessing object", 403) + except OSError as exc: + return _error_response("InternalError", f"Failed to access object: {exc}", 500) response.headers["Content-Type"] = mimetype logged_bytes = 0 - _apply_object_headers(response, file_stat=path.stat() if not is_encrypted else None, metadata=metadata, etag=etag) + try: + file_stat = path.stat() if not is_encrypted else None + except (PermissionError, OSError): + file_stat = None + _apply_object_headers(response, file_stat=file_stat, metadata=metadata, etag=etag) if request.method == "GET": response_overrides = { diff --git a/app/storage.py b/app/storage.py index 22391be..f102949 100644 --- a/app/storage.py +++ b/app/storage.py @@ -522,7 +522,7 @@ class ObjectStorage: def get_object_path(self, bucket_name: str, object_key: str) -> Path: path = self._object_path(bucket_name, object_key) - if not path.exists(): + if not path.is_file(): raise ObjectNotFoundError("Object not found") return path diff --git a/app/ui.py b/app/ui.py index 9df2131..8287259 100644 --- a/app/ui.py +++ b/app/ui.py @@ -594,7 +594,7 @@ def list_bucket_objects(bucket_name: str): "etag": obj.etag, }) - return jsonify({ + response = jsonify({ "objects": objects_data, "is_truncated": result.is_truncated, "next_continuation_token": result.next_continuation_token, @@ -613,6 +613,8 @@ def list_bucket_objects(bucket_name: str): "metadata": metadata_template, }, }) + response.headers["Cache-Control"] = "no-store" + return response @ui_bp.get("/buckets//objects/stream")