diff --git a/api/controllers/service_api/__init__.py b/api/controllers/service_api/__init__.py index d964e2781..b26f29d98 100644 --- a/api/controllers/service_api/__init__.py +++ b/api/controllers/service_api/__init__.py @@ -6,6 +6,6 @@ bp = Blueprint("service_api", __name__, url_prefix="/v1") api = ExternalApi(bp) from . import index -from .app import annotation, app, audio, completion, conversation, file, message, site, workflow +from .app import annotation, app, audio, completion, conversation, file, file_preview, message, site, workflow from .dataset import dataset, document, hit_testing, metadata, segment, upload_file from .workspace import models diff --git a/api/controllers/service_api/app/error.py b/api/controllers/service_api/app/error.py index ca91da80c..ba705f71e 100644 --- a/api/controllers/service_api/app/error.py +++ b/api/controllers/service_api/app/error.py @@ -107,3 +107,15 @@ class UnsupportedFileTypeError(BaseHTTPException): error_code = "unsupported_file_type" description = "File type not allowed." code = 415 + + +class FileNotFoundError(BaseHTTPException): + error_code = "file_not_found" + description = "The requested file was not found." + code = 404 + + +class FileAccessDeniedError(BaseHTTPException): + error_code = "file_access_denied" + description = "Access to the requested file is denied." + code = 403 diff --git a/api/controllers/service_api/app/file_preview.py b/api/controllers/service_api/app/file_preview.py new file mode 100644 index 000000000..57141033d --- /dev/null +++ b/api/controllers/service_api/app/file_preview.py @@ -0,0 +1,186 @@ +import logging +from urllib.parse import quote + +from flask import Response +from flask_restful import Resource, reqparse + +from controllers.service_api import api +from controllers.service_api.app.error import ( + FileAccessDeniedError, + FileNotFoundError, +) +from controllers.service_api.wraps import FetchUserArg, WhereisUserArg, validate_app_token +from extensions.ext_database import db +from extensions.ext_storage import storage +from models.model import App, EndUser, Message, MessageFile, UploadFile + +logger = logging.getLogger(__name__) + + +class FilePreviewApi(Resource): + """ + Service API File Preview endpoint + + Provides secure file preview/download functionality for external API users. + Files can only be accessed if they belong to messages within the requesting app's context. + """ + + @validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.QUERY)) + def get(self, app_model: App, end_user: EndUser, file_id: str): + """ + Preview/Download a file that was uploaded via Service API + + Args: + app_model: The authenticated app model + end_user: The authenticated end user (optional) + file_id: UUID of the file to preview + + Query Parameters: + user: Optional user identifier + as_attachment: Boolean, whether to download as attachment (default: false) + + Returns: + Stream response with file content + + Raises: + FileNotFoundError: File does not exist + FileAccessDeniedError: File access denied (not owned by app) + """ + file_id = str(file_id) + + # Parse query parameters + parser = reqparse.RequestParser() + parser.add_argument("as_attachment", type=bool, required=False, default=False, location="args") + args = parser.parse_args() + + # Validate file ownership and get file objects + message_file, upload_file = self._validate_file_ownership(file_id, app_model.id) + + # Get file content generator + try: + generator = storage.load(upload_file.key, stream=True) + except Exception as e: + raise FileNotFoundError(f"Failed to load file content: {str(e)}") + + # Build response with appropriate headers + response = self._build_file_response(generator, upload_file, args["as_attachment"]) + + return response + + def _validate_file_ownership(self, file_id: str, app_id: str) -> tuple[MessageFile, UploadFile]: + """ + Validate that the file belongs to a message within the requesting app's context + + Security validations performed: + 1. File exists in MessageFile table (was used in a conversation) + 2. Message belongs to the requesting app + 3. UploadFile record exists and is accessible + 4. File tenant matches app tenant (additional security layer) + + Args: + file_id: UUID of the file to validate + app_id: UUID of the requesting app + + Returns: + Tuple of (MessageFile, UploadFile) if validation passes + + Raises: + FileNotFoundError: File or related records not found + FileAccessDeniedError: File does not belong to the app's context + """ + try: + # Input validation + if not file_id or not app_id: + raise FileAccessDeniedError("Invalid file or app identifier") + + # First, find the MessageFile that references this upload file + message_file = db.session.query(MessageFile).where(MessageFile.upload_file_id == file_id).first() + + if not message_file: + raise FileNotFoundError("File not found in message context") + + # Get the message and verify it belongs to the requesting app + message = ( + db.session.query(Message).where(Message.id == message_file.message_id, Message.app_id == app_id).first() + ) + + if not message: + raise FileAccessDeniedError("File access denied: not owned by requesting app") + + # Get the actual upload file record + upload_file = db.session.query(UploadFile).where(UploadFile.id == file_id).first() + + if not upload_file: + raise FileNotFoundError("Upload file record not found") + + # Additional security: verify tenant isolation + app = db.session.query(App).where(App.id == app_id).first() + if app and upload_file.tenant_id != app.tenant_id: + raise FileAccessDeniedError("File access denied: tenant mismatch") + + return message_file, upload_file + + except (FileNotFoundError, FileAccessDeniedError): + # Re-raise our custom exceptions + raise + except Exception as e: + # Log unexpected errors for debugging + logger.exception( + "Unexpected error during file ownership validation", + extra={"file_id": file_id, "app_id": app_id, "error": str(e)}, + ) + raise FileAccessDeniedError("File access validation failed") + + def _build_file_response(self, generator, upload_file: UploadFile, as_attachment: bool = False) -> Response: + """ + Build Flask Response object with appropriate headers for file streaming + + Args: + generator: File content generator from storage + upload_file: UploadFile database record + as_attachment: Whether to set Content-Disposition as attachment + + Returns: + Flask Response object with streaming file content + """ + response = Response( + generator, + mimetype=upload_file.mime_type, + direct_passthrough=True, + headers={}, + ) + + # Add Content-Length if known + if upload_file.size and upload_file.size > 0: + response.headers["Content-Length"] = str(upload_file.size) + + # Add Accept-Ranges header for audio/video files to support seeking + if upload_file.mime_type in [ + "audio/mpeg", + "audio/wav", + "audio/mp4", + "audio/ogg", + "audio/flac", + "audio/aac", + "video/mp4", + "video/webm", + "video/quicktime", + "audio/x-m4a", + ]: + response.headers["Accept-Ranges"] = "bytes" + + # Set Content-Disposition for downloads + if as_attachment and upload_file.name: + encoded_filename = quote(upload_file.name) + response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}" + # Override content-type for downloads to force download + response.headers["Content-Type"] = "application/octet-stream" + + # Add caching headers for performance + response.headers["Cache-Control"] = "public, max-age=3600" # Cache for 1 hour + + return response + + +# Register the API endpoint +api.add_resource(FilePreviewApi, "/files//preview") diff --git a/api/tests/unit_tests/controllers/service_api/app/test_file_preview.py b/api/tests/unit_tests/controllers/service_api/app/test_file_preview.py new file mode 100644 index 000000000..5c484403a --- /dev/null +++ b/api/tests/unit_tests/controllers/service_api/app/test_file_preview.py @@ -0,0 +1,336 @@ +""" +Unit tests for Service API File Preview endpoint +""" + +import uuid +from unittest.mock import Mock, patch + +import pytest + +from controllers.service_api.app.error import FileAccessDeniedError, FileNotFoundError +from controllers.service_api.app.file_preview import FilePreviewApi +from models.model import App, EndUser, Message, MessageFile, UploadFile + + +class TestFilePreviewApi: + """Test suite for FilePreviewApi""" + + @pytest.fixture + def file_preview_api(self): + """Create FilePreviewApi instance for testing""" + return FilePreviewApi() + + @pytest.fixture + def mock_app(self): + """Mock App model""" + app = Mock(spec=App) + app.id = str(uuid.uuid4()) + app.tenant_id = str(uuid.uuid4()) + return app + + @pytest.fixture + def mock_end_user(self): + """Mock EndUser model""" + end_user = Mock(spec=EndUser) + end_user.id = str(uuid.uuid4()) + return end_user + + @pytest.fixture + def mock_upload_file(self): + """Mock UploadFile model""" + upload_file = Mock(spec=UploadFile) + upload_file.id = str(uuid.uuid4()) + upload_file.name = "test_file.jpg" + upload_file.mime_type = "image/jpeg" + upload_file.size = 1024 + upload_file.key = "storage/key/test_file.jpg" + upload_file.tenant_id = str(uuid.uuid4()) + return upload_file + + @pytest.fixture + def mock_message_file(self): + """Mock MessageFile model""" + message_file = Mock(spec=MessageFile) + message_file.id = str(uuid.uuid4()) + message_file.upload_file_id = str(uuid.uuid4()) + message_file.message_id = str(uuid.uuid4()) + return message_file + + @pytest.fixture + def mock_message(self): + """Mock Message model""" + message = Mock(spec=Message) + message.id = str(uuid.uuid4()) + message.app_id = str(uuid.uuid4()) + return message + + def test_validate_file_ownership_success( + self, file_preview_api, mock_app, mock_upload_file, mock_message_file, mock_message + ): + """Test successful file ownership validation""" + file_id = str(uuid.uuid4()) + app_id = mock_app.id + + # Set up the mocks + mock_upload_file.tenant_id = mock_app.tenant_id + mock_message.app_id = app_id + mock_message_file.upload_file_id = file_id + mock_message_file.message_id = mock_message.id + + with patch("controllers.service_api.app.file_preview.db") as mock_db: + # Mock database queries + mock_db.session.query.return_value.where.return_value.first.side_effect = [ + mock_message_file, # MessageFile query + mock_message, # Message query + mock_upload_file, # UploadFile query + mock_app, # App query for tenant validation + ] + + # Execute the method + result_message_file, result_upload_file = file_preview_api._validate_file_ownership(file_id, app_id) + + # Assertions + assert result_message_file == mock_message_file + assert result_upload_file == mock_upload_file + + def test_validate_file_ownership_file_not_found(self, file_preview_api): + """Test file ownership validation when MessageFile not found""" + file_id = str(uuid.uuid4()) + app_id = str(uuid.uuid4()) + + with patch("controllers.service_api.app.file_preview.db") as mock_db: + # Mock MessageFile not found + mock_db.session.query.return_value.where.return_value.first.return_value = None + + # Execute and assert exception + with pytest.raises(FileNotFoundError) as exc_info: + file_preview_api._validate_file_ownership(file_id, app_id) + + assert "File not found in message context" in str(exc_info.value) + + def test_validate_file_ownership_access_denied(self, file_preview_api, mock_message_file): + """Test file ownership validation when Message not owned by app""" + file_id = str(uuid.uuid4()) + app_id = str(uuid.uuid4()) + + with patch("controllers.service_api.app.file_preview.db") as mock_db: + # Mock MessageFile found but Message not owned by app + mock_db.session.query.return_value.where.return_value.first.side_effect = [ + mock_message_file, # MessageFile query - found + None, # Message query - not found (access denied) + ] + + # Execute and assert exception + with pytest.raises(FileAccessDeniedError) as exc_info: + file_preview_api._validate_file_ownership(file_id, app_id) + + assert "not owned by requesting app" in str(exc_info.value) + + def test_validate_file_ownership_upload_file_not_found(self, file_preview_api, mock_message_file, mock_message): + """Test file ownership validation when UploadFile not found""" + file_id = str(uuid.uuid4()) + app_id = str(uuid.uuid4()) + + with patch("controllers.service_api.app.file_preview.db") as mock_db: + # Mock MessageFile and Message found but UploadFile not found + mock_db.session.query.return_value.where.return_value.first.side_effect = [ + mock_message_file, # MessageFile query - found + mock_message, # Message query - found + None, # UploadFile query - not found + ] + + # Execute and assert exception + with pytest.raises(FileNotFoundError) as exc_info: + file_preview_api._validate_file_ownership(file_id, app_id) + + assert "Upload file record not found" in str(exc_info.value) + + def test_validate_file_ownership_tenant_mismatch( + self, file_preview_api, mock_app, mock_upload_file, mock_message_file, mock_message + ): + """Test file ownership validation with tenant mismatch""" + file_id = str(uuid.uuid4()) + app_id = mock_app.id + + # Set up tenant mismatch + mock_upload_file.tenant_id = "different_tenant_id" + mock_app.tenant_id = "app_tenant_id" + mock_message.app_id = app_id + mock_message_file.upload_file_id = file_id + mock_message_file.message_id = mock_message.id + + with patch("controllers.service_api.app.file_preview.db") as mock_db: + # Mock database queries + mock_db.session.query.return_value.where.return_value.first.side_effect = [ + mock_message_file, # MessageFile query + mock_message, # Message query + mock_upload_file, # UploadFile query + mock_app, # App query for tenant validation + ] + + # Execute and assert exception + with pytest.raises(FileAccessDeniedError) as exc_info: + file_preview_api._validate_file_ownership(file_id, app_id) + + assert "tenant mismatch" in str(exc_info.value) + + def test_validate_file_ownership_invalid_input(self, file_preview_api): + """Test file ownership validation with invalid input""" + + # Test with empty file_id + with pytest.raises(FileAccessDeniedError) as exc_info: + file_preview_api._validate_file_ownership("", "app_id") + assert "Invalid file or app identifier" in str(exc_info.value) + + # Test with empty app_id + with pytest.raises(FileAccessDeniedError) as exc_info: + file_preview_api._validate_file_ownership("file_id", "") + assert "Invalid file or app identifier" in str(exc_info.value) + + def test_build_file_response_basic(self, file_preview_api, mock_upload_file): + """Test basic file response building""" + mock_generator = Mock() + + response = file_preview_api._build_file_response(mock_generator, mock_upload_file, False) + + # Check response properties + assert response.mimetype == mock_upload_file.mime_type + assert response.direct_passthrough is True + assert response.headers["Content-Length"] == str(mock_upload_file.size) + assert "Cache-Control" in response.headers + + def test_build_file_response_as_attachment(self, file_preview_api, mock_upload_file): + """Test file response building with attachment flag""" + mock_generator = Mock() + + response = file_preview_api._build_file_response(mock_generator, mock_upload_file, True) + + # Check attachment-specific headers + assert "attachment" in response.headers["Content-Disposition"] + assert mock_upload_file.name in response.headers["Content-Disposition"] + assert response.headers["Content-Type"] == "application/octet-stream" + + def test_build_file_response_audio_video(self, file_preview_api, mock_upload_file): + """Test file response building for audio/video files""" + mock_generator = Mock() + mock_upload_file.mime_type = "video/mp4" + + response = file_preview_api._build_file_response(mock_generator, mock_upload_file, False) + + # Check Range support for media files + assert response.headers["Accept-Ranges"] == "bytes" + + def test_build_file_response_no_size(self, file_preview_api, mock_upload_file): + """Test file response building when size is unknown""" + mock_generator = Mock() + mock_upload_file.size = 0 # Unknown size + + response = file_preview_api._build_file_response(mock_generator, mock_upload_file, False) + + # Content-Length should not be set when size is unknown + assert "Content-Length" not in response.headers + + @patch("controllers.service_api.app.file_preview.storage") + def test_get_method_integration( + self, mock_storage, file_preview_api, mock_app, mock_end_user, mock_upload_file, mock_message_file, mock_message + ): + """Test the full GET method integration (without decorator)""" + file_id = str(uuid.uuid4()) + app_id = mock_app.id + + # Set up mocks + mock_upload_file.tenant_id = mock_app.tenant_id + mock_message.app_id = app_id + mock_message_file.upload_file_id = file_id + mock_message_file.message_id = mock_message.id + + mock_generator = Mock() + mock_storage.load.return_value = mock_generator + + with patch("controllers.service_api.app.file_preview.db") as mock_db: + # Mock database queries + mock_db.session.query.return_value.where.return_value.first.side_effect = [ + mock_message_file, # MessageFile query + mock_message, # Message query + mock_upload_file, # UploadFile query + mock_app, # App query for tenant validation + ] + + with patch("controllers.service_api.app.file_preview.reqparse") as mock_reqparse: + # Mock request parsing + mock_parser = Mock() + mock_parser.parse_args.return_value = {"as_attachment": False} + mock_reqparse.RequestParser.return_value = mock_parser + + # Test the core logic directly without Flask decorators + # Validate file ownership + result_message_file, result_upload_file = file_preview_api._validate_file_ownership(file_id, app_id) + assert result_message_file == mock_message_file + assert result_upload_file == mock_upload_file + + # Test file response building + response = file_preview_api._build_file_response(mock_generator, mock_upload_file, False) + assert response is not None + + # Verify storage was called correctly + mock_storage.load.assert_not_called() # Since we're testing components separately + + @patch("controllers.service_api.app.file_preview.storage") + def test_storage_error_handling( + self, mock_storage, file_preview_api, mock_app, mock_upload_file, mock_message_file, mock_message + ): + """Test storage error handling in the core logic""" + file_id = str(uuid.uuid4()) + app_id = mock_app.id + + # Set up mocks + mock_upload_file.tenant_id = mock_app.tenant_id + mock_message.app_id = app_id + mock_message_file.upload_file_id = file_id + mock_message_file.message_id = mock_message.id + + # Mock storage error + mock_storage.load.side_effect = Exception("Storage error") + + with patch("controllers.service_api.app.file_preview.db") as mock_db: + # Mock database queries for validation + mock_db.session.query.return_value.where.return_value.first.side_effect = [ + mock_message_file, # MessageFile query + mock_message, # Message query + mock_upload_file, # UploadFile query + mock_app, # App query for tenant validation + ] + + # First validate file ownership works + result_message_file, result_upload_file = file_preview_api._validate_file_ownership(file_id, app_id) + assert result_message_file == mock_message_file + assert result_upload_file == mock_upload_file + + # Test storage error handling + with pytest.raises(Exception) as exc_info: + mock_storage.load(mock_upload_file.key, stream=True) + + assert "Storage error" in str(exc_info.value) + + @patch("controllers.service_api.app.file_preview.logger") + def test_validate_file_ownership_unexpected_error_logging(self, mock_logger, file_preview_api): + """Test that unexpected errors are logged properly""" + file_id = str(uuid.uuid4()) + app_id = str(uuid.uuid4()) + + with patch("controllers.service_api.app.file_preview.db") as mock_db: + # Mock database query to raise unexpected exception + mock_db.session.query.side_effect = Exception("Unexpected database error") + + # Execute and assert exception + with pytest.raises(FileAccessDeniedError) as exc_info: + file_preview_api._validate_file_ownership(file_id, app_id) + + # Verify error message + assert "File access validation failed" in str(exc_info.value) + + # Verify logging was called + mock_logger.exception.assert_called_once_with( + "Unexpected error during file ownership validation", + extra={"file_id": file_id, "app_id": app_id, "error": "Unexpected database error"}, + ) diff --git a/web/app/components/develop/template/template.en.mdx b/web/app/components/develop/template/template.en.mdx index 3fdb78262..bee9261ef 100755 --- a/web/app/components/develop/template/template.en.mdx +++ b/web/app/components/develop/template/template.en.mdx @@ -277,6 +277,85 @@ The text generation application offers non-session support and is ideal for tran --- + + + + Preview or download uploaded files. This endpoint allows you to access files that have been previously uploaded via the File Upload API. + + Files can only be accessed if they belong to messages within the requesting application. + + ### Path Parameters + - `file_id` (string) Required + The unique identifier of the file to preview, obtained from the File Upload API response. + + ### Query Parameters + - `as_attachment` (boolean) Optional + Whether to force download the file as an attachment. Default is `false` (preview in browser). + + ### Response + Returns the file content with appropriate headers for browser display or download. + - `Content-Type` Set based on file mime type + - `Content-Length` File size in bytes (if available) + - `Content-Disposition` Set to "attachment" if `as_attachment=true` + - `Cache-Control` Caching headers for performance + - `Accept-Ranges` Set to "bytes" for audio/video files + + ### Errors + - 400, `invalid_param`, abnormal parameter input + - 403, `file_access_denied`, file access denied or file does not belong to current application + - 404, `file_not_found`, file not found or has been deleted + - 500, internal server error + + + + ### Request Example + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### Download as Attachment + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### Response Headers Example + + ```http {{ title: 'Headers - Image Preview' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### Download Response Headers + + ```http {{ title: 'Headers - File Download' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + +--- + --- + + + + アップロードされたファイルをプレビューまたはダウンロードします。このエンドポイントを使用すると、以前にファイルアップロード API でアップロードされたファイルにアクセスできます。 + + ファイルは、リクエストしているアプリケーションのメッセージ範囲内にある場合のみアクセス可能です。 + + ### パスパラメータ + - `file_id` (string) 必須 + プレビューするファイルの一意識別子。ファイルアップロード API レスポンスから取得します。 + + ### クエリパラメータ + - `as_attachment` (boolean) オプション + ファイルを添付ファイルとして強制ダウンロードするかどうか。デフォルトは `false`(ブラウザでプレビュー)。 + + ### レスポンス + ブラウザ表示またはダウンロード用の適切なヘッダー付きでファイル内容を返します。 + - `Content-Type` ファイル MIME タイプに基づいて設定 + - `Content-Length` ファイルサイズ(バイト、利用可能な場合) + - `Content-Disposition` `as_attachment=true` の場合は "attachment" に設定 + - `Cache-Control` パフォーマンス向上のためのキャッシュヘッダー + - `Accept-Ranges` 音声/動画ファイルの場合は "bytes" に設定 + + ### エラー + - 400, `invalid_param`, パラメータ入力異常 + - 403, `file_access_denied`, ファイルアクセス拒否またはファイルが現在のアプリケーションに属していません + - 404, `file_not_found`, ファイルが見つからないか削除されています + - 500, サーバー内部エラー + + + + ### リクエスト例 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### 添付ファイルとしてダウンロード + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### レスポンスヘッダー例 + + ```http {{ title: 'ヘッダー - 画像プレビュー' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### ファイルダウンロードレスポンスヘッダー + + ```http {{ title: 'ヘッダー - ファイルダウンロード' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + +--- + --- + + + + + 预览或下载已上传的文件。此端点允许您访问先前通过文件上传 API 上传的文件。 + + 文件只能在属于请求应用程序的消息范围内访问。 + + ### 路径参数 + - `file_id` (string) 必需 + 要预览的文件的唯一标识符,从文件上传 API 响应中获得。 + + ### 查询参数 + - `as_attachment` (boolean) 可选 + 是否强制将文件作为附件下载。默认为 `false`(在浏览器中预览)。 + + ### 响应 + 返回带有适当浏览器显示或下载标头的文件内容。 + - `Content-Type` 根据文件 MIME 类型设置 + - `Content-Length` 文件大小(以字节为单位,如果可用) + - `Content-Disposition` 如果 `as_attachment=true` 则设置为 "attachment" + - `Cache-Control` 用于性能的缓存标头 + - `Accept-Ranges` 对于音频/视频文件设置为 "bytes" + + ### 错误 + - 400, `invalid_param`, 参数输入异常 + - 403, `file_access_denied`, 文件访问被拒绝或文件不属于当前应用程序 + - 404, `file_not_found`, 文件未找到或已被删除 + - 500, 服务内部错误 + + + + ### 请求示例 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### 作为附件下载 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### 响应标头示例 + + ```http {{ title: 'Headers - 图片预览' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### 文件下载响应标头 + + ```http {{ title: 'Headers - 文件下载' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + +--- + --- + + + + Preview or download uploaded files. This endpoint allows you to access files that have been previously uploaded via the File Upload API. + + Files can only be accessed if they belong to messages within the requesting application. + + ### Path Parameters + - `file_id` (string) Required + The unique identifier of the file to preview, obtained from the File Upload API response. + + ### Query Parameters + - `as_attachment` (boolean) Optional + Whether to force download the file as an attachment. Default is `false` (preview in browser). + + ### Response + Returns the file content with appropriate headers for browser display or download. + - `Content-Type` Set based on file mime type + - `Content-Length` File size in bytes (if available) + - `Content-Disposition` Set to "attachment" if `as_attachment=true` + - `Cache-Control` Caching headers for performance + - `Accept-Ranges` Set to "bytes" for audio/video files + + ### Errors + - 400, `invalid_param`, abnormal parameter input + - 403, `file_access_denied`, file access denied or file does not belong to current application + - 404, `file_not_found`, file not found or has been deleted + - 500, internal server error + + + + ### Request Example + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### Download as Attachment + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### Response Headers Example + + ```http {{ title: 'Headers - Image Preview' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### Download Response Headers + + ```http {{ title: 'Headers - File Download' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + +--- + --- + + + + アップロードされたファイルをプレビューまたはダウンロードします。このエンドポイントを使用すると、以前にファイルアップロード API でアップロードされたファイルにアクセスできます。 + + ファイルは、リクエストしているアプリケーションのメッセージ範囲内にある場合のみアクセス可能です。 + + ### パスパラメータ + - `file_id` (string) 必須 + プレビューするファイルの一意識別子。ファイルアップロード API レスポンスから取得します。 + + ### クエリパラメータ + - `as_attachment` (boolean) オプション + ファイルを添付ファイルとして強制ダウンロードするかどうか。デフォルトは `false`(ブラウザでプレビュー)。 + + ### レスポンス + ブラウザ表示またはダウンロード用の適切なヘッダー付きでファイル内容を返します。 + - `Content-Type` ファイル MIME タイプに基づいて設定 + - `Content-Length` ファイルサイズ(バイト、利用可能な場合) + - `Content-Disposition` `as_attachment=true` の場合は "attachment" に設定 + - `Cache-Control` パフォーマンス向上のためのキャッシュヘッダー + - `Accept-Ranges` 音声/動画ファイルの場合は "bytes" に設定 + + ### エラー + - 400, `invalid_param`, パラメータ入力異常 + - 403, `file_access_denied`, ファイルアクセス拒否またはファイルが現在のアプリケーションに属していません + - 404, `file_not_found`, ファイルが見つからないか削除されています + - 500, サーバー内部エラー + + + + ### リクエスト例 + + + ```bash {{ title: 'cURL - ブラウザプレビュー' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### 添付ファイルとしてダウンロード + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### レスポンスヘッダー例 + + ```http {{ title: 'ヘッダー - 画像プレビュー' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### ダウンロードレスポンスヘッダー + + ```http {{ title: 'ヘッダー - ファイルダウンロード' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + + +--- + --- + + + + + 预览或下载已上传的文件。此端点允许您访问先前通过文件上传 API 上传的文件。 + + 文件只能在属于请求应用程序的消息范围内访问。 + + ### 路径参数 + - `file_id` (string) 必需 + 要预览的文件的唯一标识符,从文件上传 API 响应中获得。 + + ### 查询参数 + - `as_attachment` (boolean) 可选 + 是否强制将文件作为附件下载。默认为 `false`(在浏览器中预览)。 + + ### 响应 + 返回带有适当浏览器显示或下载标头的文件内容。 + - `Content-Type` 根据文件 MIME 类型设置 + - `Content-Length` 文件大小(以字节为单位,如果可用) + - `Content-Disposition` 如果 `as_attachment=true` 则设置为 "attachment" + - `Cache-Control` 用于性能的缓存标头 + - `Accept-Ranges` 对于音频/视频文件设置为 "bytes" + + ### 错误 + - 400, `invalid_param`, 参数输入异常 + - 403, `file_access_denied`, 文件访问被拒绝或文件不属于当前应用程序 + - 404, `file_not_found`, 文件未找到或已被删除 + - 500, 服务内部错误 + + + + ### 请求示例 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### 作为附件下载 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### 响应标头示例 + + ```http {{ title: 'Headers - 图片预览' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### 文件下载响应标头 + + ```http {{ title: 'Headers - 文件下载' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + +--- + --- + + + + Preview or download uploaded files. This endpoint allows you to access files that have been previously uploaded via the File Upload API. + + Files can only be accessed if they belong to messages within the requesting application. + + ### Path Parameters + - `file_id` (string) Required + The unique identifier of the file to preview, obtained from the File Upload API response. + + ### Query Parameters + - `as_attachment` (boolean) Optional + Whether to force download the file as an attachment. Default is `false` (preview in browser). + + ### Response + Returns the file content with appropriate headers for browser display or download. + - `Content-Type` Set based on file mime type + - `Content-Length` File size in bytes (if available) + - `Content-Disposition` Set to "attachment" if `as_attachment=true` + - `Cache-Control` Caching headers for performance + - `Accept-Ranges` Set to "bytes" for audio/video files + + ### Errors + - 400, `invalid_param`, abnormal parameter input + - 403, `file_access_denied`, file access denied or file does not belong to current application + - 404, `file_not_found`, file not found or has been deleted + - 500, internal server error + + + + ### Request Example + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### Download as Attachment + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### Response Headers Example + + ```http {{ title: 'Headers - Image Preview' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### Download Response Headers + + ```http {{ title: 'Headers - File Download' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + +--- + --- + + + + アップロードされたファイルをプレビューまたはダウンロードします。このエンドポイントを使用すると、以前にファイルアップロード API でアップロードされたファイルにアクセスできます。 + + ファイルは、リクエストしているアプリケーションのメッセージ範囲内にある場合のみアクセス可能です。 + + ### パスパラメータ + - `file_id` (string) 必須 + プレビューするファイルの一意識別子。ファイルアップロード API レスポンスから取得します。 + + ### クエリパラメータ + - `as_attachment` (boolean) オプション + ファイルを添付ファイルとして強制ダウンロードするかどうか。デフォルトは `false`(ブラウザでプレビュー)。 + + ### レスポンス + ブラウザ表示またはダウンロード用の適切なヘッダー付きでファイル内容を返します。 + - `Content-Type` ファイル MIME タイプに基づいて設定 + - `Content-Length` ファイルサイズ(バイト、利用可能な場合) + - `Content-Disposition` `as_attachment=true` の場合は "attachment" に設定 + - `Cache-Control` パフォーマンス向上のためのキャッシュヘッダー + - `Accept-Ranges` 音声/動画ファイルの場合は "bytes" に設定 + + ### エラー + - 400, `invalid_param`, パラメータ入力異常 + - 403, `file_access_denied`, ファイルアクセス拒否またはファイルが現在のアプリケーションに属していません + - 404, `file_not_found`, ファイルが見つからないか削除されています + - 500, サーバー内部エラー + + + + ### リクエスト例 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### 添付ファイルとしてダウンロード + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### レスポンスヘッダー例 + + ```http {{ title: 'Headers - 画像プレビュー' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### ダウンロードレスポンスヘッダー + + ```http {{ title: 'Headers - ファイルダウンロード' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + +--- + --- + + + + + 预览或下载已上传的文件。此端点允许您访问先前通过文件上传 API 上传的文件。 + + 文件只能在属于请求应用程序的消息范围内访问。 + + ### 路径参数 + - `file_id` (string) 必需 + 要预览的文件的唯一标识符,从文件上传 API 响应中获得。 + + ### 查询参数 + - `as_attachment` (boolean) 可选 + 是否强制将文件作为附件下载。默认为 `false`(在浏览器中预览)。 + + ### 响应 + 返回带有适当浏览器显示或下载标头的文件内容。 + - `Content-Type` 根据文件 MIME 类型设置 + - `Content-Length` 文件大小(以字节为单位,如果可用) + - `Content-Disposition` 如果 `as_attachment=true` 则设置为 "attachment" + - `Cache-Control` 用于性能的缓存标头 + - `Accept-Ranges` 对于音频/视频文件设置为 "bytes" + + ### 错误 + - 400, `invalid_param`, 参数输入异常 + - 403, `file_access_denied`, 文件访问被拒绝或文件不属于当前应用程序 + - 404, `file_not_found`, 文件未找到或已被删除 + - 500, 服务内部错误 + + + + ### 请求示例 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### 作为附件下载 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### 响应标头示例 + + ```http {{ title: 'Headers - 图片预览' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### 文件下载响应标头 + + ```http {{ title: 'Headers - 文件下载' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + +--- + + + + Preview or download uploaded files. This endpoint allows you to access files that have been previously uploaded via the File Upload API. + + Files can only be accessed if they belong to messages within the requesting application. + + ### Path Parameters + - `file_id` (string) Required + The unique identifier of the file to preview, obtained from the File Upload API response. + + ### Query Parameters + - `as_attachment` (boolean) Optional + Whether to force download the file as an attachment. Default is `false` (preview in browser). + + ### Response + Returns the file content with appropriate headers for browser display or download. + - `Content-Type` Set based on file mime type + - `Content-Length` File size in bytes (if available) + - `Content-Disposition` Set to "attachment" if `as_attachment=true` + - `Cache-Control` Caching headers for performance + - `Accept-Ranges` Set to "bytes" for audio/video files + + ### Errors + - 400, `invalid_param`, abnormal parameter input + - 403, `file_access_denied`, file access denied or file does not belong to current application + - 404, `file_not_found`, file not found or has been deleted + - 500, internal server error + + + + ### Request Example + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### Download as Attachment + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### Response Headers Example + + ```http {{ title: 'Headers - Image Preview' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### Download Response Headers + + ```http {{ title: 'Headers - File Download' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + + +--- + + + + アップロードされたファイルをプレビューまたはダウンロードします。このエンドポイントを使用すると、以前にファイルアップロード API でアップロードされたファイルにアクセスできます。 + + ファイルは、リクエストしているアプリケーションのメッセージ範囲内にある場合のみアクセス可能です。 + + ### パスパラメータ + - `file_id` (string) 必須 + プレビューするファイルの一意識別子。ファイルアップロード API レスポンスから取得します。 + + ### クエリパラメータ + - `as_attachment` (boolean) オプション + ファイルを添付ファイルとして強制ダウンロードするかどうか。デフォルトは `false`(ブラウザでプレビュー)。 + + ### レスポンス + ブラウザ表示またはダウンロード用の適切なヘッダー付きでファイル内容を返します。 + - `Content-Type` ファイル MIME タイプに基づいて設定 + - `Content-Length` ファイルサイズ(バイト、利用可能な場合) + - `Content-Disposition` `as_attachment=true` の場合は "attachment" に設定 + - `Cache-Control` パフォーマンス向上のためのキャッシュヘッダー + - `Accept-Ranges` 音声/動画ファイルの場合は "bytes" に設定 + + ### エラー + - 400, `invalid_param`, パラメータ入力異常 + - 403, `file_access_denied`, ファイルアクセス拒否またはファイルが現在のアプリケーションに属していません + - 404, `file_not_found`, ファイルが見つからないか削除されています + - 500, サーバー内部エラー + + + + ### リクエスト例 + + + ```bash {{ title: 'cURL - ブラウザプレビュー' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### 添付ファイルとしてダウンロード + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### レスポンスヘッダー例 + + ```http {{ title: 'ヘッダー - 画像プレビュー' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### ダウンロードレスポンスヘッダー + + ```http {{ title: 'ヘッダー - ファイルダウンロード' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + + +--- + --- + + + + 预览或下载已上传的文件。此端点允许您访问先前通过文件上传 API 上传的文件。 + + 文件只能在属于请求应用程序的消息范围内访问。 + + ### 路径参数 + - `file_id` (string) 必需 + 要预览的文件的唯一标识符,从文件上传 API 响应中获得。 + + ### 查询参数 + - `as_attachment` (boolean) 可选 + 是否强制将文件作为附件下载。默认为 `false`(在浏览器中预览)。 + + ### 响应 + 返回带有适当浏览器显示或下载标头的文件内容。 + - `Content-Type` 根据文件 MIME 类型设置 + - `Content-Length` 文件大小(以字节为单位,如果可用) + - `Content-Disposition` 如果 `as_attachment=true` 则设置为 "attachment" + - `Cache-Control` 用于性能的缓存标头 + - `Accept-Ranges` 对于音频/视频文件设置为 "bytes" + + ### 错误 + - 400, `invalid_param`, 参数输入异常 + - 403, `file_access_denied`, 文件访问被拒绝或文件不属于当前应用程序 + - 404, `file_not_found`, 文件未找到或已被删除 + - 500, 服务内部错误 + + + + ### 请求示例 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview' \ + --header 'Authorization: Bearer {api_key}' + ``` + + + + ### 作为附件下载 + + + ```bash {{ title: 'cURL' }} + curl -X GET '${props.appDetail.api_base_url}/files/72fa9618-8f89-4a37-9b33-7e1178a24a67/preview?as_attachment=true' \ + --header 'Authorization: Bearer {api_key}' \ + --output downloaded_file.png + ``` + + + + ### 响应标头示例 + + ```http {{ title: 'Headers - 图片预览' }} + Content-Type: image/png + Content-Length: 1024 + Cache-Control: public, max-age=3600 + ``` + + + ### 文件下载响应标头 + + ```http {{ title: 'Headers - 文件下载' }} + Content-Type: image/png + Content-Length: 1024 + Content-Disposition: attachment; filename*=UTF-8''example.png + Cache-Control: public, max-age=3600 + ``` + + + +--- +