diff --git a/hpc_server_openapi.json b/hpc_server_openapi.json index 5a1d98c..64dbc1e 100644 --- a/hpc_server_openapi.json +++ b/hpc_server_openapi.json @@ -23,6 +23,18 @@ { "name": "Applications", "description": "Application definition CRUD and job submission" + }, + { + "name": "File Uploads", + "description": "Chunked file upload with SHA256 dedup, breakpoint resume, and instant upload" + }, + { + "name": "Files", + "description": "File listing, metadata, download (with Range support), and deletion" + }, + { + "name": "Folders", + "description": "Folder CRUD with materialized path hierarchy" } ], "paths": { @@ -1103,6 +1115,649 @@ } } } + }, + "/files/uploads": { + "post": { + "tags": ["File Uploads"], + "operationId": "initUpload", + "summary": "Initialize a chunked upload session", + "description": "Creates an upload session for chunked file upload. If a file with the same SHA256 already exists, returns the existing file immediately (instant upload / dedup).", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InitUploadRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Upload session created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadSessionResponse" + } + } + } + }, + "200": { + "description": "Dedup hit - file already exists, returns existing file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + } + }, + "/files/uploads/{sessionId}/chunks/{chunkIndex}": { + "put": { + "tags": ["File Uploads"], + "operationId": "uploadChunk", + "summary": "Upload a single chunk", + "description": "Uploads a file chunk for the specified session. Chunks can be uploaded in any order and are idempotent.", + "parameters": [ + { + "$ref": "#/components/parameters/SessionId" + }, + { + "$ref": "#/components/parameters/ChunkIndex" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["chunk"], + "properties": { + "chunk": { + "type": "string", + "format": "binary", + "description": "File chunk data" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Chunk uploaded successfully" + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "404": { + "description": "Upload session not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + } + }, + "/files/uploads/{sessionId}/complete": { + "post": { + "tags": ["File Uploads"], + "operationId": "completeUpload", + "summary": "Complete an upload session", + "description": "Finalizes the upload by merging all chunks via ComposeObject. Supports retry if previously failed.", + "parameters": [ + { + "$ref": "#/components/parameters/SessionId" + } + ], + "responses": { + "201": { + "description": "Upload completed, returns file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileResponse" + } + } + } + }, + "400": { + "description": "Invalid request or missing chunks", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "404": { + "description": "Upload session not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + } + }, + "/files/uploads/{sessionId}": { + "get": { + "tags": ["File Uploads"], + "operationId": "getUploadStatus", + "summary": "Get upload session status", + "description": "Returns the current status of an upload session including list of uploaded chunk indices.", + "parameters": [ + { + "$ref": "#/components/parameters/SessionId" + } + ], + "responses": { + "200": { + "description": "Upload session status", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadSessionResponse" + } + } + } + }, + "404": { + "description": "Upload session not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + }, + "delete": { + "tags": ["File Uploads"], + "operationId": "cancelUpload", + "summary": "Cancel an upload session", + "description": "Cancels an in-progress upload, deletes uploaded chunks from MinIO, and removes the session.", + "parameters": [ + { + "$ref": "#/components/parameters/SessionId" + } + ], + "responses": { + "200": { + "description": "Upload cancelled successfully" + }, + "404": { + "description": "Upload session not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + } + }, + "/files": { + "get": { + "tags": ["Files"], + "operationId": "listFiles", + "summary": "List files with pagination", + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { "type": "integer", "default": 1 } + }, + { + "name": "page_size", + "in": "query", + "schema": { "type": "integer", "default": 20 } + }, + { + "name": "folder_id", + "in": "query", + "required": false, + "description": "Filter by folder ID", + "schema": { "type": "integer", "format": "int64" } + }, + { + "name": "search", + "in": "query", + "required": false, + "description": "Search files by name", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "List of files", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileListResponse" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + } + }, + "/files/{id}": { + "get": { + "tags": ["Files"], + "operationId": "getFile", + "summary": "Get file metadata", + "parameters": [ + { + "$ref": "#/components/parameters/FileId" + } + ], + "responses": { + "200": { + "description": "File metadata", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileResponse" + } + } + } + }, + "404": { + "description": "File not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + }, + "delete": { + "tags": ["Files"], + "operationId": "deleteFile", + "summary": "Delete a file", + "description": "Soft-deletes a file. If no other files reference the same blob, the blob is removed from MinIO.", + "parameters": [ + { + "$ref": "#/components/parameters/FileId" + } + ], + "responses": { + "200": { + "description": "File deleted successfully" + }, + "404": { + "description": "File not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + } + }, + "/files/{id}/download": { + "get": { + "tags": ["Files"], + "operationId": "downloadFile", + "summary": "Download a file", + "description": "Downloads the file content. Supports HTTP Range header for partial content delivery.", + "parameters": [ + { + "$ref": "#/components/parameters/FileId" + }, + { + "name": "Range", + "in": "header", + "required": false, + "description": "Byte range for partial content", + "example": "bytes=0-1023", + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "Full file content", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "206": { + "description": "Partial content", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "File not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + } + }, + "/files/folders": { + "post": { + "tags": ["Folders"], + "operationId": "createFolder", + "summary": "Create a folder", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFolderRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Folder created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FolderResponse" + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + }, + "get": { + "tags": ["Folders"], + "operationId": "listFolders", + "summary": "List folders", + "parameters": [ + { + "name": "parent_id", + "in": "query", + "required": false, + "description": "Parent folder ID (null for root)", + "schema": { "type": "integer", "format": "int64" } + } + ], + "responses": { + "200": { + "description": "List of folders", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderResponse" + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + } + }, + "/files/folders/{id}": { + "get": { + "tags": ["Folders"], + "operationId": "getFolder", + "summary": "Get folder details", + "parameters": [ + { + "$ref": "#/components/parameters/FolderId" + } + ], + "responses": { + "200": { + "description": "Folder details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FolderResponse" + } + } + } + }, + "404": { + "description": "Folder not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + }, + "delete": { + "tags": ["Folders"], + "operationId": "deleteFolder", + "summary": "Delete a folder", + "description": "Deletes an empty folder. Cannot delete folders that contain files or sub-folders.", + "parameters": [ + { + "$ref": "#/components/parameters/FolderId" + } + ], + "responses": { + "200": { + "description": "Folder deleted successfully" + }, + "400": { + "description": "Folder is not empty", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "404": { + "description": "Folder not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseError" + } + } + } + } + } + } } }, "components": { @@ -1143,6 +1798,46 @@ "type": "integer", "format": "int64" } + }, + "SessionId": { + "name": "sessionId", + "in": "path", + "required": true, + "description": "Upload session ID", + "schema": { + "type": "integer", + "format": "int64" + } + }, + "ChunkIndex": { + "name": "chunkIndex", + "in": "path", + "required": true, + "description": "Chunk index (0-based)", + "schema": { + "type": "integer", + "minimum": 0 + } + }, + "FileId": { + "name": "id", + "in": "path", + "required": true, + "description": "File ID", + "schema": { + "type": "integer", + "format": "int64" + } + }, + "FolderId": { + "name": "id", + "in": "path", + "required": true, + "description": "Folder ID", + "schema": { + "type": "integer", + "format": "int64" + } } }, "schemas": { @@ -1789,6 +2484,198 @@ "description": "Items per page" } } + }, + "InitUploadRequest": { + "type": "object", + "required": ["file_name", "file_size", "sha256", "chunk_size"], + "properties": { + "file_name": { + "type": "string", + "description": "File name" + }, + "file_size": { + "type": "integer", + "format": "int64", + "description": "Total file size in bytes" + }, + "sha256": { + "type": "string", + "description": "SHA256 hash of the entire file (hex encoded)" + }, + "chunk_size": { + "type": "integer", + "format": "int64", + "description": "Chunk size in bytes (must be >= min_chunk_size config)" + }, + "folder_id": { + "type": "integer", + "format": "int64", + "description": "Target folder ID (null for root)", + "nullable": true + } + } + }, + "UploadSessionResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "Session ID" + }, + "file_name": { + "type": "string", + "description": "File name" + }, + "file_size": { + "type": "integer", + "format": "int64", + "description": "Total file size in bytes" + }, + "chunk_size": { + "type": "integer", + "format": "int64", + "description": "Chunk size in bytes" + }, + "total_chunks": { + "type": "integer", + "description": "Total number of chunks" + }, + "sha256": { + "type": "string", + "description": "SHA256 hash of the entire file" + }, + "status": { + "type": "string", + "enum": ["pending", "uploading", "merging", "completed", "failed", "cancelled", "expired"], + "description": "Session status" + }, + "uploaded_chunks": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "List of uploaded chunk indices" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "Session expiration time" + } + } + }, + "FileResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "File ID" + }, + "name": { + "type": "string", + "description": "File name" + }, + "folder_id": { + "type": "integer", + "format": "int64", + "description": "Parent folder ID", + "nullable": true + }, + "size": { + "type": "integer", + "format": "int64", + "description": "File size in bytes" + }, + "mime_type": { + "type": "string", + "description": "MIME type" + }, + "sha256": { + "type": "string", + "description": "SHA256 hash" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "FileListResponse": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileResponse" + }, + "description": "List of files" + }, + "total": { + "type": "integer", + "description": "Total number of matching files" + }, + "page": { + "type": "integer", + "description": "Current page number" + }, + "page_size": { + "type": "integer", + "description": "Items per page" + } + } + }, + "FolderResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": "Folder ID" + }, + "name": { + "type": "string", + "description": "Folder name" + }, + "parent_id": { + "type": "integer", + "format": "int64", + "description": "Parent folder ID (null for root)", + "nullable": true + }, + "path": { + "type": "string", + "description": "Materialized path (e.g. /data/reports/)" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "CreateFolderRequest": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "Folder name (no path traversal characters)" + }, + "parent_id": { + "type": "integer", + "format": "int64", + "description": "Parent folder ID (null for root)", + "nullable": true + } + } } } }