Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Omnihance A3 Agent is a full-stack application consisting of:
- Text files (MIME type detection)
- **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.
- **File Editing**:
- **NPC File Editor**: Edit NPC properties including:
- ID, Name, Respawn Rate
Expand Down Expand Up @@ -280,6 +281,11 @@ Omnihance A3 Agent is a full-stack application consisting of:
- Create manual or scheduled jobs for a selected file or directory
- Destination directories are created when missing, and destination paths must be directories
- Source path and destination path fields support searchable path suggestions
- **Directory download jobs**:
- Directory-download requests create tagged no-repeat file backup jobs with the `directory_download` tag
- Only one directory-download compression job can run at a time; repeat requests for the same running directory resume polling, while requests for a different directory return a conflict
- Completed directory ZIPs are reused until the source directory fingerprint changes
- Directory-download backup jobs hide their destination in the Backups UI and show the run trigger as `Directory Download`
- **Local SQL Server backups**:
- Back up one or more comma-separated database names
- SQL Server host, port, username, and password can be prefilled from settings
Expand Down Expand Up @@ -517,6 +523,7 @@ The application uses environment variables for configuration. A `.env` file is a
| `VERSION_CHECK_INTERVAL_SECONDS` | `3600` | How often to check GitHub for new releases |
| `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 |
| `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 |
Expand Down Expand Up @@ -578,6 +585,8 @@ Only stable GitHub releases are considered because GitHub's latest release endpo
- `POST /api/file-tree/duplicate-file` - Duplicate a file in the same directory
- `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)
- `GET /api/file-tree/directory-downloads/{run_id}` - Poll a directory-download run until it returns a secure download URL or terminal status
- `GET /api/file-tree/download/{token}` - Download a file through a valid user-bound temp link and record the download event

### Metrics
Expand Down Expand Up @@ -687,9 +696,10 @@ The application uses SQLite with the following main tables:
- Stores process name, file path, optional port, sequence order
- Tracks start/end times for uptime calculation
- Enforces unique paths to prevent duplicates
- **backup_jobs**: Backup job definitions, scheduling metadata, paths, SQL settings, and statuses
- **backup_jobs**: Backup job definitions, scheduling metadata, paths, SQL settings, statuses, and optional tags such as `directory_download`
- **backup_runs**: Backup run history, trigger type, status, output logs, errors, and cancellation timestamps
- **backup_run_files**: Output archive files created by each backup run
- **directory_download_archives**: Cached generated directory ZIP archives keyed by normalized directory path and recursive fingerprint
- **file_download_links**: One-day user-bound file download links, file fingerprints, source context, and per-link download counts
- **file_download_events**: Per-request file download audit events with user, link, source context, file fingerprint, IP address, and user agent
- **server_view_svr_info_rows**: Raw `SvrInfo.ini` rows for Main, Account, Zone, and Battle servers
Expand Down Expand Up @@ -722,7 +732,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 with admin or super admin access.
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.

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
Expand Down
159 changes: 159 additions & 0 deletions cmd/omnihance-a3-agent/docs/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,116 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/api/file-tree/directory-download-link:
post:
tags:
- file-system
summary: Create directory download link
description: Starts or resumes a background ZIP compression job for a directory. When an unchanged cached archive is available, returns a secure temp download link immediately.
security:
- ApiKeyAuth: []
parameters:
- in: query
name: path
required: true
schema:
type: string
description: Directory path to compress and download.
responses:
'200':
description: Directory archive is ready and a secure download link was created or reused
content:
application/json:
schema:
$ref: '#/components/schemas/DirectoryDownloadResponse'
'202':
description: Directory compression started or the same directory is already in progress
content:
application/json:
schema:
$ref: '#/components/schemas/DirectoryDownloadResponse'
'400':
description: Bad Request - Missing path or path is not a directory
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Unauthorized - Authentication is required
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
description: Forbidden - Download files permission is required
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'409':
description: A different directory download job is already running
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/api/file-tree/directory-downloads/{run_id}:
get:
tags:
- file-system
summary: Get directory download status
description: Polls a directory download backup run. When the archive is ready, returns a secure temp download link for the authenticated user.
security:
- ApiKeyAuth: []
parameters:
- name: run_id
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: Directory download is ready, failed, or cancelled
content:
application/json:
schema:
$ref: '#/components/schemas/DirectoryDownloadResponse'
'202':
description: Directory compression is still in progress
content:
application/json:
schema:
$ref: '#/components/schemas/DirectoryDownloadResponse'
'401':
description: Unauthorized - Authentication is required
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
description: Forbidden - Download files permission is required
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: Directory download run or archive 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/download/{token}:
get:
tags:
Expand Down Expand Up @@ -4521,6 +4631,51 @@ components:
type: integer
format: int64
description: Number of successful download requests already recorded for this link
DirectoryDownloadResponse:
type: object
required:
- status
- message
- job_id
- run_id
properties:
status:
type: string
enum: [ready, started, in_progress, failed, cancelled]
description: Current directory download state
message:
type: string
description: User-facing status or failure message
job_id:
type: integer
format: int64
description: Backup job used for the directory archive
run_id:
type: integer
format: int64
description: Backup run used for the directory archive
file_id:
type: integer
format: int64
description: Backup run file ID for the generated archive, present when ready
download_url:
type: string
description: Secure temp download URL, present when ready
example: '/api/file-tree/download/01HZX.temp-token'
expires_at:
type: string
format: date-time
description: Link expiration timestamp, present when ready
reused:
type: boolean
description: Whether an existing unexpired secure download link was reused
download_count:
type: integer
format: int64
description: Number of successful download requests already recorded for the secure link
archive_reused:
type: boolean
description: Whether the directory ZIP archive was reused from cache
NPCFileAPIData:
type: object
description: Parsed binary data from an NPC file (API request/response format). All fields are required when used as a request body.
Expand Down Expand Up @@ -6314,6 +6469,10 @@ components:
job_type:
type: string
enum: [file, sql_server]
tag:
type: string
nullable: true
example: directory_download
name:
type: string
status:
Expand Down
2 changes: 1 addition & 1 deletion cmd/omnihance-a3-agent/omnihance-a3-agent-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Omnihance A3 Agent frontend

File-browser and backup output downloads use the backend temp-link flow. Admin and super admin users can create one-day user-bound links; viewers see file-browser download actions but receive the denial toast.
File-browser, directory, and backup output downloads use the backend temp-link flow. Admin and super admin users can create one-day user-bound links; viewers see file-browser download actions but receive the denial toast. Directory downloads first compress the selected directory in the background, then poll and auto-start the secure download when the ZIP is ready.

## Adding Shadcn Components

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ const emptyForm: BackupFormState = {

const backupRunPageSize = 10;
const emptyBackupRuns: BackupRun[] = [];
const directoryDownloadBackupTag = 'directory_download';

export function BackupPage() {
const queryClient = useQueryClient();
Expand Down Expand Up @@ -615,8 +616,8 @@ export function BackupPage() {
<TableCell>
<StatusBadge status={run.status} />
</TableCell>
<TableCell className="capitalize">
{run.trigger_type}
<TableCell>
{formatBackupTriggerLabel(run.trigger_type)}
</TableCell>
<TableCell className="text-right">
<Button
Expand Down Expand Up @@ -1068,9 +1069,13 @@ function PasswordInput({
}

function DetailGrid({ job }: { job: BackupJob }) {
const items: DetailGridItem[] = [
{ label: 'Name', value: job.name },
{ label: 'Destination', value: job.destination_directory },
const items: DetailGridItem[] = [{ label: 'Name', value: job.name }];

if (!isDirectoryDownloadBackupJob(job)) {
items.push({ label: 'Destination', value: job.destination_directory });
}

items.push(
{ label: 'Cron', value: job.cron_expression || 'Manual only' },
{
label: 'Archive Password',
Expand All @@ -1079,7 +1084,7 @@ function DetailGrid({ job }: { job: BackupJob }) {
},
{ label: 'Last Run', value: job.last_run_at, dateTime: true },
{ label: 'Last Updated', value: job.updated_at, dateTime: true },
];
);

if (job.job_type === 'file') {
items.push({ label: 'Input', value: job.source_path || '-' });
Expand Down Expand Up @@ -1220,8 +1225,8 @@ function RunDetailsDialog({
<div className="min-w-0 space-y-4 overflow-y-auto pr-1">
<div className="flex flex-wrap gap-2">
<StatusBadge status={details.run.status} />
<Badge variant="outline" className="capitalize">
{details.run.trigger_type}
<Badge variant="outline">
{formatBackupTriggerLabel(details.run.trigger_type)}
</Badge>
</div>
{details.files.length > 0 && (
Expand Down Expand Up @@ -1335,6 +1340,14 @@ function StatusBadge({ status }: { status: string }) {
return <Badge className={className}>{formatStatusLabel(status)}</Badge>;
}

function isDirectoryDownloadBackupJob(job: BackupJob) {
return job.tag === directoryDownloadBackupTag;
}

function formatBackupTriggerLabel(triggerType: string) {
return formatStatusLabel(triggerType);
}

function jobToForm(job: BackupJob): BackupFormState {
return {
job_type: job.job_type,
Expand Down
Loading
Loading