diff --git a/AGENTS.md b/AGENTS.md
index 63ac35e..ac12dbb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -129,3 +129,10 @@ For multi-step tasks, state a brief plan:
- **Magic Numbers:** Avoid using magic numbers; use constants or named variables.
- **Redundant Code:** Do NOT write redundant code.
- **Hardcoded Values:** Do NOT use hardcoded values; use constants or named variables.
+
+### Workflow Guidelines
+
+- You have access to GitHub CLI. Use it for all GitHub related actions.
+- Before implementing, check the active branch:
+ 1. On `master` or `main`: pull latest, then create a short branch named `feat/{feature}`, `fix/{fix}`, or `docs/{docs}`.
+ 2. Already on a non-default branch: no action needed.
diff --git a/README.md b/README.md
index 0c3f866..2745a1f 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ Omnihance A3 Agent is a full-stack application consisting of:
- Can manage users (list, update status, set passwords)
- Cannot have status changed by other admins
- **Admin** (`admin`): Administrative access
- - Can view and edit files
+ - Can view, edit, and upload files in the file browser
- Can revert file changes
- Can upload game client data
- Can view metrics and game data
@@ -55,7 +55,7 @@ Omnihance A3 Agent is a full-stack application consisting of:
- **Permission Actions**:
- `view_files`: View file system and file contents (super_admin, admin, viewer)
- `download_files`: Create and use one-day user-bound download links for file-browser and backup output files through `POST /api/file-tree/download-link` and `GET /api/file-tree/download/{token}` (super_admin, admin)
- - `edit_files`: Edit files (super_admin, admin)
+ - `edit_files`: Edit files and upload files through the file browser (super_admin, admin)
- `revert_files`: Revert files to previous revisions (super_admin, admin)
- `upload_game_data`: Upload MON.ull and MC.ull files (super_admin, admin)
- `manage_users`: Manage user accounts (super_admin only)
@@ -78,6 +78,7 @@ Omnihance A3 Agent is a full-stack application consisting of:
- **File Viewing**: View NPC files, quest files, spawn files, drop files, item files, item combination data files, and text files in the browser
- **File Duplication**: Right-click any file in the file explorer to duplicate it with a custom name
- **Directory Downloads**: Right-click a directory to request a ZIP download. The directory is compressed in the background through a tagged one-time backup job, stored under `DIRECTORY_DOWNLOADS_DIRECTORY`, and delivered through the same secure temp-link download flow used for files and backup outputs.
+- **Chunked File Uploads**: Drag files or folders into a real directory, or use the Upload button next to Show dotfiles to open a drop zone with file and folder pickers. Uploads are disabled on the root drive-listing page, run one file at a time per browser tab, auto-rename conflicts with the existing `(copy)` naming pattern, retry transient chunk failures, verify SHA-256 before finalizing, and keep abandoned temp data hidden until server cleanup removes it.
- **File Editing**:
- **NPC File Editor**: Edit NPC properties including:
- ID, Name, Respawn Rate
@@ -524,7 +525,7 @@ The application uses environment variables for configuration. A `.env` file is a
| `REVISIONS_DIRECTORY` | `.revisions` | Directory for file revision backups |
| `BACKUPS_DIRECTORY` | `.backups` | Directory for internal backup lock files |
| `DIRECTORY_DOWNLOADS_DIRECTORY` | `.directory-download` | Directory for generated directory-download ZIP archives |
-| `MAX_FILE_UPLOAD_SIZE_MB` | `2` | Maximum multipart upload size in MB |
+| `MAX_FILE_UPLOAD_SIZE_MB` | `1024` | Maximum per-file upload size in MB for file browser and game client uploads |
| `DIRECTORY_SHORTCUTS_LIMIT` | `5` | Maximum pinned directories per user (`0` disables the limit) |
| `RUNNING_IN_DOCKER` | `false` | Disable host metrics collection when running in Docker |
| `SESSION_TIMEOUT_SECONDS` | `2592000` | Session timeout (30 days) |
@@ -583,6 +584,11 @@ Only stable GitHub releases are considered because GitHub's latest release endpo
- `PUT /api/file-tree/item-combination-data` - Update A3 item combination data
- `POST /api/file-tree/revert-file` - Revert file to previous revision
- `POST /api/file-tree/duplicate-file` - Duplicate a file in the same directory
+- `POST /api/file-tree/uploads` - Start a file-browser upload batch for a non-root destination directory, enforce the configured per-file upload size limit, and reserve conflict-safe target names
+- `PUT /api/file-tree/uploads/{upload_id}/files/{file_id}/chunks/{chunk_index}` - Upload one binary file chunk with retry support
+- `POST /api/file-tree/uploads/{upload_id}/files/{file_id}/complete` - Verify SHA-256 and finalize one uploaded file
+- `POST /api/file-tree/uploads/{upload_id}/heartbeat` - Keep an active upload session alive while the browser tab is open
+- `DELETE /api/file-tree/uploads/{upload_id}` - Cancel an upload session and remove hidden temp data
- `GET /api/file-tree/revision-summary` - Get revision count for a file
- `POST /api/file-tree/download-link` - Create or reuse a one-day user-bound download link for a file (requires `download_files` permission)
- `POST /api/file-tree/directory-download-link` - Create, reuse, or start a background directory ZIP job and return either a secure download URL or polling metadata (requires `download_files` permission)
@@ -599,14 +605,14 @@ Only stable GitHub releases are considered because GitHub's latest release endpo
- `GET /api/game-client-data/counts` - Get imported record counts for monsters, maps, and item file types
- `GET /api/game-client-data/monsters` - Get monster client data (supports optional `s` query parameter for search)
-- `POST /api/game-client-data/upload-mon-file` - Upload MON.ull file to populate monster database
+- `POST /api/game-client-data/upload-mon-file` - Upload MON.ull file to populate monster database, enforcing the configured per-file upload size limit
- `GET /api/game-client-data/maps` - Get map client data (supports optional `s` query parameter for search)
-- `POST /api/game-client-data/upload-mc-file` - Upload MC.ull file to populate map database
+- `POST /api/game-client-data/upload-mc-file` - Upload MC.ull file to populate map database, enforcing the configured per-file upload size limit
- `GET /api/game-client-data/items` - Get item client data (supports optional `s` query parameter for search)
-- `POST /api/game-client-data/upload-it0-file` - Upload IT0.ull file to populate item data
-- `POST /api/game-client-data/upload-it1-file` - Upload IT1.ull file to populate item data
-- `POST /api/game-client-data/upload-it2-file` - Upload IT2.ull file to populate item data
-- `POST /api/game-client-data/upload-it3-file` - Upload IT3.ull file to populate item data
+- `POST /api/game-client-data/upload-it0-file` - Upload IT0.ull file to populate item data, enforcing the configured per-file upload size limit
+- `POST /api/game-client-data/upload-it1-file` - Upload IT1.ull file to populate item data, enforcing the configured per-file upload size limit
+- `POST /api/game-client-data/upload-it2-file` - Upload IT2.ull file to populate item data, enforcing the configured per-file upload size limit
+- `POST /api/game-client-data/upload-it3-file` - Upload IT3.ull file to populate item data, enforcing the configured per-file upload size limit
### Directory Shortcuts
@@ -732,7 +738,7 @@ The application uses SQLite with the following main tables:
5. **Upload Game Client Data**: Navigate to the Client Data section and upload MON.ull, MC.ull, and IT0.ull through IT3.ull files to populate monster, map, and item databases (requires admin or super admin role).
-6. **Navigate Files**: Use the file tree sidebar to browse your server's file system (all authenticated users can view). Pin frequently used directories as shortcuts, right-click files to duplicate them, and download files or directories with admin or super admin access. Directory downloads compress in the background; keep the page open and do not refresh so the download can start automatically when ready. If you refresh, click the same directory download again to resume polling for the in-progress job.
+6. **Navigate Files**: Use the file tree sidebar to browse your server's file system (all authenticated users can view). Pin frequently used directories as shortcuts, right-click files to duplicate them, and download files or directories with admin or super admin access. Admins and super admins can upload files or folders inside a selected directory by dragging into the browser or using the Upload button beside Show dotfiles; uploads are queued per tab, conflicts are auto-renamed, and progress stays visible in the bottom-right panel. Directory downloads compress in the background; keep the page open and do not refresh so the download can start automatically when ready. If you refresh, click the same directory download again to resume polling for the in-progress job.
7. **Edit Files**: Click on editable files (NPC files, quest files, spawn files, drop files (monster drop configurations), item files, item combination data files, or text files) to view and edit them (requires admin or super admin role).
- **Quest Files**: Edit quest configurations with type-aware objectives, add/remove controls for optional slots, and binary-safe padding preservation
diff --git a/cmd/omnihance-a3-agent/docs/openapi.yml b/cmd/omnihance-a3-agent/docs/openapi.yml
index 46fc492..b02f8d0 100644
--- a/cmd/omnihance-a3-agent/docs/openapi.yml
+++ b/cmd/omnihance-a3-agent/docs/openapi.yml
@@ -227,7 +227,7 @@ paths:
tags:
- status
summary: Get server status
- description: Returns the current server status including name, version, setup status, and new version availability.
+ description: Returns the current server status including name, version, setup status, configured upload size limit, and new version availability.
responses:
'200':
description: Server status retrieved successfully
@@ -1362,6 +1362,337 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
+ /api/file-tree/uploads:
+ post:
+ tags:
+ - file-system
+ summary: Start file-browser upload batch
+ description: Creates a chunked upload session for a non-root destination directory. Requires edit file permission. Each file must not exceed the server's configured per-file upload size limit. The server reserves final target names with the existing copy-name pattern so concurrent uploads cannot overwrite each other.
+ security:
+ - ApiKeyAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateFileUploadRequest'
+ responses:
+ '201':
+ description: Upload session created successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateFileUploadResponse'
+ '400':
+ description: Bad Request - Invalid destination path, upload file path, file size, duplicate relative path, or chunk size
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Forbidden - Missing edit file permission or filesystem permission denied
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Destination path not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '413':
+ description: Request Entity Too Large - One or more files exceed the configured per-file upload size limit
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '507':
+ description: Insufficient storage while creating upload temp storage
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /api/file-tree/uploads/{upload_id}/files/{file_id}/chunks/{chunk_index}:
+ put:
+ tags:
+ - file-system
+ summary: Upload one file chunk
+ description: Writes one binary chunk into hidden upload temp storage. Chunk uploads refresh the session activity timestamp and may be retried by the client for network or transient server failures.
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: upload_id
+ required: true
+ schema:
+ type: string
+ description: Upload session ID returned by the batch creation endpoint.
+ - in: path
+ name: file_id
+ required: true
+ schema:
+ type: string
+ description: Server file ID returned for the file in the upload batch.
+ - in: path
+ name: chunk_index
+ required: true
+ schema:
+ type: integer
+ minimum: 0
+ description: Zero-based chunk index.
+ requestBody:
+ required: true
+ content:
+ application/octet-stream:
+ schema:
+ type: string
+ format: binary
+ responses:
+ '200':
+ description: Chunk uploaded successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FileUploadChunkResponse'
+ '400':
+ description: Bad Request - Invalid chunk index, unexpected chunk size, or chunk out of range
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Forbidden - Missing edit file permission or filesystem permission denied
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Upload session or file not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '410':
+ description: Upload session expired
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '507':
+ description: Insufficient storage while writing upload chunk
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /api/file-tree/uploads/{upload_id}/files/{file_id}/complete:
+ post:
+ tags:
+ - file-system
+ summary: Complete uploaded file
+ description: Verifies that all chunks are present, checks the uploaded file SHA-256 against the client hash, resolves any final external path conflict without overwriting, then moves the hidden temp file into place.
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: upload_id
+ required: true
+ schema:
+ type: string
+ description: Upload session ID returned by the batch creation endpoint.
+ - in: path
+ name: file_id
+ required: true
+ schema:
+ type: string
+ description: Server file ID returned for the file in the upload batch.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CompleteFileUploadRequest'
+ responses:
+ '200':
+ description: File verified and finalized successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CompleteFileUploadResponse'
+ '400':
+ description: Bad Request - Invalid SHA-256 value or missing chunks
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Forbidden - Missing edit file permission or filesystem permission denied
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Upload session or file not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '409':
+ description: Conflict - Uploaded file failed integrity check
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '410':
+ description: Upload session expired
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '507':
+ description: Insufficient storage while finalizing uploaded file
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /api/file-tree/uploads/{upload_id}/heartbeat:
+ post:
+ tags:
+ - file-system
+ summary: Refresh upload session heartbeat
+ description: Refreshes the activity timestamp for an active upload session so hidden temp data is not treated as abandoned while the browser tab is still uploading.
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: upload_id
+ required: true
+ schema:
+ type: string
+ description: Upload session ID returned by the batch creation endpoint.
+ responses:
+ '200':
+ description: Upload heartbeat accepted
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FileUploadHeartbeatResponse'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Forbidden - Missing edit file permission
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Upload session not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '410':
+ description: Upload session expired
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ /api/file-tree/uploads/{upload_id}:
+ delete:
+ tags:
+ - file-system
+ summary: Cancel upload session
+ description: Cancels an active upload session, removes hidden temp upload data, and releases any reserved final paths for files that have not completed.
+ security:
+ - ApiKeyAuth: []
+ parameters:
+ - in: path
+ name: upload_id
+ required: true
+ schema:
+ type: string
+ description: Upload session ID returned by the batch creation endpoint.
+ responses:
+ '200':
+ description: Upload cancelled successfully
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ example: 'Upload cancelled'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '403':
+ description: Forbidden - Missing edit file permission
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '404':
+ description: Upload session not found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ '500':
+ description: Internal server error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
/api/file-tree/revision-summary:
get:
tags:
@@ -1672,7 +2003,7 @@ paths:
tags:
- game-data
summary: Upload monster list client data file
- description: Uploads and processes a monster list client data file (MON.ull). The file is decoded using ULL decryption, parsed into structured monster data, and bulk replaces all existing monster client data in the database. The file size must not exceed the maximum upload size configured in the server. All uploaded monsters are associated with the current user as both creator and updater.
+ description: Uploads and processes a monster list client data file (MON.ull). The file is decoded using ULL decryption, parsed into structured monster data, and bulk replaces all existing monster client data in the database. The file size must not exceed the maximum per-file upload size configured in the server. All uploaded monsters are associated with the current user as both creator and updater.
security:
- ApiKeyAuth: []
requestBody:
@@ -1705,7 +2036,7 @@ paths:
description: The number of monster records that were uploaded
example: 150
'400':
- description: Bad Request - File size exceeds maximum allowed size, failed to parse multipart form, file not found in form, or failed to parse monster file
+ description: Bad Request - Failed to parse multipart form, file not found in form, or failed to parse monster file
content:
application/json:
schema:
@@ -1716,6 +2047,12 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
+ '413':
+ description: Request Entity Too Large - File exceeds the configured per-file upload size limit
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal server error - Failed to read file or save monster data
content:
@@ -1727,7 +2064,7 @@ paths:
tags:
- game-data
summary: Upload map list client data file
- description: Uploads and processes a map list client data file (MC.ull). The file is decoded using ULL decryption, parsed into structured map data, and bulk replaces all existing map client data in the database. The file size must not exceed the maximum upload size configured in the server. All uploaded maps are associated with the current user as both creator and updater.
+ description: Uploads and processes a map list client data file (MC.ull). The file is decoded using ULL decryption, parsed into structured map data, and bulk replaces all existing map client data in the database. The file size must not exceed the maximum per-file upload size configured in the server. All uploaded maps are associated with the current user as both creator and updater.
security:
- ApiKeyAuth: []
requestBody:
@@ -1760,7 +2097,7 @@ paths:
description: The number of map list records that were uploaded
example: 50
'400':
- description: Bad Request - File size exceeds maximum allowed size, failed to parse multipart form, file not found in form, or failed to parse map file
+ description: Bad Request - Failed to parse multipart form, file not found in form, or failed to parse map file
content:
application/json:
schema:
@@ -1771,6 +2108,12 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
+ '413':
+ description: Request Entity Too Large - File exceeds the configured per-file upload size limit
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal server error - Failed to read file or save map data
content:
@@ -1866,7 +2209,7 @@ paths:
tags:
- game-data
summary: Upload IT0 item client data file
- description: Uploads and processes an IT0.ull item client data file. The file is decoded using ULL decryption, parsed with agonyl-utils-go itemfile helpers, and replaces existing IT0 item client data only.
+ description: Uploads and processes an IT0.ull item client data file. The file must not exceed the maximum per-file upload size configured in the server. The file is decoded using ULL decryption, parsed with agonyl-utils-go itemfile helpers, and replaces existing IT0 item client data only.
security:
- ApiKeyAuth: []
requestBody:
@@ -1901,6 +2244,12 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
+ '413':
+ description: Request Entity Too Large - File exceeds the configured per-file upload size limit
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal server error - Failed to read file or save item data
content:
@@ -1912,7 +2261,7 @@ paths:
tags:
- game-data
summary: Upload IT1 item client data file
- description: Uploads and processes an IT1.ull item client data file and replaces existing IT1 item client data only.
+ description: Uploads and processes an IT1.ull item client data file. The file must not exceed the maximum per-file upload size configured in the server and replaces existing IT1 item client data only.
security:
- ApiKeyAuth: []
requestBody:
@@ -1947,6 +2296,12 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
+ '413':
+ description: Request Entity Too Large - File exceeds the configured per-file upload size limit
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal server error - Failed to read file or save item data
content:
@@ -1958,7 +2313,7 @@ paths:
tags:
- game-data
summary: Upload IT2 item client data file
- description: Uploads and processes an IT2.ull item client data file and replaces existing IT2 item client data only.
+ description: Uploads and processes an IT2.ull item client data file. The file must not exceed the maximum per-file upload size configured in the server and replaces existing IT2 item client data only.
security:
- ApiKeyAuth: []
requestBody:
@@ -1993,6 +2348,12 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
+ '413':
+ description: Request Entity Too Large - File exceeds the configured per-file upload size limit
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal server error - Failed to read file or save item data
content:
@@ -2004,7 +2365,7 @@ paths:
tags:
- game-data
summary: Upload IT3 item client data file
- description: Uploads and processes an IT3.ull item client data file and replaces existing IT3 item client data only.
+ description: Uploads and processes an IT3.ull item client data file. The file must not exceed the maximum per-file upload size configured in the server and replaces existing IT3 item client data only.
security:
- ApiKeyAuth: []
requestBody:
@@ -2039,6 +2400,12 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
+ '413':
+ description: Request Entity Too Large - File exceeds the configured per-file upload size limit
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal server error - Failed to read file or save item data
content:
@@ -4518,6 +4885,11 @@ components:
type: boolean
description: Whether metrics collection is enabled
example: true
+ max_file_upload_size_bytes:
+ type: integer
+ format: int64
+ description: Configured maximum upload size per file in bytes
+ example: 1073741824
ErrorResponse:
type: object
properties:
@@ -4831,6 +5203,165 @@ components:
type: string
description: Full path of the newly created duplicate file
example: "C:\\A3Server\\configs\\server (copy).ini"
+ CreateFileUploadRequest:
+ type: object
+ required:
+ - destination_path
+ - chunk_size
+ - files
+ properties:
+ destination_path:
+ type: string
+ description: Destination directory path. Upload creation is rejected for the root drive-listing page.
+ example: "C:\\A3Server"
+ chunk_size:
+ type: integer
+ format: int64
+ minimum: 1
+ maximum: 8388608
+ description: Chunk size in bytes that the client will use for each uploaded file.
+ example: 4194304
+ files:
+ type: array
+ minItems: 1
+ items:
+ $ref: '#/components/schemas/CreateFileUploadRequestFile'
+ CreateFileUploadRequestFile:
+ type: object
+ required:
+ - client_file_id
+ - relative_path
+ - size
+ properties:
+ client_file_id:
+ type: string
+ description: Client-generated ID used to map the response file reservation back to the selected browser file.
+ example: 'task-1-file-0'
+ relative_path:
+ type: string
+ description: Slash-separated upload path relative to the selected upload item. Folder uploads preserve nested paths here.
+ example: 'ZoneData/map/0.n_ndt'
+ size:
+ type: integer
+ format: int64
+ minimum: 0
+ description: File size in bytes. Must not exceed the server's configured per-file upload size limit.
+ example: 7340032
+ CreateFileUploadResponse:
+ type: object
+ properties:
+ upload_id:
+ type: string
+ description: Server upload session ID.
+ example: '01HZXY2M3A4B5C6D7E8F9G0H1J'
+ expires_at:
+ type: string
+ format: date-time
+ description: Session expiration timestamp if no heartbeat or chunk activity is received.
+ files:
+ type: array
+ items:
+ $ref: '#/components/schemas/CreateFileUploadResponseFile'
+ CreateFileUploadResponseFile:
+ type: object
+ properties:
+ client_file_id:
+ type: string
+ description: Client file ID from the creation request.
+ example: 'task-1-file-0'
+ file_id:
+ type: string
+ description: Server-generated file ID used for chunk upload and completion calls.
+ example: '01HZXY2M3A4B5C6D7E8F9G0H1K'
+ relative_path:
+ type: string
+ description: Original normalized upload relative path.
+ example: 'ZoneData/map/0.n_ndt'
+ resolved_relative_path:
+ type: string
+ description: Conflict-safe relative path reserved by the server.
+ example: 'ZoneData/map/0 (copy).n_ndt'
+ target_path:
+ type: string
+ description: Full reserved destination path for the uploaded file.
+ example: "C:\\A3Server\\ZoneData\\map\\0 (copy).n_ndt"
+ size:
+ type: integer
+ format: int64
+ description: File size in bytes.
+ example: 7340032
+ chunk_size:
+ type: integer
+ format: int64
+ description: Chunk size in bytes for this file.
+ example: 4194304
+ total_chunks:
+ type: integer
+ description: Number of chunks expected for this file.
+ example: 2
+ FileUploadChunkResponse:
+ type: object
+ properties:
+ message:
+ type: string
+ example: 'Chunk uploaded successfully'
+ received_chunks:
+ type: integer
+ description: Number of chunks received for the file so far.
+ example: 1
+ total_chunks:
+ type: integer
+ description: Number of chunks required for the file.
+ example: 2
+ CompleteFileUploadRequest:
+ type: object
+ required:
+ - sha256
+ properties:
+ sha256:
+ type: string
+ minLength: 64
+ maxLength: 64
+ pattern: '^[a-f0-9]{64}$'
+ description: Lowercase hexadecimal SHA-256 hash of the original browser file.
+ example: '6d37795021e544d53d2569bb6139720ee0869d1111d924aed610ea148a84efcf'
+ CompleteFileUploadResponse:
+ type: object
+ properties:
+ message:
+ type: string
+ example: 'File uploaded successfully'
+ file_id:
+ type: string
+ description: Server file ID that was completed.
+ example: '01HZXY2M3A4B5C6D7E8F9G0H1K'
+ relative_path:
+ type: string
+ description: Original normalized upload relative path.
+ example: 'ZoneData/map/0.n_ndt'
+ resolved_relative_path:
+ type: string
+ description: Final relative path used after reservation and final conflict checks.
+ example: 'ZoneData/map/0 (copy).n_ndt'
+ final_path:
+ type: string
+ description: Full destination path where the uploaded file was finalized.
+ example: "C:\\A3Server\\ZoneData\\map\\0 (copy).n_ndt"
+ sha256:
+ type: string
+ description: Server-calculated SHA-256 hash of the finalized upload.
+ example: '6d37795021e544d53d2569bb6139720ee0869d1111d924aed610ea148a84efcf'
+ FileUploadHeartbeatResponse:
+ type: object
+ properties:
+ upload_id:
+ type: string
+ description: Upload session ID.
+ example: '01HZXY2M3A4B5C6D7E8F9G0H1J'
+ expires_at:
+ type: string
+ format: date-time
+ description: Updated session expiration timestamp.
SpawnFileAPIData:
type: object
description: Parsed binary data from a spawn file (API request/response format). All fields are required when used as a request body.
diff --git a/cmd/omnihance-a3-agent/omnihance-a3-agent-ui/src/components/client-data-page.tsx b/cmd/omnihance-a3-agent/omnihance-a3-agent-ui/src/components/client-data-page.tsx
index 9d689d3..4285884 100644
--- a/cmd/omnihance-a3-agent/omnihance-a3-agent-ui/src/components/client-data-page.tsx
+++ b/cmd/omnihance-a3-agent/omnihance-a3-agent-ui/src/components/client-data-page.tsx
@@ -11,10 +11,13 @@ import {
uploadIt2File,
uploadIt3File,
} from '@/lib/api';
+import { useStatus } from '@/hooks/use-status';
export function ClientDataPage() {
const { hasPermission } = usePermissions();
+ const { status } = useStatus();
const canUploadGameData = hasPermission('upload_game_data');
+ const maxFileUploadSizeBytes = status?.max_file_upload_size_bytes;
const {
data: counts,
isLoading: countsLoading,
@@ -40,11 +43,13 @@ export function ClientDataPage() {
existingCount={counts?.monsters}
countLoading={countsLoading}
countError={countsError}
+ maxFileUploadSizeBytes={maxFileUploadSizeBytes}
/>
) : (
diff --git a/cmd/omnihance-a3-agent/omnihance-a3-agent-ui/src/components/client-data/item-file-upload.tsx b/cmd/omnihance-a3-agent/omnihance-a3-agent-ui/src/components/client-data/item-file-upload.tsx
index fd17f19..3781149 100644
--- a/cmd/omnihance-a3-agent/omnihance-a3-agent-ui/src/components/client-data/item-file-upload.tsx
+++ b/cmd/omnihance-a3-agent/omnihance-a3-agent-ui/src/components/client-data/item-file-upload.tsx
@@ -11,9 +11,10 @@ import {
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { APIError, type UploadFileResponse } from '@/lib/api';
-import { cn } from '@/lib/util';
+import { cn, formatBytes } from '@/lib/util';
import { queryKeys } from '@/constants';
import { ClientDataCountBadge } from '@/components/client-data/client-data-count-badge';
+import { validateGameClientUploadFile } from '@/components/client-data/upload-validation';
type ItemFileUploadProps = {
fileLabel: string;
@@ -21,6 +22,7 @@ type ItemFileUploadProps = {
countLoading: boolean;
countError: boolean;
uploadFile: (file: File) => Promise;
+ maxFileUploadSizeBytes?: number;
};
export function ItemFileUpload({
@@ -29,6 +31,7 @@ export function ItemFileUpload({
countLoading,
countError,
uploadFile,
+ maxFileUploadSizeBytes,
}: ItemFileUploadProps) {
const [file, setFile] = useState(null);
const [isDragging, setIsDragging] = useState(false);
@@ -40,6 +43,7 @@ export function ItemFileUpload({
mutationFn: uploadFile,
onSuccess: () => {
setFile(null);
+ setValidationError(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
@@ -74,13 +78,7 @@ export function ItemFileUpload({
return;
}
- if (!droppedFile.name.toLowerCase().endsWith('.ull')) {
- setValidationError(`Please select a valid ${fileLabel}.ull file.`);
- return;
- }
-
- setValidationError(null);
- setFile(droppedFile);
+ selectFile(droppedFile);
};
const handleFileSelect = (e: React.ChangeEvent) => {
@@ -89,8 +87,22 @@ export function ItemFileUpload({
return;
}
- if (!selectedFile.name.toLowerCase().endsWith('.ull')) {
- setValidationError(`Please select a valid ${fileLabel}.ull file.`);
+ selectFile(selectedFile);
+ };
+
+ const selectFile = (selectedFile: File) => {
+ uploadMutation.reset();
+ const errorMessage = validateGameClientUploadFile(
+ selectedFile,
+ fileLabel,
+ maxFileUploadSizeBytes,
+ );
+ if (errorMessage) {
+ setFile(null);
+ setValidationError(errorMessage);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
return;
}
@@ -111,6 +123,7 @@ export function ItemFileUpload({
const resetFile = () => {
setFile(null);
setValidationError(null);
+ uploadMutation.reset();
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
@@ -197,7 +210,7 @@ export function ItemFileUpload({
{file.name}
- ({(file.size / 1024).toFixed(2)} KB)
+ ({formatBytes(file.size)})