mirror of
https://github.com/unraid/api.git
synced 2026-02-18 14:08:29 -06:00
feat: backups working
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="4.8.0"
|
||||
version="4.4.1"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox="yes"
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
"remoteName": "FlashBackup",
|
||||
"destinationPath": "backup",
|
||||
"schedule": "0 2 * * *",
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"rcloneOptions": {},
|
||||
"createdAt": "2025-05-24T12:19:29.150Z",
|
||||
"updatedAt": "2025-05-24T12:19:29.150Z"
|
||||
"updatedAt": "2025-05-24T23:54:07.561Z",
|
||||
"lastRunStatus": "Started with job ID: 40",
|
||||
"currentJobId": 40,
|
||||
"lastRunAt": "2025-05-24T23:54:07.561Z"
|
||||
}
|
||||
]
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"demo": "hello.unraider"
|
||||
}
|
||||
@@ -903,6 +903,12 @@ type BackupMutations {
|
||||
|
||||
"""Manually trigger a backup job using existing configuration"""
|
||||
triggerJob(id: PrefixedID!): BackupStatus!
|
||||
|
||||
"""Stop all running backup jobs"""
|
||||
stopAllBackupJobs: BackupStatus!
|
||||
|
||||
"""Forget all finished backup jobs to clean up the job list"""
|
||||
forgetFinishedBackupJobs: BackupStatus!
|
||||
}
|
||||
|
||||
input CreateBackupJobConfigInput {
|
||||
@@ -923,6 +929,7 @@ input UpdateBackupJobConfigInput {
|
||||
schedule: String
|
||||
enabled: Boolean
|
||||
rcloneOptions: JSON
|
||||
lastRunStatus: String
|
||||
}
|
||||
|
||||
input InitiateBackupInput {
|
||||
@@ -1092,9 +1099,121 @@ type RCloneRemote {
|
||||
config: JSON!
|
||||
}
|
||||
|
||||
type RCloneJobStats {
|
||||
"""Bytes transferred"""
|
||||
bytes: Float
|
||||
|
||||
"""Transfer speed in bytes/sec"""
|
||||
speed: Float
|
||||
|
||||
"""Estimated time to completion in seconds"""
|
||||
eta: Float
|
||||
|
||||
"""Elapsed time in seconds"""
|
||||
elapsedTime: Float
|
||||
|
||||
"""Progress percentage (0-100)"""
|
||||
percentage: Float
|
||||
|
||||
"""Number of checks completed"""
|
||||
checks: Float
|
||||
|
||||
"""Number of deletes completed"""
|
||||
deletes: Float
|
||||
|
||||
"""Number of errors encountered"""
|
||||
errors: Float
|
||||
|
||||
"""Whether a fatal error occurred"""
|
||||
fatalError: Boolean
|
||||
|
||||
"""Last error message"""
|
||||
lastError: String
|
||||
|
||||
"""Number of renames completed"""
|
||||
renames: Float
|
||||
|
||||
"""Whether there is a retry error"""
|
||||
retryError: Boolean
|
||||
|
||||
"""Number of server-side copies"""
|
||||
serverSideCopies: Float
|
||||
|
||||
"""Bytes in server-side copies"""
|
||||
serverSideCopyBytes: Float
|
||||
|
||||
"""Number of server-side moves"""
|
||||
serverSideMoves: Float
|
||||
|
||||
"""Bytes in server-side moves"""
|
||||
serverSideMoveBytes: Float
|
||||
|
||||
"""Total bytes to transfer"""
|
||||
totalBytes: Float
|
||||
|
||||
"""Total checks to perform"""
|
||||
totalChecks: Float
|
||||
|
||||
"""Total transfers to perform"""
|
||||
totalTransfers: Float
|
||||
|
||||
"""Time spent transferring in seconds"""
|
||||
transferTime: Float
|
||||
|
||||
"""Number of transfers completed"""
|
||||
transfers: Float
|
||||
|
||||
"""Currently transferring files"""
|
||||
transferring: JSON
|
||||
|
||||
"""Currently checking files"""
|
||||
checking: JSON
|
||||
|
||||
"""Human-readable bytes transferred"""
|
||||
formattedBytes: String
|
||||
|
||||
"""Human-readable transfer speed"""
|
||||
formattedSpeed: String
|
||||
|
||||
"""Human-readable elapsed time"""
|
||||
formattedElapsedTime: String
|
||||
|
||||
"""Human-readable ETA"""
|
||||
formattedEta: String
|
||||
}
|
||||
|
||||
type RCloneJob {
|
||||
"""Job ID"""
|
||||
id: PrefixedID!
|
||||
|
||||
"""RClone group for the job"""
|
||||
group: String
|
||||
|
||||
"""Job status and statistics"""
|
||||
stats: RCloneJobStats
|
||||
|
||||
"""Progress percentage (0-100)"""
|
||||
progressPercentage: Float
|
||||
|
||||
"""Configuration ID that triggered this job"""
|
||||
configId: PrefixedID
|
||||
|
||||
"""Detailed status of the job"""
|
||||
detailedStatus: String
|
||||
|
||||
"""Whether the job is finished"""
|
||||
finished: Boolean
|
||||
|
||||
"""Whether the job was successful"""
|
||||
success: Boolean
|
||||
|
||||
"""Error message if job failed"""
|
||||
error: String
|
||||
}
|
||||
|
||||
type Backup implements Node {
|
||||
id: PrefixedID!
|
||||
jobs(showSystemJobs: Boolean = false): [BackupJob!]!
|
||||
jobs: [RCloneJob!]!
|
||||
configs: [BackupJobConfig!]!
|
||||
|
||||
"""Get the status for the backup service"""
|
||||
@@ -1109,38 +1228,6 @@ type BackupStatus {
|
||||
jobId: String
|
||||
}
|
||||
|
||||
type BackupJob {
|
||||
"""Job ID"""
|
||||
id: PrefixedID!
|
||||
|
||||
"""RClone group for the job"""
|
||||
group: String
|
||||
|
||||
"""Job status and statistics"""
|
||||
stats: JSON!
|
||||
|
||||
"""Formatted bytes transferred"""
|
||||
formattedBytes: String
|
||||
|
||||
"""Formatted transfer speed"""
|
||||
formattedSpeed: String
|
||||
|
||||
"""Formatted elapsed time"""
|
||||
formattedElapsedTime: String
|
||||
|
||||
"""Formatted ETA"""
|
||||
formattedEta: String
|
||||
|
||||
"""Progress percentage (0-100)"""
|
||||
progressPercentage: Float
|
||||
|
||||
"""Configuration ID that triggered this job"""
|
||||
configId: PrefixedID
|
||||
|
||||
"""Detailed status of the job"""
|
||||
detailedStatus: String
|
||||
}
|
||||
|
||||
type BackupJobConfig implements Node {
|
||||
id: PrefixedID!
|
||||
|
||||
@@ -1176,6 +1263,9 @@ type BackupJobConfig implements Node {
|
||||
|
||||
"""Status of last run"""
|
||||
lastRunStatus: String
|
||||
|
||||
"""Current running job ID"""
|
||||
currentJobId: String
|
||||
}
|
||||
|
||||
type BackupJobConfigForm {
|
||||
@@ -1741,7 +1831,7 @@ type Query {
|
||||
backupJobConfig(id: String!): BackupJobConfig
|
||||
|
||||
"""Get status of a specific backup job"""
|
||||
backupJob(jobId: PrefixedID!): BackupJob
|
||||
backupJob(jobId: PrefixedID!): RCloneJob
|
||||
|
||||
"""Get the JSON schema for backup job configuration form"""
|
||||
backupJobConfigForm(input: BackupJobConfigFormInput): BackupJobConfigForm!
|
||||
@@ -1905,7 +1995,7 @@ type Subscription {
|
||||
arraySubscription: UnraidArray!
|
||||
|
||||
"""Subscribe to real-time backup job progress updates"""
|
||||
backupJobProgress(jobId: PrefixedID!): BackupJob
|
||||
backupJobProgress(jobId: PrefixedID!): RCloneJob
|
||||
}
|
||||
|
||||
"""Available authentication action verbs"""
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
RCloneStartBackupInput,
|
||||
UpdateRCloneRemoteDto,
|
||||
} from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
|
||||
import { FormatService } from '@app/unraid-api/utils/format.service.js';
|
||||
|
||||
vi.mock('got');
|
||||
vi.mock('execa');
|
||||
@@ -55,6 +56,8 @@ describe('RCloneApiService', () => {
|
||||
let mockExeca: any;
|
||||
let mockPRetry: any;
|
||||
let mockExistsSync: any;
|
||||
let mockFormatService: FormatService;
|
||||
let mockCacheManager: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
@@ -79,7 +82,19 @@ describe('RCloneApiService', () => {
|
||||
mockPRetry.mockResolvedValue(undefined);
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
service = new RCloneApiService();
|
||||
mockFormatService = {
|
||||
formatBytes: vi.fn(),
|
||||
formatDuration: vi.fn(),
|
||||
} as any;
|
||||
|
||||
// Mock cache manager
|
||||
mockCacheManager = {
|
||||
get: vi.fn().mockResolvedValue(null),
|
||||
set: vi.fn().mockResolvedValue(undefined),
|
||||
del: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
service = new RCloneApiService(mockFormatService, mockCacheManager);
|
||||
await service.onModuleInit();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { existsSync } from 'fs';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from '@app/unraid-api/graph/resolvers/backup/backup.model.js';
|
||||
import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js';
|
||||
|
||||
const JOB_GROUP_PREFIX = 'backup-';
|
||||
|
||||
interface BackupJobConfigData {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -28,10 +30,11 @@ interface BackupJobConfigData {
|
||||
updatedAt: string;
|
||||
lastRunAt?: string;
|
||||
lastRunStatus?: string;
|
||||
currentJobId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BackupConfigService {
|
||||
export class BackupConfigService implements OnModuleInit {
|
||||
private readonly logger = new Logger(BackupConfigService.name);
|
||||
private readonly configPath: string;
|
||||
private configs: Map<string, BackupJobConfigData> = new Map();
|
||||
@@ -42,7 +45,10 @@ export class BackupConfigService {
|
||||
) {
|
||||
const paths = getters.paths();
|
||||
this.configPath = join(paths.backupBase, 'backup-jobs.json');
|
||||
this.loadConfigs();
|
||||
}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
await this.loadConfigs();
|
||||
}
|
||||
|
||||
async createBackupJobConfig(input: CreateBackupJobConfigInput): Promise<BackupJobConfig> {
|
||||
@@ -113,18 +119,19 @@ export class BackupConfigService {
|
||||
this.logger.log(`Executing backup job: ${config.name}`);
|
||||
|
||||
try {
|
||||
const result = await this.rcloneService['rcloneApiService'].startBackup({
|
||||
const result = (await this.rcloneService['rcloneApiService'].startBackup({
|
||||
srcPath: config.sourcePath,
|
||||
dstPath: `${config.remoteName}:${config.destinationPath}`,
|
||||
async: true,
|
||||
group: `backup/${config.id}`,
|
||||
configId: config.id,
|
||||
options: config.rcloneOptions || {},
|
||||
});
|
||||
})) as { jobId?: string; jobid?: string };
|
||||
|
||||
const jobId = result.jobId || result.jobid;
|
||||
|
||||
config.lastRunAt = new Date().toISOString();
|
||||
config.lastRunStatus = `Started with job ID: ${jobId}`;
|
||||
config.currentJobId = jobId;
|
||||
this.configs.set(config.id, config);
|
||||
await this.saveConfigs();
|
||||
|
||||
@@ -133,6 +140,7 @@ export class BackupConfigService {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
config.lastRunAt = new Date().toISOString();
|
||||
config.lastRunStatus = `Failed: ${errorMessage}`;
|
||||
config.currentJobId = undefined;
|
||||
this.configs.set(config.id, config);
|
||||
await this.saveConfigs();
|
||||
|
||||
@@ -150,7 +158,7 @@ export class BackupConfigService {
|
||||
'UTC'
|
||||
);
|
||||
|
||||
this.schedulerRegistry.addCronJob(`backup-${config.id}`, job);
|
||||
this.schedulerRegistry.addCronJob(`${JOB_GROUP_PREFIX}${config.id}`, job);
|
||||
job.start();
|
||||
this.logger.log(`Scheduled backup job: ${config.name} with schedule: ${config.schedule}`);
|
||||
} catch (error) {
|
||||
@@ -160,22 +168,42 @@ export class BackupConfigService {
|
||||
|
||||
private unscheduleJob(id: string): void {
|
||||
try {
|
||||
const jobName = `backup-${id}`;
|
||||
const jobName = `${JOB_GROUP_PREFIX}${id}`;
|
||||
if (this.schedulerRegistry.doesExist('cron', jobName)) {
|
||||
this.schedulerRegistry.deleteCronJob(jobName);
|
||||
this.logger.log(`Unscheduled backup job: ${id}`);
|
||||
} else {
|
||||
this.logger.debug(`No existing cron job found to unschedule for backup job: ${id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to unschedule backup job ${id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
addCronJob(name: string, seconds: string) {
|
||||
const job = new CronJob(`${seconds} * * * * *`, () => {
|
||||
this.logger.warn(`time (${seconds}) for job ${name} to run!`);
|
||||
});
|
||||
|
||||
this.schedulerRegistry.addCronJob(name, job);
|
||||
job.start();
|
||||
|
||||
this.logger.warn(`job ${name} added for each minute at ${seconds} seconds!`);
|
||||
}
|
||||
|
||||
private async loadConfigs(): Promise<void> {
|
||||
try {
|
||||
if (existsSync(this.configPath)) {
|
||||
const data = await readFile(this.configPath, 'utf-8');
|
||||
const configs: BackupJobConfigData[] = JSON.parse(data);
|
||||
|
||||
// First, unschedule any existing jobs before clearing the config map
|
||||
this.configs.forEach((config) => {
|
||||
if (config.enabled) {
|
||||
this.unscheduleJob(config.id);
|
||||
}
|
||||
});
|
||||
|
||||
this.configs.clear();
|
||||
configs.forEach((config) => {
|
||||
this.configs.set(config.id, config);
|
||||
@@ -214,6 +242,7 @@ export class BackupConfigService {
|
||||
updatedAt: new Date(config.updatedAt),
|
||||
lastRunAt: config.lastRunAt ? new Date(config.lastRunAt) : undefined,
|
||||
lastRunStatus: config.lastRunStatus,
|
||||
currentJobId: config.currentJobId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,20 +33,18 @@ export class BackupMutationsResolver {
|
||||
remoteName: string,
|
||||
destinationPath: string,
|
||||
options: Record<string, any> = {},
|
||||
group: string
|
||||
configId?: string
|
||||
): Promise<BackupStatus> {
|
||||
try {
|
||||
this.logger.log(
|
||||
`Executing backup: ${sourcePath} -> ${remoteName}:${destinationPath} (group: ${group})`
|
||||
);
|
||||
this.logger.log(`Executing backup: ${sourcePath} -> ${remoteName}:${destinationPath}`);
|
||||
|
||||
const result = await this.rcloneService['rcloneApiService'].startBackup({
|
||||
const result = (await this.rcloneService['rcloneApiService'].startBackup({
|
||||
srcPath: sourcePath,
|
||||
dstPath: `${remoteName}:${destinationPath}`,
|
||||
async: true,
|
||||
group: group,
|
||||
configId: configId,
|
||||
options: options,
|
||||
});
|
||||
})) as { jobId?: string; jobid?: string };
|
||||
|
||||
this.logger.debug(`RClone startBackup result: ${JSON.stringify(result)}`);
|
||||
|
||||
@@ -126,8 +124,7 @@ export class BackupMutationsResolver {
|
||||
input.sourcePath,
|
||||
input.remoteName,
|
||||
input.destinationPath,
|
||||
input.options || {},
|
||||
'backup/manual'
|
||||
input.options || {}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,12 +163,91 @@ export class BackupMutationsResolver {
|
||||
};
|
||||
}
|
||||
|
||||
return this.executeBackup(
|
||||
const result = await this.executeBackup(
|
||||
config.sourcePath,
|
||||
config.remoteName,
|
||||
config.destinationPath,
|
||||
config.rcloneOptions || {},
|
||||
`backup/${id}`
|
||||
config.id
|
||||
);
|
||||
|
||||
// Store the job ID in the config if successful
|
||||
if (result.jobId) {
|
||||
await this.backupConfigService.updateBackupJobConfig(id, {
|
||||
lastRunStatus: `Started with job ID: ${result.jobId}`,
|
||||
});
|
||||
|
||||
// Update the currentJobId in the config
|
||||
const configData = this.backupConfigService['configs'].get(id);
|
||||
if (configData) {
|
||||
configData.currentJobId = result.jobId;
|
||||
configData.lastRunAt = new Date().toISOString();
|
||||
this.backupConfigService['configs'].set(id, configData);
|
||||
await this.backupConfigService['saveConfigs']();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ResolveField(() => BackupStatus, {
|
||||
description: 'Stop all running backup jobs',
|
||||
})
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.DELETE,
|
||||
resource: Resource.BACKUP,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
async stopAllBackupJobs(): Promise<BackupStatus> {
|
||||
try {
|
||||
const result = await this.rcloneService['rcloneApiService'].stopAllJobs();
|
||||
const stoppedCount = result.stopped.length;
|
||||
const errorCount = result.errors.length;
|
||||
|
||||
if (stoppedCount > 0) {
|
||||
this.logger.log(`Stopped ${stoppedCount} backup jobs`);
|
||||
}
|
||||
|
||||
if (errorCount > 0) {
|
||||
this.logger.warn(`Failed operations on ${errorCount} jobs: ${result.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: `Stopped ${stoppedCount} jobs${errorCount > 0 ? `, ${errorCount} errors` : ''}`,
|
||||
jobId: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Failed to stop backup jobs: ${errorMessage}`);
|
||||
return {
|
||||
status: `Failed to stop backup jobs: ${errorMessage}`,
|
||||
jobId: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => BackupStatus, {
|
||||
description: 'Forget all finished backup jobs to clean up the job list',
|
||||
})
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.DELETE,
|
||||
resource: Resource.BACKUP,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
async forgetFinishedBackupJobs(): Promise<BackupStatus> {
|
||||
try {
|
||||
this.logger.log('Forgetting finished backup jobs is handled automatically by RClone');
|
||||
return {
|
||||
status: 'Finished jobs are automatically cleaned up by RClone',
|
||||
jobId: undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
this.logger.error(`Failed to forget finished backup jobs: ${errorMessage}`);
|
||||
return {
|
||||
status: `Failed to forget finished backup jobs: ${errorMessage}`,
|
||||
jobId: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,9 @@ export class BackupJobConfig extends Node {
|
||||
|
||||
@Field(() => String, { description: 'Status of last run', nullable: true })
|
||||
lastRunStatus?: string;
|
||||
|
||||
@Field(() => String, { description: 'Current running job ID', nullable: true })
|
||||
currentJobId?: string;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
@@ -199,6 +202,11 @@ export class UpdateBackupJobConfigInput {
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
rcloneOptions?: Record<string, unknown>;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastRunStatus?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
@@ -4,12 +4,11 @@ import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { BackupConfigService } from '@app/unraid-api/graph/resolvers/backup/backup-config.service.js';
|
||||
import { BackupMutationsResolver } from '@app/unraid-api/graph/resolvers/backup/backup-mutations.resolver.js';
|
||||
import { BackupResolver } from '@app/unraid-api/graph/resolvers/backup/backup.resolver.js';
|
||||
import { FormatService } from '@app/unraid-api/graph/resolvers/backup/format.service.js';
|
||||
import { RCloneModule } from '@app/unraid-api/graph/resolvers/rclone/rclone.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [RCloneModule, ScheduleModule.forRoot()],
|
||||
providers: [BackupResolver, BackupMutationsResolver, BackupConfigService, FormatService],
|
||||
providers: [BackupResolver, BackupMutationsResolver, BackupConfigService],
|
||||
exports: [],
|
||||
})
|
||||
export class BackupModule {}
|
||||
|
||||
@@ -5,17 +5,18 @@ import { pubsub } from '@app/core/pubsub.js';
|
||||
import { BackupConfigService } from '@app/unraid-api/graph/resolvers/backup/backup-config.service.js';
|
||||
import {
|
||||
Backup,
|
||||
BackupJob,
|
||||
BackupJobConfig,
|
||||
BackupJobConfigForm,
|
||||
BackupJobConfigFormInput,
|
||||
BackupStatus,
|
||||
} from '@app/unraid-api/graph/resolvers/backup/backup.model.js';
|
||||
import { FormatService } from '@app/unraid-api/graph/resolvers/backup/format.service.js';
|
||||
import { buildBackupJobConfigSchema } from '@app/unraid-api/graph/resolvers/backup/jsonforms/backup-jsonforms-config.js';
|
||||
import { RCloneJob } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
|
||||
import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js';
|
||||
import { PrefixedID } from '@app/unraid-api/graph/scalars/graphql-type-prefixed-id.js';
|
||||
import { FormatService } from '@app/unraid-api/utils/format.service.js';
|
||||
|
||||
const JOB_GROUP_PREFIX = 'backup-';
|
||||
|
||||
@Resolver(() => Backup)
|
||||
export class BackupResolver {
|
||||
@@ -38,14 +39,11 @@ export class BackupResolver {
|
||||
};
|
||||
}
|
||||
|
||||
@ResolveField(() => [BackupJob], {
|
||||
description: 'Get all running backup jobs',
|
||||
@ResolveField(() => [RCloneJob], {
|
||||
description: 'Get all running jobs (filtering should be done on frontend)',
|
||||
})
|
||||
async jobs(
|
||||
@Args('showSystemJobs', { type: () => Boolean, nullable: true, defaultValue: false })
|
||||
showSystemJobs?: boolean
|
||||
): Promise<BackupJob[]> {
|
||||
return this.backupJobs(showSystemJobs);
|
||||
async jobs(): Promise<RCloneJob[]> {
|
||||
return this.backupJobs();
|
||||
}
|
||||
|
||||
@ResolveField(() => [BackupJobConfig], {
|
||||
@@ -63,22 +61,28 @@ export class BackupResolver {
|
||||
return this.backupConfigService.getBackupJobConfig(id);
|
||||
}
|
||||
|
||||
@Query(() => BackupJob, {
|
||||
@Query(() => RCloneJob, {
|
||||
description: 'Get status of a specific backup job',
|
||||
nullable: true,
|
||||
})
|
||||
async backupJob(
|
||||
@Args('jobId', { type: () => PrefixedID }) jobId: string
|
||||
): Promise<BackupJob | null> {
|
||||
): Promise<RCloneJob | null> {
|
||||
try {
|
||||
const status = await this.rcloneService['rcloneApiService'].getJobStatus({ jobId });
|
||||
console.log(status);
|
||||
return {
|
||||
id: jobId,
|
||||
group: status.group || '',
|
||||
stats: status,
|
||||
group: status.group || undefined,
|
||||
stats: status.stats,
|
||||
finished: status.finished,
|
||||
success: status.success,
|
||||
error: status.error || undefined,
|
||||
progressPercentage: status.stats?.percentage,
|
||||
detailedStatus: status.error ? 'Error' : status.finished ? 'Completed' : 'Running',
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to fetch backup job ${jobId}:`, error);
|
||||
this.logger.error(`Failed to fetch backup job ${jobId}: %o`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -114,7 +118,7 @@ export class BackupResolver {
|
||||
};
|
||||
}
|
||||
|
||||
@Subscription(() => BackupJob, {
|
||||
@Subscription(() => RCloneJob, {
|
||||
description: 'Subscribe to real-time backup job progress updates',
|
||||
nullable: true,
|
||||
})
|
||||
@@ -122,65 +126,17 @@ export class BackupResolver {
|
||||
return pubsub.asyncIterableIterator(`BACKUP_JOB_PROGRESS:${jobId}`);
|
||||
}
|
||||
|
||||
private async backupJobs(showSystemJobs: boolean = false): Promise<RCloneJob[]> {
|
||||
private async backupJobs(): Promise<RCloneJob[]> {
|
||||
try {
|
||||
this.logger.debug(`backupJobs called with showSystemJobs: ${showSystemJobs}`);
|
||||
this.logger.debug('backupJobs called - returning all jobs for frontend filtering');
|
||||
|
||||
let jobs;
|
||||
if (showSystemJobs) {
|
||||
// Get all jobs when showing system jobs
|
||||
jobs = await this.rcloneService['rcloneApiService'].getAllJobsWithStats();
|
||||
this.logger.debug(`All jobs with stats: ${JSON.stringify(jobs)}`);
|
||||
} else {
|
||||
// Get only backup jobs with enhanced stats when not showing system jobs
|
||||
jobs = await this.rcloneService['rcloneApiService'].getBackupJobsWithStats();
|
||||
this.logger.debug(`Backup jobs with enhanced stats: ${JSON.stringify(jobs)}`);
|
||||
}
|
||||
const jobs = (await this.rcloneService['rcloneApiService'].getAllJobsWithStats()).filter(
|
||||
(job) => job.group?.startsWith(JOB_GROUP_PREFIX)
|
||||
);
|
||||
|
||||
// Filter and map jobs
|
||||
const allJobs =
|
||||
jobs.jobids?.map((jobId: string | number, index: number) => {
|
||||
const stats = jobs.stats?.[index] || {};
|
||||
const group = stats.group || '';
|
||||
this.logger.debug(`Returning ${jobs.length} jobs total for frontend filtering`);
|
||||
|
||||
this.logger.debug(
|
||||
`Processing job ${jobId}: group="${group}", stats keys: [${Object.keys(stats).join(', ')}]`
|
||||
);
|
||||
|
||||
return {
|
||||
id: String(jobId),
|
||||
group: group,
|
||||
stats,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
this.logger.debug(`Mapped ${allJobs.length} jobs total`);
|
||||
|
||||
// Log all job groups for analysis
|
||||
const jobGroupSummary = allJobs.map((job) => ({ id: job.id, group: job.group }));
|
||||
this.logger.debug(`All job groups: ${JSON.stringify(jobGroupSummary)}`);
|
||||
|
||||
// Filter based on showSystemJobs flag
|
||||
if (showSystemJobs) {
|
||||
this.logger.debug(`Returning all ${allJobs.length} jobs (showSystemJobs=true)`);
|
||||
return allJobs;
|
||||
} else {
|
||||
// When not showing system jobs, we already filtered to backup jobs in getBackupJobsWithStats
|
||||
// But let's double-check the filtering for safety
|
||||
const filteredJobs = allJobs.filter((job) => job.group.startsWith('backup/'));
|
||||
this.logger.debug(
|
||||
`Filtered to ${filteredJobs.length} backup jobs (group starts with 'backup/')`
|
||||
);
|
||||
|
||||
const nonBackupJobs = allJobs.filter((job) => !job.group.startsWith('backup/'));
|
||||
if (nonBackupJobs.length > 0) {
|
||||
this.logger.debug(
|
||||
`Excluded ${nonBackupJobs.length} non-backup jobs: ${JSON.stringify(nonBackupJobs.map((j) => ({ id: j.id, group: j.group })))}`
|
||||
);
|
||||
}
|
||||
|
||||
return filteredJobs;
|
||||
}
|
||||
return jobs;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to fetch backup jobs:', error);
|
||||
return [];
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { convert } from 'convert';
|
||||
|
||||
@Injectable()
|
||||
export class FormatService {
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const result = convert(bytes, 'bytes').to('best');
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
formatDuration(seconds: number): string {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
|
||||
const result = convert(seconds, 'seconds').to('best');
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,16 @@ import got, { HTTPError } from 'got';
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
import { sanitizeParams } from '@app/core/log.js';
|
||||
import { FormatService } from '@app/unraid-api/graph/resolvers/backup/format.service.js';
|
||||
import { RCloneStatusService } from '@app/unraid-api/graph/resolvers/rclone/rclone-status.service.js';
|
||||
import {
|
||||
CreateRCloneRemoteDto,
|
||||
DeleteRCloneRemoteDto,
|
||||
GetRCloneJobStatusDto,
|
||||
GetRCloneRemoteConfigDto,
|
||||
GetRCloneRemoteDetailsDto,
|
||||
RCloneJob,
|
||||
RCloneJobListResponse,
|
||||
RCloneJobStats,
|
||||
RCloneJobStatusResponse,
|
||||
RCloneJobsWithStatsResponse,
|
||||
RCloneJobWithStats,
|
||||
RCloneProviderOptionResponse,
|
||||
RCloneProviderResponse,
|
||||
RCloneRemoteConfig,
|
||||
RCloneStartBackupInput,
|
||||
@@ -30,6 +27,50 @@ import {
|
||||
} from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
|
||||
import { validateObject } from '@app/unraid-api/graph/resolvers/validation.utils.js';
|
||||
|
||||
// Internal interface for job status response from RClone API
|
||||
interface RCloneJobStatusResponse {
|
||||
id?: string | number;
|
||||
group?: string;
|
||||
stats?: RCloneJobStats;
|
||||
finished?: boolean;
|
||||
error?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface BackupStatusResult {
|
||||
isRunning: boolean;
|
||||
stats: RCloneJobStats | null;
|
||||
jobCount: number;
|
||||
activeJobs: RCloneJobStatusResponse[];
|
||||
}
|
||||
|
||||
interface JobOperationResult {
|
||||
stopped: string[];
|
||||
forgotten?: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
const CONSTANTS = {
|
||||
RETRY_CONFIG: {
|
||||
retries: 6,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 5000,
|
||||
factor: 2,
|
||||
maxRetryTime: 30000,
|
||||
},
|
||||
TIMEOUTS: {
|
||||
GRACEFUL_SHUTDOWN: 2000,
|
||||
PROCESS_CLEANUP: 1000,
|
||||
STOP_JOBS_WAIT: 1000,
|
||||
},
|
||||
LOG_LEVEL: {
|
||||
DEBUG: 'DEBUG',
|
||||
INFO: 'INFO',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const JOB_GROUP_PREFIX = 'backup-';
|
||||
|
||||
@Injectable()
|
||||
export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
private isInitialized: boolean = false;
|
||||
@@ -41,40 +82,12 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
process.env.RCLONE_USERNAME || crypto.randomBytes(12).toString('base64');
|
||||
private readonly rclonePassword: string =
|
||||
process.env.RCLONE_PASSWORD || crypto.randomBytes(24).toString('base64');
|
||||
constructor(private readonly formatService: FormatService) {}
|
||||
|
||||
constructor(private readonly statusService: RCloneStatusService) {}
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
try {
|
||||
const { getters } = await import('@app/store/index.js');
|
||||
this.rcloneSocketPath = getters.paths()['rclone-socket'];
|
||||
const logFilePath = join(getters.paths()['log-base'], 'rclone-unraid-api.log');
|
||||
this.logger.log(`RClone socket path: ${this.rcloneSocketPath}`);
|
||||
this.logger.log(`RClone log file path: ${logFilePath}`);
|
||||
|
||||
this.rcloneBaseUrl = `http://unix:${this.rcloneSocketPath}:`;
|
||||
|
||||
const socketExists = await this.checkRcloneSocketExists(this.rcloneSocketPath);
|
||||
|
||||
if (socketExists) {
|
||||
const isRunning = await this.checkRcloneSocketRunning();
|
||||
if (isRunning) {
|
||||
this.isInitialized = true;
|
||||
return;
|
||||
} else {
|
||||
this.logger.warn(
|
||||
'RClone socket is not running but socket exists, removing socket before starting...'
|
||||
);
|
||||
await rm(this.rcloneSocketPath, { force: true });
|
||||
}
|
||||
|
||||
this.logger.warn('RClone socket is not running, starting it...');
|
||||
this.isInitialized = await this.startRcloneSocket(this.rcloneSocketPath, logFilePath);
|
||||
return;
|
||||
} else {
|
||||
this.logger.warn('RClone socket does not exist, creating it...');
|
||||
this.isInitialized = await this.startRcloneSocket(this.rcloneSocketPath, logFilePath);
|
||||
return;
|
||||
}
|
||||
await this.initializeRCloneService();
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error initializing RCloneApiService: ${error}`);
|
||||
this.isInitialized = false;
|
||||
@@ -86,92 +99,144 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
this.logger.log('RCloneApiService module destroyed');
|
||||
}
|
||||
|
||||
private async initializeRCloneService(): Promise<void> {
|
||||
const { getters } = await import('@app/store/index.js');
|
||||
this.rcloneSocketPath = getters.paths()['rclone-socket'];
|
||||
const logFilePath = join(getters.paths()['log-base'], 'rclone-unraid-api.log');
|
||||
|
||||
this.rcloneBaseUrl = `http://unix:${this.rcloneSocketPath}:`;
|
||||
this.logger.log(`RClone socket path: ${this.rcloneSocketPath}`);
|
||||
|
||||
const socketExists = await this.checkRcloneSocketExists(this.rcloneSocketPath);
|
||||
|
||||
if (socketExists && (await this.checkRcloneSocketRunning())) {
|
||||
this.isInitialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (socketExists) {
|
||||
this.logger.warn('RClone socket exists but not running, removing before restart...');
|
||||
await rm(this.rcloneSocketPath, { force: true });
|
||||
}
|
||||
|
||||
this.logger.warn('Starting new RClone socket...');
|
||||
await this.killExistingRcloneProcesses();
|
||||
this.isInitialized = await this.startRcloneSocket(this.rcloneSocketPath, logFilePath);
|
||||
}
|
||||
|
||||
private async startRcloneSocket(socketPath: string, logFilePath: string): Promise<boolean> {
|
||||
try {
|
||||
if (!existsSync(logFilePath)) {
|
||||
await mkdir(dirname(logFilePath), { recursive: true });
|
||||
await writeFile(logFilePath, '', 'utf-8');
|
||||
}
|
||||
await this.ensureLogFileExists(logFilePath);
|
||||
|
||||
const rcloneArgs = this.buildRcloneArgs(socketPath, logFilePath);
|
||||
this.logger.log(`Starting RClone RC daemon on socket: ${socketPath}`);
|
||||
|
||||
this.rcloneProcess = execa(
|
||||
'rclone',
|
||||
[
|
||||
'rcd',
|
||||
'--rc-addr',
|
||||
socketPath,
|
||||
'--log-level',
|
||||
'INFO',
|
||||
'--log-file',
|
||||
logFilePath,
|
||||
...(this.rcloneUsername ? ['--rc-user', this.rcloneUsername] : []),
|
||||
...(this.rclonePassword ? ['--rc-pass', this.rclonePassword] : []),
|
||||
],
|
||||
{ detached: false }
|
||||
);
|
||||
|
||||
this.rcloneProcess.on('error', (error: Error) => {
|
||||
this.logger.error(`RClone process failed to start: ${error.message}`);
|
||||
this.rcloneProcess = null;
|
||||
this.isInitialized = false;
|
||||
});
|
||||
|
||||
this.rcloneProcess.on('exit', (code, signal) => {
|
||||
this.logger.warn(
|
||||
`RClone process exited unexpectedly with code: ${code}, signal: ${signal}`
|
||||
);
|
||||
this.rcloneProcess = null;
|
||||
this.isInitialized = false;
|
||||
});
|
||||
|
||||
await pRetry(
|
||||
async () => {
|
||||
const isRunning = await this.checkRcloneSocketRunning();
|
||||
if (!isRunning) throw new Error('Rclone socket not ready');
|
||||
},
|
||||
{
|
||||
retries: 6,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 5000,
|
||||
factor: 2,
|
||||
maxRetryTime: 30000,
|
||||
}
|
||||
);
|
||||
this.rcloneProcess = execa('rclone', rcloneArgs, { detached: false });
|
||||
this.setupProcessListeners();
|
||||
|
||||
await this.waitForSocketReady();
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error starting RClone RC daemon: ${error}`);
|
||||
this.rcloneProcess?.kill();
|
||||
this.rcloneProcess = null;
|
||||
this.cleanupFailedProcess();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async stopRcloneSocket(): Promise<void> {
|
||||
if (this.rcloneProcess && !this.rcloneProcess.killed) {
|
||||
this.logger.log(`Stopping RClone RC daemon process (PID: ${this.rcloneProcess.pid})...`);
|
||||
try {
|
||||
const killed = this.rcloneProcess.kill('SIGTERM');
|
||||
if (!killed) {
|
||||
this.logger.warn('Failed to kill RClone process with SIGTERM, trying SIGKILL.');
|
||||
this.rcloneProcess.kill('SIGKILL');
|
||||
}
|
||||
this.logger.log('RClone process stopped.');
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error stopping RClone process: ${error}`);
|
||||
} finally {
|
||||
this.rcloneProcess = null;
|
||||
}
|
||||
} else {
|
||||
this.logger.log('RClone process not running or already stopped.');
|
||||
private async ensureLogFileExists(logFilePath: string): Promise<void> {
|
||||
if (!existsSync(logFilePath)) {
|
||||
await mkdir(dirname(logFilePath), { recursive: true });
|
||||
await writeFile(logFilePath, '', 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
private buildRcloneArgs(socketPath: string, logFilePath: string): string[] {
|
||||
const enableDebugMode = true;
|
||||
const enableRcServe = true;
|
||||
const logLevel = enableDebugMode ? CONSTANTS.LOG_LEVEL.DEBUG : CONSTANTS.LOG_LEVEL.INFO;
|
||||
|
||||
const args = [
|
||||
'rcd',
|
||||
'--rc-addr',
|
||||
socketPath,
|
||||
'--log-level',
|
||||
logLevel,
|
||||
'--log-file',
|
||||
logFilePath,
|
||||
];
|
||||
|
||||
if (this.rcloneUsername) args.push('--rc-user', this.rcloneUsername);
|
||||
if (this.rclonePassword) args.push('--rc-pass', this.rclonePassword);
|
||||
if (enableRcServe) args.push('--rc-serve');
|
||||
|
||||
if (enableDebugMode) {
|
||||
this.logger.log('Debug mode: Enhanced logging and RC serve enabled');
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private setupProcessListeners(): void {
|
||||
if (!this.rcloneProcess) return;
|
||||
|
||||
this.rcloneProcess.on('error', (error: Error) => {
|
||||
this.logger.error(`RClone process failed to start: ${error.message}`);
|
||||
this.cleanupFailedProcess();
|
||||
});
|
||||
|
||||
this.rcloneProcess.on('exit', (code, signal) => {
|
||||
this.logger.warn(`RClone process exited unexpectedly with code: ${code}, signal: ${signal}`);
|
||||
this.cleanupFailedProcess();
|
||||
});
|
||||
}
|
||||
|
||||
private cleanupFailedProcess(): void {
|
||||
this.rcloneProcess = null;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
private async waitForSocketReady(): Promise<void> {
|
||||
await pRetry(async () => {
|
||||
const isRunning = await this.checkRcloneSocketRunning();
|
||||
if (!isRunning) throw new Error('Rclone socket not ready');
|
||||
}, CONSTANTS.RETRY_CONFIG);
|
||||
}
|
||||
|
||||
private async stopRcloneSocket(): Promise<void> {
|
||||
if (this.rcloneProcess && !this.rcloneProcess.killed) {
|
||||
await this.terminateProcess();
|
||||
}
|
||||
|
||||
await this.killExistingRcloneProcesses();
|
||||
await this.removeSocketFile();
|
||||
}
|
||||
|
||||
private async terminateProcess(): Promise<void> {
|
||||
if (!this.rcloneProcess) return;
|
||||
|
||||
this.logger.log(`Stopping RClone RC daemon process (PID: ${this.rcloneProcess.pid})...`);
|
||||
|
||||
try {
|
||||
const killed = this.rcloneProcess.kill('SIGTERM');
|
||||
if (!killed) {
|
||||
this.logger.warn('Failed to kill with SIGTERM, using SIGKILL');
|
||||
this.rcloneProcess.kill('SIGKILL');
|
||||
}
|
||||
this.logger.log('RClone process stopped');
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error stopping RClone process: ${error}`);
|
||||
} finally {
|
||||
this.rcloneProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async removeSocketFile(): Promise<void> {
|
||||
if (this.rcloneSocketPath && existsSync(this.rcloneSocketPath)) {
|
||||
this.logger.log(`Removing RClone socket file: ${this.rcloneSocketPath}`);
|
||||
try {
|
||||
await rm(this.rcloneSocketPath, { force: true });
|
||||
} catch (error: unknown) {
|
||||
this.logger.error(`Error removing RClone socket file: ${error}`);
|
||||
this.logger.error(`Error removing socket file: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,16 +245,15 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
const socketExists = existsSync(socketPath);
|
||||
if (!socketExists) {
|
||||
this.logger.warn(`RClone socket does not exist at: ${socketPath}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return socketExists;
|
||||
}
|
||||
|
||||
private async checkRcloneSocketRunning(): Promise<boolean> {
|
||||
try {
|
||||
await this.callRcloneApi('core/pid');
|
||||
await this.callRcloneApi('rc/noop');
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -208,8 +272,7 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
async getRemoteDetails(input: GetRCloneRemoteDetailsDto): Promise<RCloneRemoteConfig> {
|
||||
await validateObject(GetRCloneRemoteDetailsDto, input);
|
||||
const config = (await this.getRemoteConfig({ name: input.name })) || {};
|
||||
return config as RCloneRemoteConfig;
|
||||
return this.getRemoteConfig({ name: input.name });
|
||||
}
|
||||
|
||||
async getRemoteConfig(input: GetRCloneRemoteConfigDto): Promise<RCloneRemoteConfig> {
|
||||
@@ -217,249 +280,254 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
return this.callRcloneApi('config/get', { name: input.name });
|
||||
}
|
||||
|
||||
async createRemote(input: CreateRCloneRemoteDto): Promise<any> {
|
||||
async createRemote(input: CreateRCloneRemoteDto): Promise<unknown> {
|
||||
await validateObject(CreateRCloneRemoteDto, input);
|
||||
this.logger.log(`Creating new remote: ${input.name} of type: ${input.type}`);
|
||||
const params = {
|
||||
this.logger.log(`Creating remote: ${input.name} (${input.type})`);
|
||||
|
||||
const result = await this.callRcloneApi('config/create', {
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
parameters: input.parameters,
|
||||
};
|
||||
const result = await this.callRcloneApi('config/create', params);
|
||||
});
|
||||
|
||||
this.logger.log(`Successfully created remote: ${input.name}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateRemote(input: UpdateRCloneRemoteDto): Promise<any> {
|
||||
async updateRemote(input: UpdateRCloneRemoteDto): Promise<unknown> {
|
||||
await validateObject(UpdateRCloneRemoteDto, input);
|
||||
this.logger.log(`Updating remote: ${input.name}`);
|
||||
const params = {
|
||||
|
||||
return this.callRcloneApi('config/update', {
|
||||
name: input.name,
|
||||
...input.parameters,
|
||||
};
|
||||
return this.callRcloneApi('config/update', params);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteRemote(input: DeleteRCloneRemoteDto): Promise<any> {
|
||||
async deleteRemote(input: DeleteRCloneRemoteDto): Promise<unknown> {
|
||||
await validateObject(DeleteRCloneRemoteDto, input);
|
||||
this.logger.log(`Deleting remote: ${input.name}`);
|
||||
return this.callRcloneApi('config/delete', { name: input.name });
|
||||
}
|
||||
|
||||
async startBackup(input: RCloneStartBackupInput): Promise<any> {
|
||||
async startBackup(input: RCloneStartBackupInput): Promise<unknown> {
|
||||
await validateObject(RCloneStartBackupInput, input);
|
||||
this.logger.log(
|
||||
`Starting backup from ${input.srcPath} to ${input.dstPath} with group: ${input.group}`
|
||||
);
|
||||
|
||||
this.logger.log(`Starting backup: ${input.srcPath} → ${input.dstPath}`);
|
||||
|
||||
const group = input.configId
|
||||
? `${JOB_GROUP_PREFIX}${input.configId}`
|
||||
: JOB_GROUP_PREFIX + 'manual';
|
||||
|
||||
const params = {
|
||||
srcFs: input.srcPath,
|
||||
dstFs: input.dstPath,
|
||||
...(input.async && { _async: input.async }),
|
||||
...(input.group && { _group: input.group }),
|
||||
_group: group,
|
||||
...(input.options || {}),
|
||||
};
|
||||
|
||||
const result = await this.callRcloneApi('sync/copy', params);
|
||||
|
||||
this.logger.log(
|
||||
`Backup job created with ID: ${result.jobid || result.jobId || 'unknown'}, group: ${input.group}`
|
||||
);
|
||||
const jobId = result.jobid || result.jobId || 'unknown';
|
||||
this.logger.log(`Backup job created with ID: ${jobId} in group: ${group}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getJobStatus(input: GetRCloneJobStatusDto): Promise<RCloneJobStatusResponse> {
|
||||
async getJobStatus(input: GetRCloneJobStatusDto): Promise<RCloneJob> {
|
||||
await validateObject(GetRCloneJobStatusDto, input);
|
||||
|
||||
const result = await this.callRcloneApi('job/status', { jobid: input.jobId });
|
||||
|
||||
if (result.error) {
|
||||
this.logger.warn(`Job ${input.jobId} has error: ${result.error}`);
|
||||
}
|
||||
|
||||
if (!result.stats && result.group) {
|
||||
// If the jobId looks like a group name (starts with backup-), get group stats
|
||||
if (input.jobId.startsWith(JOB_GROUP_PREFIX)) {
|
||||
try {
|
||||
const groupStats = await this.getGroupStats(result.group);
|
||||
if (groupStats && typeof groupStats === 'object') {
|
||||
result.stats = { ...groupStats };
|
||||
}
|
||||
} catch (groupError) {
|
||||
this.logger.warn(`Failed to get group stats for job ${input.jobId}: ${groupError}`);
|
||||
const stats = await this.callRcloneApi('core/stats', { group: input.jobId });
|
||||
const enhancedStats = this.statusService.enhanceStatsWithFormattedFields({
|
||||
...stats,
|
||||
group: input.jobId,
|
||||
});
|
||||
|
||||
const job = this.statusService.transformStatsToJob(input.jobId, enhancedStats);
|
||||
job.configId = input.jobId.substring(JOB_GROUP_PREFIX.length);
|
||||
return job;
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get group stats for ${input.jobId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (result.stats) {
|
||||
result.stats = this.enhanceStatsWithFormattedFields(result.stats);
|
||||
// Fallback to individual job status
|
||||
const jobStatus = await this.getIndividualJobStatus(input.jobId);
|
||||
return this.statusService.parseJobWithStats(input.jobId, jobStatus);
|
||||
}
|
||||
|
||||
async getIndividualJobStatus(jobId: string): Promise<RCloneJobStatusResponse> {
|
||||
this.logger.debug(`Fetching status for job ${jobId}`);
|
||||
const result = await this.callRcloneApi('job/status', { jobid: jobId });
|
||||
|
||||
if (result.error) {
|
||||
this.logger.warn(`Job ${jobId} has error: ${result.error}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async listRunningJobs(): Promise<RCloneJobListResponse> {
|
||||
const result = await this.callRcloneApi('job/list');
|
||||
return result;
|
||||
this.logger.debug('Fetching job list from RClone API');
|
||||
return this.callRcloneApi('job/list');
|
||||
}
|
||||
|
||||
async getGroupStats(group: string): Promise<any> {
|
||||
const result = await this.callRcloneApi('core/stats', { group });
|
||||
return result;
|
||||
}
|
||||
async getAllJobsWithStats(): Promise<RCloneJob[]> {
|
||||
try {
|
||||
// Get both the job list and group list
|
||||
const [runningJobs, groupList] = await Promise.all([
|
||||
this.listRunningJobs(),
|
||||
this.callRcloneApi('core/group-list'),
|
||||
]);
|
||||
|
||||
async getBackupJobsWithStats(): Promise<RCloneJobsWithStatsResponse> {
|
||||
const jobList = await this.listRunningJobs();
|
||||
this.logger.debug(`Running jobs: ${JSON.stringify(runningJobs)}`);
|
||||
this.logger.debug(`Group list: ${JSON.stringify(groupList)}`);
|
||||
|
||||
if (!jobList.jobids || jobList.jobids.length === 0) {
|
||||
this.logger.log('No active jobs found in RClone');
|
||||
return { jobids: [], stats: [] };
|
||||
}
|
||||
// Safety check: if too many groups, something is wrong
|
||||
if (groupList.groups && groupList.groups.length > 100) {
|
||||
this.logger.error(
|
||||
`DANGER: Found ${groupList.groups.length} groups, aborting to prevent job explosion`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${jobList.jobids.length} active jobs in RClone, processing all jobs with stats`
|
||||
);
|
||||
// Safety check: if too many individual jobs, something is wrong
|
||||
if (runningJobs.jobids && runningJobs.jobids.length > 1000) {
|
||||
this.logger.error(
|
||||
`DANGER: Found ${runningJobs.jobids.length} individual jobs, aborting to prevent performance issues`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const allJobs: RCloneJobWithStats[] = [];
|
||||
let successfulJobQueries = 0;
|
||||
if (!runningJobs.jobids?.length) {
|
||||
this.logger.debug('No running jobs found');
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const jobId of jobList.jobids) {
|
||||
try {
|
||||
const jobStatus = await this.getJobStatus({ jobId: String(jobId) });
|
||||
const group = jobStatus.group || '';
|
||||
const backupGroups = (groupList.groups || []).filter((group: string) =>
|
||||
group.startsWith(JOB_GROUP_PREFIX)
|
||||
);
|
||||
|
||||
let detailedStats = {};
|
||||
if (group) {
|
||||
if (backupGroups.length === 0) {
|
||||
this.logger.debug('No backup groups found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get group stats for all backup groups to get proper stats and group info
|
||||
const groupStatsMap = new Map<string, any>();
|
||||
await Promise.all(
|
||||
backupGroups.map(async (group: string) => {
|
||||
try {
|
||||
const groupStats = await this.getGroupStats(group);
|
||||
if (groupStats && typeof groupStats === 'object') {
|
||||
detailedStats = { ...groupStats };
|
||||
}
|
||||
} catch (groupError) {
|
||||
this.logger.warn(
|
||||
`Failed to get core/stats for job ${jobId}, group ${group}: ${groupError}`
|
||||
);
|
||||
const stats = await this.callRcloneApi('core/stats', { group });
|
||||
groupStatsMap.set(group, stats);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to get stats for group ${group}: ${error}`);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const enhancedStats = {
|
||||
...jobStatus.stats,
|
||||
...detailedStats,
|
||||
};
|
||||
const jobs: RCloneJob[] = [];
|
||||
|
||||
const finalStats = this.enhanceStatsWithFormattedFields(enhancedStats);
|
||||
// For each backup group, create a job entry with proper stats
|
||||
backupGroups.forEach((group) => {
|
||||
const groupStats = groupStatsMap.get(group);
|
||||
if (!groupStats) return;
|
||||
|
||||
allJobs.push({
|
||||
jobId,
|
||||
stats: finalStats,
|
||||
this.logger.debug(`Processing group ${group}: stats=${JSON.stringify(groupStats)}`);
|
||||
|
||||
const configId = group.startsWith(JOB_GROUP_PREFIX)
|
||||
? group.substring(JOB_GROUP_PREFIX.length)
|
||||
: undefined;
|
||||
|
||||
// Use the group name as the job ID for consistency, but add group info to stats
|
||||
const enhancedStats = this.statusService.enhanceStatsWithFormattedFields({
|
||||
...groupStats,
|
||||
group, // Add group to stats so it gets picked up in transformStatsToJob
|
||||
});
|
||||
|
||||
successfulJobQueries++;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get status for job ${jobId}: ${error}`);
|
||||
}
|
||||
}
|
||||
const job = this.statusService.transformStatsToJob(group, enhancedStats);
|
||||
job.configId = configId;
|
||||
|
||||
this.logger.log(
|
||||
`Successfully queried ${successfulJobQueries} jobs from ${jobList.jobids.length} total jobs`
|
||||
);
|
||||
// Only include jobs that are truly active (not completed)
|
||||
const isActivelyTransferring = groupStats.transferring?.length > 0;
|
||||
const isActivelyChecking = groupStats.checking?.length > 0;
|
||||
const hasActiveSpeed = groupStats.speed > 0;
|
||||
const isNotFinished = !groupStats.finished && groupStats.fatalError !== true;
|
||||
|
||||
const result: RCloneJobsWithStatsResponse = {
|
||||
jobids: allJobs.map((job) => job.jobId),
|
||||
stats: allJobs.map((job) => job.stats),
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getAllJobsWithStats(): Promise<RCloneJobsWithStatsResponse> {
|
||||
const jobList = await this.listRunningJobs();
|
||||
|
||||
if (!jobList.jobids || jobList.jobids.length === 0) {
|
||||
this.logger.log('No active jobs found in RClone');
|
||||
return { jobids: [], stats: [] };
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${jobList.jobids.length} active jobs in RClone: [${jobList.jobids.join(', ')}]`
|
||||
);
|
||||
|
||||
const allJobs: RCloneJobWithStats[] = [];
|
||||
let successfulJobQueries = 0;
|
||||
|
||||
for (const jobId of jobList.jobids) {
|
||||
try {
|
||||
const jobStatus = await this.getJobStatus({ jobId: String(jobId) });
|
||||
const group = jobStatus.group || '';
|
||||
|
||||
let detailedStats = {};
|
||||
if (group) {
|
||||
try {
|
||||
const groupStats = await this.getGroupStats(group);
|
||||
if (groupStats && typeof groupStats === 'object') {
|
||||
detailedStats = { ...groupStats };
|
||||
}
|
||||
} catch (groupError) {
|
||||
this.logger.warn(
|
||||
`Failed to get core/stats for job ${jobId}, group ${group}: ${groupError}`
|
||||
);
|
||||
}
|
||||
if ((isActivelyTransferring || isActivelyChecking || hasActiveSpeed) && isNotFinished) {
|
||||
jobs.push(job);
|
||||
}
|
||||
});
|
||||
|
||||
const enhancedStats = {
|
||||
...jobStatus.stats,
|
||||
...detailedStats,
|
||||
};
|
||||
this.logger.debug(
|
||||
`Found ${jobs.length} active backup jobs from ${backupGroups.length} groups`
|
||||
);
|
||||
return jobs;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get jobs with stats:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const finalStats = this.enhanceStatsWithFormattedFields(enhancedStats);
|
||||
async stopAllJobs(): Promise<JobOperationResult> {
|
||||
const runningJobs = await this.listRunningJobs();
|
||||
|
||||
allJobs.push({
|
||||
jobId,
|
||||
stats: finalStats,
|
||||
});
|
||||
|
||||
successfulJobQueries++;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get status for job ${jobId}: ${error}`);
|
||||
}
|
||||
if (!runningJobs.jobids?.length) {
|
||||
this.logger.log('No running jobs to stop');
|
||||
return { stopped: [], errors: [] };
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Successfully queried ${successfulJobQueries}/${jobList.jobids.length} jobs for detailed stats`
|
||||
this.logger.log(`Stopping ${runningJobs.jobids.length} running jobs`);
|
||||
return this.executeJobOperation(runningJobs.jobids, 'stop');
|
||||
}
|
||||
|
||||
private async executeJobOperation(
|
||||
jobIds: (string | number)[],
|
||||
operation: 'stop'
|
||||
): Promise<JobOperationResult> {
|
||||
const stopped: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
const promises = jobIds.map(async (jobId) => {
|
||||
try {
|
||||
await this.callRcloneApi(`job/${operation}`, { jobid: jobId });
|
||||
stopped.push(String(jobId));
|
||||
this.logger.log(`${operation}ped job: ${jobId}`);
|
||||
} catch (error) {
|
||||
const errorMsg = `Failed to ${operation} job ${jobId}: ${error}`;
|
||||
errors.push(errorMsg);
|
||||
this.logger.error(errorMsg);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
return { stopped, errors };
|
||||
}
|
||||
|
||||
async getBackupStatus(): Promise<BackupStatusResult> {
|
||||
const runningJobs = await this.listRunningJobs();
|
||||
|
||||
if (!runningJobs.jobids?.length) {
|
||||
return this.statusService.parseBackupStatus(runningJobs, []);
|
||||
}
|
||||
|
||||
const jobStatuses = await Promise.allSettled(
|
||||
runningJobs.jobids.map((jobId) => this.getIndividualJobStatus(String(jobId)))
|
||||
);
|
||||
|
||||
const result: RCloneJobsWithStatsResponse = {
|
||||
jobids: allJobs.map((job) => job.jobId),
|
||||
stats: allJobs.map((job) => job.stats),
|
||||
};
|
||||
|
||||
return result;
|
||||
return this.statusService.parseBackupStatus(runningJobs, jobStatuses);
|
||||
}
|
||||
|
||||
private enhanceStatsWithFormattedFields(stats: RCloneJobStats): RCloneJobStats {
|
||||
const enhancedStats = { ...stats };
|
||||
|
||||
if (stats.bytes !== undefined && stats.bytes !== null) {
|
||||
enhancedStats.formattedBytes = this.formatService.formatBytes(stats.bytes);
|
||||
}
|
||||
|
||||
if (stats.speed !== undefined && stats.speed !== null && stats.speed > 0) {
|
||||
enhancedStats.formattedSpeed = this.formatService.formatBytes(stats.speed);
|
||||
}
|
||||
|
||||
if (stats.elapsedTime !== undefined && stats.elapsedTime !== null) {
|
||||
enhancedStats.formattedElapsedTime = this.formatService.formatDuration(stats.elapsedTime);
|
||||
}
|
||||
|
||||
if (stats.eta !== undefined && stats.eta !== null && stats.eta > 0) {
|
||||
enhancedStats.formattedEta = this.formatService.formatDuration(stats.eta);
|
||||
}
|
||||
|
||||
return enhancedStats;
|
||||
}
|
||||
|
||||
private async callRcloneApi(endpoint: string, params: Record<string, any> = {}): Promise<any> {
|
||||
private async callRcloneApi(endpoint: string, params: Record<string, unknown> = {}): Promise<any> {
|
||||
const url = `${this.rcloneBaseUrl}/${endpoint}`;
|
||||
const finalParams = { _async: false, ...params };
|
||||
|
||||
try {
|
||||
const response = await got.post(url, {
|
||||
json: params,
|
||||
json: finalParams,
|
||||
responseType: 'json',
|
||||
enableUnixSockets: true,
|
||||
headers: {
|
||||
@@ -469,59 +537,113 @@ export class RCloneApiService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
return response.body;
|
||||
} catch (error: unknown) {
|
||||
this.handleApiError(error, endpoint, params);
|
||||
this.handleApiError(error, endpoint, finalParams);
|
||||
}
|
||||
}
|
||||
|
||||
private handleApiError(error: unknown, endpoint: string, params: Record<string, unknown>): never {
|
||||
const sanitizedParams = sanitizeParams(params);
|
||||
|
||||
if (error instanceof HTTPError) {
|
||||
const statusCode = error.response.statusCode;
|
||||
const rcloneError = this.extractRcloneError(error.response.body, params);
|
||||
const detailedErrorMessage = `Rclone API Error (${endpoint}, HTTP ${statusCode}): ${rcloneError}`;
|
||||
const message = `Rclone API Error (${endpoint}, HTTP ${statusCode}): ${rcloneError}`;
|
||||
|
||||
const sanitizedParams = sanitizeParams(params);
|
||||
this.logger.error(
|
||||
`Original ${detailedErrorMessage} | Params: ${JSON.stringify(sanitizedParams)}`,
|
||||
error.stack
|
||||
);
|
||||
|
||||
throw new Error(detailedErrorMessage);
|
||||
} else if (error instanceof Error) {
|
||||
const detailedErrorMessage = `Error calling RClone API (${endpoint}) with params ${JSON.stringify(sanitizeParams(params))}: ${error.message}`;
|
||||
this.logger.error(detailedErrorMessage, error.stack);
|
||||
throw error;
|
||||
} else {
|
||||
const detailedErrorMessage = `Unknown error calling RClone API (${endpoint}) with params ${JSON.stringify(sanitizeParams(params))}: ${String(error)}`;
|
||||
this.logger.error(detailedErrorMessage);
|
||||
throw new Error(detailedErrorMessage);
|
||||
this.logger.error(`${message} | Params: ${JSON.stringify(sanitizedParams)}`, error.stack);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const message =
|
||||
error instanceof Error
|
||||
? `Error calling RClone API (${endpoint}): ${error.message}`
|
||||
: `Unknown error calling RClone API (${endpoint}): ${String(error)}`;
|
||||
|
||||
this.logger.error(
|
||||
`${message} | Params: ${JSON.stringify(sanitizedParams)}`,
|
||||
error instanceof Error ? error.stack : undefined
|
||||
);
|
||||
throw error instanceof Error ? error : new Error(message);
|
||||
}
|
||||
|
||||
private extractRcloneError(responseBody: unknown, fallbackParams: Record<string, unknown>): string {
|
||||
try {
|
||||
let errorBody: unknown;
|
||||
if (typeof responseBody === 'string') {
|
||||
errorBody = JSON.parse(responseBody);
|
||||
} else if (typeof responseBody === 'object' && responseBody !== null) {
|
||||
errorBody = responseBody;
|
||||
}
|
||||
const errorBody = typeof responseBody === 'string' ? JSON.parse(responseBody) : responseBody;
|
||||
|
||||
if (errorBody && typeof errorBody === 'object' && 'error' in errorBody) {
|
||||
const typedErrorBody = errorBody as { error: unknown; input?: unknown };
|
||||
let rcloneError = `Rclone Error: ${String(typedErrorBody.error)}`;
|
||||
if (typedErrorBody.input) {
|
||||
rcloneError += ` | Input: ${JSON.stringify(typedErrorBody.input)}`;
|
||||
} else if (fallbackParams) {
|
||||
rcloneError += ` | Original Params: ${JSON.stringify(fallbackParams)}`;
|
||||
const typedError = errorBody as { error: unknown; input?: unknown };
|
||||
let message = `Rclone Error: ${String(typedError.error)}`;
|
||||
|
||||
if (typedError.input) {
|
||||
message += ` | Input: ${JSON.stringify(typedError.input)}`;
|
||||
} else {
|
||||
message += ` | Params: ${JSON.stringify(fallbackParams)}`;
|
||||
}
|
||||
return rcloneError;
|
||||
} else if (responseBody) {
|
||||
return `Non-standard error response body: ${typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody)}`;
|
||||
} else {
|
||||
return 'Empty error response body received.';
|
||||
|
||||
return message;
|
||||
}
|
||||
} catch (parseOrAccessError) {
|
||||
return `Failed to process error response body. Raw body: ${typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody)}`;
|
||||
|
||||
return responseBody
|
||||
? `Non-standard error response: ${typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody)}`
|
||||
: 'Empty error response received';
|
||||
} catch {
|
||||
return `Failed to process error response: ${typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody)}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async killExistingRcloneProcesses(): Promise<void> {
|
||||
try {
|
||||
this.logger.log('Checking for existing rclone processes...');
|
||||
const { stdout } = await execa('pgrep', ['-f', 'rclone.*rcd'], { reject: false });
|
||||
|
||||
if (!stdout.trim()) {
|
||||
this.logger.log('No existing rclone processes found');
|
||||
return;
|
||||
}
|
||||
|
||||
const pids = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((pid) => pid.trim());
|
||||
this.logger.log(`Found ${pids.length} existing rclone process(es): ${pids.join(', ')}`);
|
||||
|
||||
await this.terminateProcesses(pids);
|
||||
await this.cleanupStaleSocket();
|
||||
} catch (error) {
|
||||
this.logger.warn(`Error during rclone process cleanup: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async terminateProcesses(pids: string[]): Promise<void> {
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
this.logger.log(`Terminating rclone process PID: ${pid}`);
|
||||
|
||||
await execa('kill', ['-TERM', pid], { reject: false });
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, CONSTANTS.TIMEOUTS.GRACEFUL_SHUTDOWN)
|
||||
);
|
||||
|
||||
const { exitCode } = await execa('kill', ['-0', pid], { reject: false });
|
||||
|
||||
if (exitCode === 0) {
|
||||
this.logger.warn(`Process ${pid} still running, using SIGKILL`);
|
||||
await execa('kill', ['-KILL', pid], { reject: false });
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, CONSTANTS.TIMEOUTS.PROCESS_CLEANUP)
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(`Successfully terminated process ${pid}`);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to kill process ${pid}: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupStaleSocket(): Promise<void> {
|
||||
if (this.rcloneSocketPath && existsSync(this.rcloneSocketPath)) {
|
||||
await rm(this.rcloneSocketPath, { force: true });
|
||||
this.logger.log('Removed stale socket file');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { RCloneStatusService } from '@app/unraid-api/graph/resolvers/rclone/rclone-status.service.js';
|
||||
import { RCloneJobStats } from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
|
||||
import { FormatService } from '@app/unraid-api/utils/format.service.js';
|
||||
|
||||
// Mock NestJS Logger to suppress logs during tests
|
||||
vi.mock('@nestjs/common', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('@nestjs/common')>();
|
||||
return {
|
||||
...original,
|
||||
Logger: vi.fn(() => ({
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
describe('RCloneStatusService', () => {
|
||||
let service: RCloneStatusService;
|
||||
let mockFormatService: FormatService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockFormatService = {
|
||||
formatBytes: vi.fn().mockImplementation((bytes: number) => `${bytes} B`),
|
||||
formatSpeed: vi.fn().mockImplementation((bytesPerSecond: number) => `${bytesPerSecond} B/s`),
|
||||
formatDuration: vi.fn().mockImplementation((seconds: number) => `${seconds}s`),
|
||||
} as any;
|
||||
|
||||
service = new RCloneStatusService(mockFormatService);
|
||||
});
|
||||
|
||||
describe('enhanceStatsWithFormattedFields', () => {
|
||||
it('should add formatted fields for all numeric stats', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
bytes: 1024,
|
||||
speed: 512,
|
||||
elapsedTime: 60,
|
||||
eta: 120,
|
||||
};
|
||||
|
||||
const result = service.enhanceStatsWithFormattedFields(stats);
|
||||
|
||||
expect(result).toEqual({
|
||||
bytes: 1024,
|
||||
speed: 512,
|
||||
elapsedTime: 60,
|
||||
eta: 120,
|
||||
formattedBytes: '1024 B',
|
||||
formattedSpeed: '512 B/s',
|
||||
formattedElapsedTime: '60s',
|
||||
formattedEta: '120s',
|
||||
});
|
||||
expect(mockFormatService.formatBytes).toHaveBeenCalledWith(1024);
|
||||
expect(mockFormatService.formatSpeed).toHaveBeenCalledWith(512);
|
||||
expect(mockFormatService.formatDuration).toHaveBeenCalledWith(60);
|
||||
expect(mockFormatService.formatDuration).toHaveBeenCalledWith(120);
|
||||
});
|
||||
|
||||
it('should not add formatted fields for undefined values', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
bytes: undefined,
|
||||
speed: undefined,
|
||||
elapsedTime: undefined,
|
||||
eta: undefined,
|
||||
};
|
||||
|
||||
const result = service.enhanceStatsWithFormattedFields(stats);
|
||||
|
||||
expect(result).toEqual(stats);
|
||||
expect(mockFormatService.formatBytes).not.toHaveBeenCalled();
|
||||
expect(mockFormatService.formatDuration).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not add formatted fields for null values', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
bytes: null as any,
|
||||
speed: null as any,
|
||||
elapsedTime: null as any,
|
||||
eta: null as any,
|
||||
};
|
||||
|
||||
const result = service.enhanceStatsWithFormattedFields(stats);
|
||||
|
||||
expect(result).toEqual(stats);
|
||||
expect(mockFormatService.formatBytes).not.toHaveBeenCalled();
|
||||
expect(mockFormatService.formatDuration).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not add formatted speed for zero speed', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
speed: 0,
|
||||
};
|
||||
|
||||
const result = service.enhanceStatsWithFormattedFields(stats);
|
||||
|
||||
expect(result).toEqual({ speed: 0 });
|
||||
expect(mockFormatService.formatSpeed).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not add formatted eta for zero eta', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
eta: 0,
|
||||
};
|
||||
|
||||
const result = service.enhanceStatsWithFormattedFields(stats);
|
||||
|
||||
expect(result).toEqual({ eta: 0 });
|
||||
expect(mockFormatService.formatDuration).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformStatsToJob', () => {
|
||||
it('should create RCloneJob with completed status when transfers match total', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
group: 'unraid-backup',
|
||||
fatalError: false,
|
||||
transfers: 5,
|
||||
totalTransfers: 5,
|
||||
errors: 0,
|
||||
percentage: 100,
|
||||
};
|
||||
|
||||
const result = service.transformStatsToJob('123', stats);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
group: 'unraid-backup',
|
||||
stats,
|
||||
finished: true,
|
||||
success: true,
|
||||
error: undefined,
|
||||
progressPercentage: 100,
|
||||
detailedStatus: 'Completed',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create RCloneJob with running status when transfers incomplete', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
group: 'unraid-backup',
|
||||
fatalError: false,
|
||||
transfers: 3,
|
||||
totalTransfers: 5,
|
||||
errors: 0,
|
||||
percentage: 60,
|
||||
};
|
||||
|
||||
const result = service.transformStatsToJob('123', stats);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
group: 'unraid-backup',
|
||||
stats,
|
||||
finished: false,
|
||||
success: true,
|
||||
error: undefined,
|
||||
progressPercentage: 60,
|
||||
detailedStatus: 'Running',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create RCloneJob with error status when lastError exists', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
group: 'unraid-backup',
|
||||
fatalError: false,
|
||||
transfers: 0,
|
||||
totalTransfers: 5,
|
||||
errors: 1,
|
||||
percentage: 0,
|
||||
lastError: 'Connection timeout',
|
||||
};
|
||||
|
||||
const result = service.transformStatsToJob('123', stats);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: '123',
|
||||
group: 'unraid-backup',
|
||||
stats,
|
||||
finished: false,
|
||||
success: false,
|
||||
error: 'Connection timeout',
|
||||
progressPercentage: 0,
|
||||
detailedStatus: 'Error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle numeric job ID', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
fatalError: false,
|
||||
transfers: 0,
|
||||
totalTransfers: 0,
|
||||
};
|
||||
|
||||
const result = service.transformStatsToJob(456, stats);
|
||||
|
||||
expect(result.id).toBe('456');
|
||||
});
|
||||
|
||||
it('should handle missing group', () => {
|
||||
const stats: RCloneJobStats = {
|
||||
fatalError: false,
|
||||
transfers: 0,
|
||||
totalTransfers: 0,
|
||||
};
|
||||
|
||||
const result = service.transformStatsToJob('123', stats);
|
||||
|
||||
expect(result.group).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateCombinedStats', () => {
|
||||
it('should combine stats from multiple jobs', () => {
|
||||
const mockActiveJobs = [
|
||||
{
|
||||
stats: {
|
||||
bytes: 1024,
|
||||
checks: 2,
|
||||
transfers: 3,
|
||||
totalBytes: 2048,
|
||||
totalChecks: 4,
|
||||
totalTransfers: 6,
|
||||
speed: 100,
|
||||
eta: 120,
|
||||
},
|
||||
},
|
||||
{
|
||||
stats: {
|
||||
bytes: 512,
|
||||
checks: 1,
|
||||
transfers: 2,
|
||||
totalBytes: 1024,
|
||||
totalChecks: 2,
|
||||
totalTransfers: 4,
|
||||
speed: 200,
|
||||
eta: 60,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = service.calculateCombinedStats(mockActiveJobs);
|
||||
|
||||
expect(result).toEqual({
|
||||
bytes: 1536,
|
||||
checks: 3,
|
||||
transfers: 5,
|
||||
totalBytes: 3072,
|
||||
totalChecks: 6,
|
||||
totalTransfers: 10,
|
||||
speed: 200, // Max speed
|
||||
eta: 120, // Max eta
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null for empty jobs array', () => {
|
||||
const result = service.calculateCombinedStats([]);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when no valid stats', () => {
|
||||
const mockActiveJobs = [{ stats: null as any }, { stats: undefined as any }];
|
||||
const result = service.calculateCombinedStats(mockActiveJobs);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseActiveJobs', () => {
|
||||
it('should return active jobs that are not finished', () => {
|
||||
const mockJobStatuses = [
|
||||
{ status: 'fulfilled', value: { id: '1', finished: false } },
|
||||
{ status: 'fulfilled', value: { id: '2', finished: true } },
|
||||
{ status: 'rejected', reason: 'Error' },
|
||||
] as PromiseSettledResult<any>[];
|
||||
|
||||
const result = service.parseActiveJobs(mockJobStatuses);
|
||||
|
||||
expect(result).toEqual([{ id: '1', finished: false }]);
|
||||
});
|
||||
|
||||
it('should return empty array when all jobs are finished', () => {
|
||||
const mockJobStatuses = [
|
||||
{ status: 'fulfilled', value: { id: '1', finished: true } },
|
||||
] as PromiseSettledResult<any>[];
|
||||
|
||||
const result = service.parseActiveJobs(mockJobStatuses);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseBackupStatus', () => {
|
||||
it('should return running status when active jobs exist', () => {
|
||||
const mockRunningJobs = { jobids: ['123', '456'] };
|
||||
const mockJobStatuses = [
|
||||
{ status: 'fulfilled', value: { id: '123', finished: false, stats: { bytes: 1024 } } },
|
||||
{ status: 'fulfilled', value: { id: '456', finished: false, stats: { bytes: 512 } } },
|
||||
] as PromiseSettledResult<any>[];
|
||||
|
||||
const result = service.parseBackupStatus(mockRunningJobs, mockJobStatuses);
|
||||
|
||||
expect(result).toEqual({
|
||||
isRunning: true,
|
||||
stats: expect.objectContaining({ bytes: 1536 }),
|
||||
jobCount: 2,
|
||||
activeJobs: expect.arrayContaining([
|
||||
expect.objectContaining({ id: '123', finished: false }),
|
||||
expect.objectContaining({ id: '456', finished: false }),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return not running when no job IDs', () => {
|
||||
const mockRunningJobs = { jobids: [] };
|
||||
const mockJobStatuses = [] as PromiseSettledResult<any>[];
|
||||
|
||||
const result = service.parseBackupStatus(mockRunningJobs, mockJobStatuses);
|
||||
|
||||
expect(result).toEqual({
|
||||
isRunning: false,
|
||||
stats: null,
|
||||
jobCount: 0,
|
||||
activeJobs: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseJobWithStats', () => {
|
||||
it('should parse job with enhanced stats', () => {
|
||||
const mockJobStatus = {
|
||||
stats: { bytes: 1024, speed: 512 },
|
||||
};
|
||||
|
||||
const result = service.parseJobWithStats('123', mockJobStatus);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
stats: expect.objectContaining({
|
||||
bytes: 1024,
|
||||
speed: 512,
|
||||
formattedBytes: '1024 B',
|
||||
formattedSpeed: '512 B/s',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing stats', () => {
|
||||
const mockJobStatus = {};
|
||||
|
||||
const result = service.parseJobWithStats('123', mockJobStatus);
|
||||
|
||||
expect(result.id).toBe('123');
|
||||
expect(result.stats).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseAllJobsWithStats', () => {
|
||||
it('should return jobs when job IDs exist', () => {
|
||||
const mockRunningJobs = { jobids: ['123', '456'] };
|
||||
const mockJobs = [
|
||||
{ id: '123', group: 'unraid-backup' },
|
||||
{ id: '456', group: 'unraid-backup' },
|
||||
] as any[];
|
||||
|
||||
const result = service.parseAllJobsWithStats(mockRunningJobs, mockJobs);
|
||||
|
||||
expect(result).toEqual(mockJobs);
|
||||
});
|
||||
|
||||
it('should return empty array when no job IDs', () => {
|
||||
const mockRunningJobs = { jobids: [] };
|
||||
const mockJobs = [] as any[];
|
||||
|
||||
const result = service.parseAllJobsWithStats(mockRunningJobs, mockJobs);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseJobsWithStats', () => {
|
||||
it('should parse fulfilled job statuses', () => {
|
||||
const mockJobStatuses = [
|
||||
{ status: 'fulfilled', value: { id: '123', stats: { bytes: 1024 } } },
|
||||
{ status: 'fulfilled', value: { id: '456', stats: { bytes: 512 } } },
|
||||
{ status: 'rejected', reason: 'Error' },
|
||||
] as PromiseSettledResult<any>[];
|
||||
|
||||
const result = service.parseJobsWithStats(mockJobStatuses);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: '123',
|
||||
stats: expect.objectContaining({ bytes: 1024, formattedBytes: '1024 B' }),
|
||||
})
|
||||
);
|
||||
expect(result[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: '456',
|
||||
stats: expect.objectContaining({ bytes: 512, formattedBytes: '512 B' }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle rejected statuses gracefully', () => {
|
||||
const mockJobStatuses = [
|
||||
{ status: 'rejected', reason: 'Error' },
|
||||
] as PromiseSettledResult<any>[];
|
||||
|
||||
const result = service.parseJobsWithStats(mockJobStatuses);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBackupStatus', () => {
|
||||
it('should return default backup status', () => {
|
||||
const result = service.getBackupStatus();
|
||||
|
||||
expect(result).toEqual({
|
||||
isRunning: false,
|
||||
stats: null,
|
||||
jobCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
RCloneJob,
|
||||
RCloneJobListResponse,
|
||||
RCloneJobStats,
|
||||
RCloneJobWithStats,
|
||||
} from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
|
||||
import { FormatService } from '@app/unraid-api/utils/format.service.js';
|
||||
|
||||
// Internal interface for job status response from RClone API
|
||||
interface RCloneJobStatusResponse {
|
||||
id?: string | number;
|
||||
group?: string;
|
||||
stats?: RCloneJobStats;
|
||||
finished?: boolean;
|
||||
error?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface BackupStatusResult {
|
||||
isRunning: boolean;
|
||||
stats: RCloneJobStats | null;
|
||||
jobCount: number;
|
||||
activeJobs: RCloneJobStatusResponse[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RCloneStatusService {
|
||||
private readonly logger = new Logger(RCloneStatusService.name);
|
||||
|
||||
constructor(private readonly formatService: FormatService) {}
|
||||
|
||||
enhanceStatsWithFormattedFields(stats: RCloneJobStats): RCloneJobStats {
|
||||
const enhancedStats = { ...stats };
|
||||
|
||||
if (stats.bytes !== undefined && stats.bytes !== null) {
|
||||
enhancedStats.formattedBytes = this.formatService.formatBytes(stats.bytes);
|
||||
}
|
||||
|
||||
if (stats.speed !== undefined && stats.speed !== null && stats.speed > 0) {
|
||||
enhancedStats.formattedSpeed = this.formatService.formatSpeed(stats.speed);
|
||||
}
|
||||
|
||||
if (stats.elapsedTime !== undefined && stats.elapsedTime !== null) {
|
||||
enhancedStats.formattedElapsedTime = this.formatService.formatDuration(stats.elapsedTime);
|
||||
}
|
||||
|
||||
if (stats.eta !== undefined && stats.eta !== null && stats.eta > 0) {
|
||||
enhancedStats.formattedEta = this.formatService.formatDuration(stats.eta);
|
||||
}
|
||||
|
||||
return enhancedStats;
|
||||
}
|
||||
|
||||
transformStatsToJob(jobId: string | number, stats: RCloneJobStats): RCloneJob {
|
||||
this.logger.debug(`Stats for job ${jobId}: %o`, stats);
|
||||
const group = stats.group || undefined;
|
||||
|
||||
this.logger.debug(`Processing job ${jobId}: group="${group}", stats: ${JSON.stringify(stats)}`);
|
||||
|
||||
return {
|
||||
id: String(jobId),
|
||||
group: group,
|
||||
stats,
|
||||
finished:
|
||||
stats.fatalError === false &&
|
||||
stats.transfers === (stats.totalTransfers || 0) &&
|
||||
(stats.totalTransfers || 0) > 0,
|
||||
success: stats.fatalError === false && (stats.errors || 0) === 0,
|
||||
error: stats.lastError || undefined,
|
||||
progressPercentage: stats.percentage,
|
||||
detailedStatus: stats.lastError
|
||||
? 'Error'
|
||||
: stats.percentage === 100
|
||||
? 'Completed'
|
||||
: 'Running',
|
||||
};
|
||||
}
|
||||
|
||||
calculateCombinedStats(activeJobs: RCloneJobStatusResponse[]): RCloneJobStats | null {
|
||||
if (activeJobs.length === 0) return null;
|
||||
|
||||
const validStats = activeJobs
|
||||
.map((job) => job.stats)
|
||||
.filter((stats): stats is RCloneJobStats => Boolean(stats));
|
||||
|
||||
if (validStats.length === 0) return null;
|
||||
|
||||
return validStats.reduce(
|
||||
(combined, stats) => ({
|
||||
bytes: (combined.bytes || 0) + (stats.bytes || 0),
|
||||
checks: (combined.checks || 0) + (stats.checks || 0),
|
||||
transfers: (combined.transfers || 0) + (stats.transfers || 0),
|
||||
totalBytes: (combined.totalBytes || 0) + (stats.totalBytes || 0),
|
||||
totalChecks: (combined.totalChecks || 0) + (stats.totalChecks || 0),
|
||||
totalTransfers: (combined.totalTransfers || 0) + (stats.totalTransfers || 0),
|
||||
speed: Math.max(combined.speed || 0, stats.speed || 0),
|
||||
eta: Math.max(combined.eta || 0, stats.eta || 0),
|
||||
}),
|
||||
{} as RCloneJobStats
|
||||
);
|
||||
}
|
||||
|
||||
parseActiveJobs(
|
||||
jobStatuses: PromiseSettledResult<RCloneJobStatusResponse>[]
|
||||
): RCloneJobStatusResponse[] {
|
||||
const activeJobs: RCloneJobStatusResponse[] = [];
|
||||
|
||||
this.logger.debug(`Job statuses: ${JSON.stringify(jobStatuses)}`);
|
||||
|
||||
jobStatuses.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled' && !result.value.finished) {
|
||||
activeJobs.push(result.value);
|
||||
} else if (result.status === 'rejected') {
|
||||
this.logger.warn(`Failed to get status for job ${index}: ${result.reason}`);
|
||||
}
|
||||
});
|
||||
|
||||
return activeJobs;
|
||||
}
|
||||
|
||||
parseBackupStatus(
|
||||
runningJobs: RCloneJobListResponse,
|
||||
jobStatuses: PromiseSettledResult<RCloneJobStatusResponse>[]
|
||||
): BackupStatusResult {
|
||||
if (!runningJobs.jobids?.length) {
|
||||
return {
|
||||
isRunning: false,
|
||||
stats: null,
|
||||
jobCount: 0,
|
||||
activeJobs: [],
|
||||
};
|
||||
}
|
||||
|
||||
const activeJobs = this.parseActiveJobs(jobStatuses);
|
||||
const combinedStats = this.calculateCombinedStats(activeJobs);
|
||||
|
||||
return {
|
||||
isRunning: activeJobs.length > 0,
|
||||
stats: combinedStats,
|
||||
jobCount: activeJobs.length,
|
||||
activeJobs,
|
||||
};
|
||||
}
|
||||
|
||||
parseJobWithStats(jobId: string, jobStatus: RCloneJobStatusResponse): RCloneJob {
|
||||
const stats = jobStatus.stats ? this.enhanceStatsWithFormattedFields(jobStatus.stats) : {};
|
||||
return this.transformStatsToJob(jobId, stats);
|
||||
}
|
||||
|
||||
parseAllJobsWithStats(runningJobs: RCloneJobListResponse, jobs: RCloneJob[]): RCloneJob[] {
|
||||
if (!runningJobs.jobids?.length) {
|
||||
this.logger.log('No active jobs found in RClone');
|
||||
return [];
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Found ${runningJobs.jobids.length} active jobs in RClone: [${runningJobs.jobids.join(', ')}]`
|
||||
);
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
parseJobsWithStats(jobStatuses: PromiseSettledResult<RCloneJobStatusResponse>[]): RCloneJob[] {
|
||||
const allJobs: RCloneJob[] = [];
|
||||
|
||||
jobStatuses.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
const jobStatus = result.value;
|
||||
const stats = jobStatus.stats
|
||||
? this.enhanceStatsWithFormattedFields(jobStatus.stats)
|
||||
: {};
|
||||
const job = this.transformStatsToJob(jobStatus.id || index, stats);
|
||||
allJobs.push(job);
|
||||
} else {
|
||||
this.logger.error(`Failed to get status for job ${index}: ${result.reason}`);
|
||||
}
|
||||
});
|
||||
|
||||
return allJobs;
|
||||
}
|
||||
|
||||
getBackupStatus(): {
|
||||
isRunning: boolean;
|
||||
stats: RCloneJobStats | null;
|
||||
jobCount: number;
|
||||
} {
|
||||
try {
|
||||
return {
|
||||
isRunning: false,
|
||||
stats: null,
|
||||
jobCount: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.debug(`Error getting backup status: ${error}`);
|
||||
return {
|
||||
isRunning: false,
|
||||
stats: null,
|
||||
jobCount: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,10 +153,13 @@ export class RCloneStartBackupInput {
|
||||
@IsBoolean()
|
||||
async?: boolean;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@Field(() => String, {
|
||||
nullable: true,
|
||||
description: 'Configuration ID for job grouping and identification',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
group?: string;
|
||||
configId?: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@IsOptional()
|
||||
@@ -336,20 +339,41 @@ export class RCloneJob {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class RCloneJobStatusDto {
|
||||
@Field(() => Number, { description: 'Job ID' })
|
||||
id!: number;
|
||||
|
||||
@Field(() => String, { description: 'RClone group for the job' })
|
||||
group!: string;
|
||||
|
||||
@Field(() => Boolean, { description: 'Whether the job is finished' })
|
||||
finished!: boolean;
|
||||
|
||||
@Field(() => Boolean, { description: 'Whether the job was successful' })
|
||||
success!: boolean;
|
||||
|
||||
@Field(() => String, { description: 'Error message if any' })
|
||||
error!: string;
|
||||
|
||||
@Field(() => Number, { description: 'Job duration in seconds' })
|
||||
duration!: number;
|
||||
|
||||
@Field(() => String, { description: 'Job start time in ISO format' })
|
||||
startTime!: string;
|
||||
|
||||
@Field(() => String, { description: 'Job end time in ISO format' })
|
||||
endTime!: string;
|
||||
|
||||
@Field(() => GraphQLJSON, { description: 'Job output data', nullable: true })
|
||||
output?: Record<string, any>;
|
||||
}
|
||||
|
||||
// API Response Types (for internal use)
|
||||
export interface RCloneJobListResponse {
|
||||
jobids: (string | number)[];
|
||||
}
|
||||
|
||||
export interface RCloneJobStatusResponse {
|
||||
group?: string;
|
||||
finished?: boolean;
|
||||
success?: boolean;
|
||||
error?: string;
|
||||
stats?: RCloneJobStats;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface RCloneJobWithStats {
|
||||
jobId: string | number;
|
||||
stats: RCloneJobStats;
|
||||
|
||||
@@ -2,19 +2,22 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { RCloneApiService } from '@app/unraid-api/graph/resolvers/rclone/rclone-api.service.js';
|
||||
import { RCloneFormService } from '@app/unraid-api/graph/resolvers/rclone/rclone-form.service.js';
|
||||
import { RCloneStatusService } from '@app/unraid-api/graph/resolvers/rclone/rclone-status.service.js';
|
||||
import { RCloneMutationsResolver } from '@app/unraid-api/graph/resolvers/rclone/rclone.mutation.resolver.js';
|
||||
import { RCloneBackupSettingsResolver } from '@app/unraid-api/graph/resolvers/rclone/rclone.resolver.js';
|
||||
import { RCloneService } from '@app/unraid-api/graph/resolvers/rclone/rclone.service.js';
|
||||
import { UtilsModule } from '@app/unraid-api/utils/utils.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [UtilsModule],
|
||||
providers: [
|
||||
RCloneService,
|
||||
RCloneApiService,
|
||||
RCloneStatusService,
|
||||
RCloneFormService,
|
||||
RCloneBackupSettingsResolver,
|
||||
RCloneMutationsResolver,
|
||||
],
|
||||
exports: [RCloneService, RCloneApiService],
|
||||
exports: [RCloneService, RCloneApiService, RCloneStatusService],
|
||||
})
|
||||
export class RCloneModule {}
|
||||
|
||||
@@ -32,9 +32,11 @@ import { VmsService } from '@app/unraid-api/graph/resolvers/vms/vms.service.js';
|
||||
import { ServicesResolver } from '@app/unraid-api/graph/services/services.resolver.js';
|
||||
import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js';
|
||||
import { MeResolver } from '@app/unraid-api/graph/user/user.resolver.js';
|
||||
import { UtilsModule } from '@app/unraid-api/utils/utils.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UtilsModule,
|
||||
ArrayModule,
|
||||
ApiKeyModule,
|
||||
AuthModule,
|
||||
|
||||
10
api/src/unraid-api/graph/utils/utils.module.ts
Normal file
10
api/src/unraid-api/graph/utils/utils.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
|
||||
import { FormatService } from '@app/unraid-api/utils/format.service.js';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [FormatService],
|
||||
exports: [FormatService],
|
||||
})
|
||||
export class UtilsModule {}
|
||||
59
api/src/unraid-api/utils/format.service.test.ts
Normal file
59
api/src/unraid-api/utils/format.service.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { FormatService } from '@app/unraid-api/utils/format.service.js';
|
||||
|
||||
describe('FormatService', () => {
|
||||
const service = new FormatService();
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('should format zero bytes', () => {
|
||||
expect(service.formatBytes(0)).toBe('0 B');
|
||||
});
|
||||
|
||||
it('should format bytes to best unit', () => {
|
||||
expect(service.formatBytes(1024)).toBe('1.02 KB');
|
||||
expect(service.formatBytes(1048576)).toBe('1.05 MB');
|
||||
expect(service.formatBytes(1073741824)).toBe('1.07 GB');
|
||||
});
|
||||
|
||||
it('should format with decimals when needed', () => {
|
||||
expect(service.formatBytes(1536)).toBe('1.54 KB');
|
||||
expect(service.formatBytes(9636529)).toBe('9.64 MB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatSpeed', () => {
|
||||
it('should format zero speed', () => {
|
||||
expect(service.formatSpeed(0)).toBe('0 B/s');
|
||||
});
|
||||
|
||||
it('should format speed with /s suffix', () => {
|
||||
expect(service.formatSpeed(1024)).toBe('1.02 KB/s');
|
||||
expect(service.formatSpeed(1048576)).toBe('1.05 MB/s');
|
||||
expect(service.formatSpeed(1073741824)).toBe('1.07 GB/s');
|
||||
});
|
||||
|
||||
it('should format with decimals when needed', () => {
|
||||
expect(service.formatSpeed(1536)).toBe('1.54 KB/s');
|
||||
expect(service.formatSpeed(9636529.183648435)).toBe('9.64 MB/s');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDuration', () => {
|
||||
it('should format small durations in seconds', () => {
|
||||
expect(service.formatDuration(30)).toBe('30s');
|
||||
expect(service.formatDuration(45.5)).toBe('45.5s');
|
||||
});
|
||||
|
||||
it('should format longer durations to best unit', () => {
|
||||
expect(service.formatDuration(60)).toBe('60 s');
|
||||
expect(service.formatDuration(3600)).toBe('60 min');
|
||||
expect(service.formatDuration(86400)).toBe('24 h');
|
||||
});
|
||||
|
||||
it('should format with decimals when needed', () => {
|
||||
expect(service.formatDuration(90)).toBe('1.5 min');
|
||||
expect(service.formatDuration(11.615060290966666 * 60)).toBe('11.62 min');
|
||||
});
|
||||
});
|
||||
});
|
||||
33
api/src/unraid-api/utils/format.service.ts
Normal file
33
api/src/unraid-api/utils/format.service.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { convert } from 'convert';
|
||||
|
||||
@Injectable()
|
||||
export class FormatService {
|
||||
formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
const result = convert(bytes, 'bytes').to('best');
|
||||
const value =
|
||||
typeof result.quantity === 'number' ? Number(result.quantity.toFixed(2)) : result.quantity;
|
||||
return `${value} ${result.unit}`;
|
||||
}
|
||||
|
||||
formatSpeed(bytesPerSecond: number): string {
|
||||
if (bytesPerSecond === 0) return '0 B/s';
|
||||
|
||||
const result = convert(bytesPerSecond, 'bytes').to('best');
|
||||
const value =
|
||||
typeof result.quantity === 'number' ? Number(result.quantity.toFixed(2)) : result.quantity;
|
||||
return `${value} ${result.unit}/s`;
|
||||
}
|
||||
|
||||
formatDuration(seconds: number): string {
|
||||
if (seconds < 60) return `${Math.round(seconds * 100) / 100}s`;
|
||||
|
||||
const result = convert(seconds, 'seconds').to('best');
|
||||
const value =
|
||||
typeof result.quantity === 'number' ? Number(result.quantity.toFixed(2)) : result.quantity;
|
||||
return `${value} ${result.unit}`;
|
||||
}
|
||||
}
|
||||
10
api/src/unraid-api/utils/utils.module.ts
Normal file
10
api/src/unraid-api/utils/utils.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
|
||||
import { FormatService } from '@app/unraid-api/utils/format.service.js';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [FormatService],
|
||||
exports: [FormatService],
|
||||
})
|
||||
export class UtilsModule {}
|
||||
138
web/components/Backup/BackupEntry.vue
Normal file
138
web/components/Backup/BackupEntry.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import type { BackupJobsQuery } from '~/composables/gql/graphql';
|
||||
import { useFragment } from '~/composables/gql/fragment-masking';
|
||||
import { BACKUP_STATS_FRAGMENT } from './backup-jobs.query';
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
job: NonNullable<BackupJobsQuery['backup']>['jobs'][0];
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const stats = useFragment(BACKUP_STATS_FRAGMENT, props.job.stats);
|
||||
|
||||
// Calculate percentage if it's null but we have bytes and totalBytes
|
||||
const calculatedPercentage = computed(() => {
|
||||
if (stats?.percentage !== null) {
|
||||
return stats?.percentage;
|
||||
}
|
||||
if (stats?.bytes && stats?.totalBytes) {
|
||||
return Math.round((stats.bytes / stats.totalBytes) * 100);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Determine job status based on job properties
|
||||
const jobStatus = computed(() => {
|
||||
if (props.job.error) return 'error';
|
||||
if (props.job.finished && props.job.success) return 'completed';
|
||||
if (props.job.finished && !props.job.success) return 'failed';
|
||||
return 'running';
|
||||
});
|
||||
|
||||
const statusColor = computed(() => {
|
||||
switch (jobStatus.value) {
|
||||
case 'error':
|
||||
case 'failed':
|
||||
return 'red';
|
||||
case 'completed':
|
||||
return 'green';
|
||||
case 'running':
|
||||
default:
|
||||
return 'blue';
|
||||
}
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
switch (jobStatus.value) {
|
||||
case 'error':
|
||||
return 'Error';
|
||||
case 'failed':
|
||||
return 'Failed';
|
||||
case 'completed':
|
||||
return 'Completed';
|
||||
case 'running':
|
||||
default:
|
||||
return 'Running';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 shadow-sm">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-3 h-3 bg-green-400 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
Backup Job
|
||||
</h3>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<p>Job ID: {{ job.id }}</p>
|
||||
<p v-if="job.configId">Config ID: {{ job.configId }}</p>
|
||||
<p v-if="job.group">Group: {{ job.group }}</p>
|
||||
<p v-if="job.detailedStatus">Status: {{ job.detailedStatus }}</p>
|
||||
<p v-if="job.error" class="text-red-600 dark:text-red-400">Error: {{ job.error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
:class="`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColor === 'green' ? 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400' : statusColor === 'red' ? 'bg-red-100 text-red-800 dark:bg-red-900/20 dark:text-red-400' : 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400'}`"
|
||||
>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div v-if="stats?.formattedBytes" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Bytes Transferred</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ stats.formattedBytes }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="stats?.transfers" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Files Transferred</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ stats.transfers }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="stats?.formattedSpeed" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Transfer Speed</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ stats.formattedSpeed }}</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="stats?.formattedElapsedTime" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Elapsed Time</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ stats.formattedElapsedTime }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="stats?.formattedEta" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">ETA</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ stats.formattedEta }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="calculatedPercentage" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Progress</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ calculatedPercentage }}%</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="calculatedPercentage" class="mt-4">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: `${calculatedPercentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,56 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onUnmounted } from 'vue';
|
||||
import { useQuery, useMutation, useSubscription } from '@vue/apollo-composable';
|
||||
import {
|
||||
Switch,
|
||||
Button,
|
||||
Badge,
|
||||
Spinner,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTitle
|
||||
} from '@unraid/ui';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable';
|
||||
import { useFragment } from '~/composables/gql/fragment-masking';
|
||||
|
||||
import {
|
||||
import { Badge, Button, Sheet, SheetContent, SheetTitle, Spinner, Switch } from '@unraid/ui';
|
||||
|
||||
import {
|
||||
BACKUP_JOB_CONFIGS_QUERY,
|
||||
BACKUP_JOBS_QUERY,
|
||||
BACKUP_STATS_FRAGMENT,
|
||||
TOGGLE_BACKUP_JOB_CONFIG_MUTATION,
|
||||
TRIGGER_BACKUP_JOB_MUTATION,
|
||||
BACKUP_JOB_PROGRESS_SUBSCRIPTION
|
||||
} from '~/components/Backup/backup-jobs.query';
|
||||
import BackupJobConfigForm from '~/components/Backup/BackupJobConfigForm.vue';
|
||||
|
||||
const showConfigModal = ref(false);
|
||||
const togglingJobs = ref<Set<string>>(new Set<string>());
|
||||
const triggeringJobs = ref<Set<string>>(new Set<string>());
|
||||
const activeJobId = ref<string | null>(null);
|
||||
|
||||
const { result, loading, error, refetch } = useQuery(BACKUP_JOB_CONFIGS_QUERY);
|
||||
const { result, loading, error, refetch } = useQuery(BACKUP_JOB_CONFIGS_QUERY, {}, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
const { result: jobsResult, refetch: refetchJobs } = useQuery(BACKUP_JOBS_QUERY, {}, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval: 5000, // Poll every 5 seconds for real-time updates
|
||||
});
|
||||
|
||||
const { mutate: toggleJobConfig } = useMutation(TOGGLE_BACKUP_JOB_CONFIG_MUTATION);
|
||||
const { mutate: triggerJob } = useMutation(TRIGGER_BACKUP_JOB_MUTATION);
|
||||
|
||||
const { result: progressResult } = useSubscription(
|
||||
BACKUP_JOB_PROGRESS_SUBSCRIPTION,
|
||||
{ jobId: activeJobId.value || '' },
|
||||
{ enabled: computed(() => !!activeJobId.value) }
|
||||
);
|
||||
|
||||
const backupConfigs = computed(() => result.value?.backup?.configs || []);
|
||||
const runningJobs = computed(() => jobsResult.value?.backup?.jobs || []);
|
||||
|
||||
const currentJobProgress = computed(() => {
|
||||
if (!progressResult.value?.backupJobProgress) return null;
|
||||
|
||||
const job = progressResult.value.backupJobProgress;
|
||||
const percentage = job.stats?.percentage || 0;
|
||||
|
||||
return {
|
||||
jobId: job.id,
|
||||
percentage: Math.round(percentage),
|
||||
transferredBytes: job.formattedBytes || '0 B',
|
||||
speed: job.formattedSpeed || '0 B/s',
|
||||
elapsedTime: job.formattedElapsedTime || '0s',
|
||||
eta: job.formattedEta || 'Unknown'
|
||||
};
|
||||
// Match running jobs to configs and create combined data
|
||||
const configsWithJobs = computed(() => {
|
||||
return backupConfigs.value.map(config => {
|
||||
// Find running job that matches this config using the configId field
|
||||
const runningJob = runningJobs.value.find(job => job.configId === config.id);
|
||||
|
||||
let jobStats = null;
|
||||
if (runningJob?.stats) {
|
||||
const stats = useFragment(BACKUP_STATS_FRAGMENT, runningJob.stats);
|
||||
|
||||
// Calculate percentage if it's null but we have bytes and totalBytes
|
||||
let calculatedPercentage = stats?.percentage;
|
||||
if (calculatedPercentage === null && stats?.bytes && stats?.totalBytes) {
|
||||
calculatedPercentage = Math.round((stats.bytes / stats.totalBytes) * 100);
|
||||
}
|
||||
|
||||
jobStats = {
|
||||
percentage: Math.round(calculatedPercentage || 0),
|
||||
transferredBytes: stats?.formattedBytes || '0 B',
|
||||
speed: stats?.formattedSpeed || '0 B/s',
|
||||
elapsedTime: stats?.formattedElapsedTime || '0s',
|
||||
eta: stats?.formattedEta || 'Unknown',
|
||||
transfers: stats?.transfers || 0,
|
||||
checks: stats?.checks || 0,
|
||||
errors: stats?.errors || 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
runningJob,
|
||||
jobStats,
|
||||
isRunning: !!runningJob,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
@@ -64,9 +80,9 @@ function onConfigComplete() {
|
||||
|
||||
async function handleToggleJob(jobId: string) {
|
||||
if (togglingJobs.value.has(jobId)) return;
|
||||
|
||||
|
||||
togglingJobs.value.add(jobId);
|
||||
|
||||
|
||||
try {
|
||||
await toggleJobConfig({ id: jobId });
|
||||
await refetch();
|
||||
@@ -79,15 +95,17 @@ async function handleToggleJob(jobId: string) {
|
||||
|
||||
async function handleTriggerJob(jobId: string) {
|
||||
if (triggeringJobs.value.has(jobId)) return;
|
||||
|
||||
|
||||
triggeringJobs.value.add(jobId);
|
||||
|
||||
|
||||
try {
|
||||
const result = await triggerJob({ id: jobId });
|
||||
if (result?.data?.backup?.triggerJob?.jobId) {
|
||||
const backupJobId = result.data.backup.triggerJob.jobId;
|
||||
activeJobId.value = backupJobId;
|
||||
console.log('Backup job triggered:', result.data.backup.triggerJob);
|
||||
// Wait a moment for the RClone API to register the job
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
// Refetch both configs and jobs to show the running job immediately
|
||||
await Promise.all([refetch(), refetchJobs()]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to trigger backup job:', error);
|
||||
@@ -95,73 +113,13 @@ async function handleTriggerJob(jobId: string) {
|
||||
triggeringJobs.value.delete(jobId);
|
||||
}
|
||||
}
|
||||
|
||||
function stopProgressMonitoring() {
|
||||
activeJobId.value = null;
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
activeJobId.value = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="backup-config">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Scheduled Backup Jobs</h2>
|
||||
<Button
|
||||
variant="primary"
|
||||
@click="showConfigModal = true"
|
||||
>
|
||||
Add Backup Job
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Progress monitoring banner -->
|
||||
<div
|
||||
v-if="currentJobProgress"
|
||||
class="mb-6 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
Backup in Progress
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@click="stopProgressMonitoring"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between text-sm text-blue-700 dark:text-blue-300 mb-1">
|
||||
<span>{{ currentJobProgress.percentage }}% complete</span>
|
||||
<span>{{ currentJobProgress.speed }}</span>
|
||||
</div>
|
||||
<div class="w-full bg-blue-200 dark:bg-blue-700 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: `${currentJobProgress.percentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 text-sm text-blue-700 dark:text-blue-300">
|
||||
<div>
|
||||
<span class="font-medium">Transferred:</span> {{ currentJobProgress.transferredBytes }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Elapsed:</span> {{ currentJobProgress.elapsedTime }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">ETA:</span> {{ currentJobProgress.eta }}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="primary" @click="showConfigModal = true"> Add Backup Job </Button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading && !result" class="text-center py-8">
|
||||
@@ -200,18 +158,13 @@ onUnmounted(() => {
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Create your first scheduled backup job to automatically protect your data.
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
@click="showConfigModal = true"
|
||||
>
|
||||
Create First Backup Job
|
||||
</Button>
|
||||
<Button variant="primary" @click="showConfigModal = true"> Create First Backup Job </Button>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
v-for="config in backupConfigs"
|
||||
:key="config.id"
|
||||
v-for="configWithJob in configsWithJobs"
|
||||
:key="configWithJob.id"
|
||||
class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 shadow-sm"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
@@ -219,86 +172,135 @@ onUnmounted(() => {
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
:class="[
|
||||
'w-3 h-3 rounded-full',
|
||||
config.enabled ? 'bg-green-400' : 'bg-gray-400',
|
||||
currentJobProgress?.jobId === activeJobId ? 'animate-pulse' : ''
|
||||
'w-3 h-3 rounded-full',
|
||||
configWithJob.isRunning
|
||||
? 'bg-blue-400 animate-pulse'
|
||||
: configWithJob.enabled
|
||||
? 'bg-green-400'
|
||||
: 'bg-gray-400',
|
||||
]"
|
||||
></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{{ config.name }}
|
||||
<span v-if="currentJobProgress?.jobId === activeJobId" class="text-sm text-blue-600 dark:text-blue-400 ml-2">
|
||||
{{ configWithJob.name }}
|
||||
<span
|
||||
v-if="configWithJob.isRunning"
|
||||
class="text-sm text-blue-600 dark:text-blue-400 ml-2"
|
||||
>
|
||||
(Running)
|
||||
</span>
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ config.sourcePath }} → {{ config.remoteName }}:{{ config.destinationPath }}
|
||||
{{ configWithJob.sourcePath }} → {{ configWithJob.remoteName }}:{{ configWithJob.destinationPath }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Toggle Switch -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||
{{ config.enabled ? 'Enabled' : 'Disabled' }}
|
||||
{{ configWithJob.enabled ? 'Enabled' : 'Disabled' }}
|
||||
</span>
|
||||
<Switch
|
||||
:checked="config.enabled"
|
||||
:disabled="togglingJobs.has(config.id)"
|
||||
@update:checked="() => handleToggleJob(config.id)"
|
||||
:checked="configWithJob.enabled"
|
||||
:disabled="togglingJobs.has(configWithJob.id) || configWithJob.isRunning"
|
||||
@update:checked="() => handleToggleJob(configWithJob.id)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Run Now Button -->
|
||||
|
||||
<Button
|
||||
:disabled="!config.enabled || triggeringJobs.has(config.id)"
|
||||
:variant="config.enabled && !triggeringJobs.has(config.id) ? 'primary' : 'outline'"
|
||||
:disabled="triggeringJobs.has(configWithJob.id) || configWithJob.isRunning"
|
||||
:variant="!triggeringJobs.has(configWithJob.id) && !configWithJob.isRunning ? 'primary' : 'outline'"
|
||||
size="sm"
|
||||
@click="handleTriggerJob(config.id)"
|
||||
@click="handleTriggerJob(configWithJob.id)"
|
||||
>
|
||||
<span v-if="triggeringJobs.has(config.id)" class="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin mr-1"></span>
|
||||
<span
|
||||
v-if="triggeringJobs.has(configWithJob.id)"
|
||||
class="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin mr-1"
|
||||
></span>
|
||||
<svg v-else class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6-4h8a2 2 0 012 2v8a2 2 0 01-2 2H8a2 2 0 01-2-2V8a2 2 0 012-2z" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6-4h8a2 2 0 012 2v8a2 2 0 01-2 2H8a2 2 0 01-2-2V8a2 2 0 012-2z"
|
||||
/>
|
||||
</svg>
|
||||
{{ triggeringJobs.has(config.id) ? 'Starting...' : 'Run Now' }}
|
||||
{{
|
||||
configWithJob.isRunning
|
||||
? 'Running'
|
||||
: triggeringJobs.has(configWithJob.id)
|
||||
? 'Starting...'
|
||||
: 'Run Now'
|
||||
}}
|
||||
</Button>
|
||||
|
||||
<Badge
|
||||
:variant="config.enabled ? 'green' : 'gray'"
|
||||
<Badge
|
||||
:variant="configWithJob.isRunning ? 'blue' : configWithJob.enabled ? 'green' : 'gray'"
|
||||
size="sm"
|
||||
>
|
||||
{{ config.enabled ? 'Active' : 'Inactive' }}
|
||||
{{ configWithJob.isRunning ? 'Running' : configWithJob.enabled ? 'Active' : 'Inactive' }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress information for running jobs -->
|
||||
<div
|
||||
v-if="configWithJob.isRunning && configWithJob.jobStats"
|
||||
class="mb-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4"
|
||||
>
|
||||
<div class="flex justify-between text-sm text-blue-700 dark:text-blue-300 mb-3">
|
||||
<span class="font-medium">{{ configWithJob.jobStats.percentage }}% complete</span>
|
||||
<span>{{ configWithJob.jobStats.speed }}</span>
|
||||
</div>
|
||||
<div class="w-full bg-blue-200 dark:bg-blue-700 rounded-full h-2 mb-3">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: `${configWithJob.jobStats.percentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-blue-700 dark:text-blue-300">
|
||||
<div>
|
||||
<span class="font-medium">Transferred:</span> {{ configWithJob.jobStats.transferredBytes }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Elapsed:</span> {{ configWithJob.jobStats.elapsedTime }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">ETA:</span> {{ configWithJob.jobStats.eta }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Files:</span> {{ configWithJob.jobStats.transfers }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule and status information -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Schedule</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ config.schedule }}
|
||||
{{ configWithJob.schedule }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Last Run</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ config.lastRunAt ? formatDate(config.lastRunAt) : 'Never' }}
|
||||
{{ configWithJob.lastRunAt ? formatDate(configWithJob.lastRunAt) : 'Never' }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Status</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ config.lastRunStatus || 'Not run yet' }}
|
||||
{{ configWithJob.isRunning ? 'Running' : configWithJob.lastRunStatus || 'Not run yet' }}
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for adding new backup job -->
|
||||
<Sheet v-model:open="showConfigModal">
|
||||
<SheetContent class="w-full max-w-4xl max-h-[90vh] overflow-auto">
|
||||
<SheetTitle class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
|
||||
@@ -4,15 +4,29 @@ import { useQuery } from '@vue/apollo-composable';
|
||||
|
||||
import { BACKUP_JOBS_QUERY } from './backup-jobs.query';
|
||||
import BackupJobConfig from './BackupJobConfig.vue';
|
||||
import BackupEntry from './BackupEntry.vue';
|
||||
|
||||
const showSystemJobs = ref(false);
|
||||
|
||||
const { result, loading, error, refetch } = useQuery(
|
||||
BACKUP_JOBS_QUERY,
|
||||
() => ({ showSystemJobs: showSystemJobs.value }),
|
||||
);
|
||||
const { result, loading, error, refetch } = useQuery(BACKUP_JOBS_QUERY, {}, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval: 5000, // Poll every 5 seconds for real-time updates
|
||||
});
|
||||
|
||||
const backupJobs = computed(() => result.value?.backup?.jobs || []);
|
||||
const backupJobs = computed(() => {
|
||||
const allJobs = result.value?.backup?.jobs || [];
|
||||
|
||||
if (showSystemJobs.value) {
|
||||
return allJobs;
|
||||
}
|
||||
|
||||
return allJobs.filter(job => !job.group || job.group.toLowerCase() !== 'system');
|
||||
});
|
||||
|
||||
// Enhanced refresh function that forces a network request
|
||||
const refreshJobs = async () => {
|
||||
await refetch();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -22,7 +36,7 @@ const backupJobs = computed(() => result.value?.backup?.jobs || []);
|
||||
<button
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
:disabled="loading"
|
||||
@click="() => refetch()"
|
||||
@click="refreshJobs"
|
||||
>
|
||||
{{ loading ? 'Refreshing...' : 'Refresh' }}
|
||||
</button>
|
||||
@@ -83,79 +97,11 @@ const backupJobs = computed(() => result.value?.backup?.jobs || []);
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div
|
||||
<BackupEntry
|
||||
v-for="job in backupJobs"
|
||||
:key="job.id"
|
||||
class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 shadow-sm"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-3 h-3 bg-green-400 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
{{ job.type || 'Backup Job' }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Job ID: {{ job.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div v-if="job.stats?.formattedBytes" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Bytes Transferred</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.stats.formattedBytes }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats?.transfers" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Files Transferred</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.stats.transfers }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats?.formattedSpeed" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Transfer Speed</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ job.stats.formattedSpeed }}/s</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats?.formattedElapsedTime" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Elapsed Time</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.stats.formattedElapsedTime }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats?.formattedEta" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">ETA</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
|
||||
{{ job.stats.formattedEta }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats?.percentage" class="bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Progress</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ job.stats.percentage }}%</dd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="job.stats?.percentage" class="mt-4">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: `${job.stats.percentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:job="job"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { graphql } from '~/composables/gql/gql';
|
||||
|
||||
|
||||
export const BACKUP_STATS_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment BackupStats on BackupJobStats {
|
||||
fragment BackupStats on RCloneJobStats {
|
||||
bytes
|
||||
speed
|
||||
eta
|
||||
@@ -30,20 +30,21 @@ export const BACKUP_STATS_FRAGMENT = graphql(/* GraphQL */ `
|
||||
formattedSpeed
|
||||
formattedElapsedTime
|
||||
formattedEta
|
||||
group
|
||||
finished
|
||||
success
|
||||
error
|
||||
}
|
||||
`);
|
||||
|
||||
export const BACKUP_JOBS_QUERY = graphql(/* GraphQL */ `
|
||||
query BackupJobs($showSystemJobs: Boolean) {
|
||||
query BackupJobs {
|
||||
backup {
|
||||
id
|
||||
jobs(showSystemJobs: $showSystemJobs) {
|
||||
jobs {
|
||||
id
|
||||
group
|
||||
configId
|
||||
finished
|
||||
success
|
||||
error
|
||||
detailedStatus
|
||||
stats {
|
||||
...BackupStats
|
||||
}
|
||||
@@ -57,6 +58,11 @@ export const BACKUP_JOB_QUERY = graphql(/* GraphQL */ `
|
||||
backupJob(jobId: $jobId) {
|
||||
id
|
||||
group
|
||||
configId
|
||||
finished
|
||||
success
|
||||
error
|
||||
detailedStatus
|
||||
stats {
|
||||
...BackupStats
|
||||
}
|
||||
@@ -80,6 +86,7 @@ export const BACKUP_JOB_CONFIGS_QUERY = graphql(/* GraphQL */ `
|
||||
updatedAt
|
||||
lastRunAt
|
||||
lastRunStatus
|
||||
currentJobId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +115,7 @@ export const CREATE_BACKUP_JOB_CONFIG_MUTATION = graphql(/* GraphQL */ `
|
||||
enabled
|
||||
createdAt
|
||||
updatedAt
|
||||
currentJobId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +136,7 @@ export const UPDATE_BACKUP_JOB_CONFIG_MUTATION = graphql(/* GraphQL */ `
|
||||
updatedAt
|
||||
lastRunAt
|
||||
lastRunStatus
|
||||
currentJobId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,6 +165,7 @@ export const TOGGLE_BACKUP_JOB_CONFIG_MUTATION = graphql(/* GraphQL */ `
|
||||
updatedAt
|
||||
lastRunAt
|
||||
lastRunStatus
|
||||
currentJobId
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +197,6 @@ export const BACKUP_JOB_PROGRESS_SUBSCRIPTION = graphql(/* GraphQL */ `
|
||||
subscription BackupJobProgress($jobId: PrefixedID!) {
|
||||
backupJobProgress(jobId: $jobId) {
|
||||
id
|
||||
type
|
||||
stats {
|
||||
...BackupStats
|
||||
}
|
||||
|
||||
@@ -20,17 +20,18 @@ type Documents = {
|
||||
"\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n": typeof types.CreateApiKeyDocument,
|
||||
"\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n": typeof types.DeleteApiKeyDocument,
|
||||
"\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n": typeof types.ApiKeyMetaDocument,
|
||||
"\n query BackupJobs($showSystemJobs: Boolean) {\n backup {\n id\n jobs(showSystemJobs: $showSystemJobs) {\n id\n group\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n": typeof types.BackupJobsDocument,
|
||||
"\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n stats\n }\n }\n": typeof types.BackupJobDocument,
|
||||
"\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": typeof types.BackupJobConfigsDocument,
|
||||
"\n fragment BackupStats on RCloneJobStats {\n bytes\n speed\n eta\n elapsedTime\n percentage\n checks\n deletes\n errors\n fatalError\n lastError\n renames\n retryError\n serverSideCopies\n serverSideCopyBytes\n serverSideMoves\n serverSideMoveBytes\n totalBytes\n totalChecks\n totalTransfers\n transferTime\n transfers\n transferring\n checking\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n": typeof types.BackupStatsFragmentDoc,
|
||||
"\n query BackupJobs {\n backup {\n id\n jobs {\n id\n group\n configId\n finished\n success\n error\n detailedStatus\n stats {\n ...BackupStats\n }\n }\n }\n }\n": typeof types.BackupJobsDocument,
|
||||
"\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n configId\n finished\n success\n error\n detailedStatus\n stats {\n ...BackupStats\n }\n }\n }\n": typeof types.BackupJobDocument,
|
||||
"\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n": typeof types.BackupJobConfigsDocument,
|
||||
"\n query BackupJobConfigForm($input: BackupJobConfigFormInput) {\n backupJobConfigForm(input: $input) {\n id\n dataSchema\n uiSchema\n }\n }\n": typeof types.BackupJobConfigFormDocument,
|
||||
"\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n }\n": typeof types.CreateBackupJobConfigDocument,
|
||||
"\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": typeof types.UpdateBackupJobConfigDocument,
|
||||
"\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n currentJobId\n }\n }\n }\n": typeof types.CreateBackupJobConfigDocument,
|
||||
"\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n": typeof types.UpdateBackupJobConfigDocument,
|
||||
"\n mutation DeleteBackupJobConfig($id: String!) {\n backup {\n deleteBackupJobConfig(id: $id)\n }\n }\n": typeof types.DeleteBackupJobConfigDocument,
|
||||
"\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": typeof types.ToggleBackupJobConfigDocument,
|
||||
"\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n": typeof types.ToggleBackupJobConfigDocument,
|
||||
"\n mutation TriggerBackupJob($id: PrefixedID!) {\n backup {\n triggerJob(id: $id) {\n status\n jobId\n }\n }\n }\n": typeof types.TriggerBackupJobDocument,
|
||||
"\n mutation InitiateBackup($input: InitiateBackupInput!) {\n backup {\n initiateBackup(input: $input) {\n status\n jobId\n }\n }\n }\n": typeof types.InitiateBackupDocument,
|
||||
"\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n": typeof types.BackupJobProgressDocument,
|
||||
"\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n stats {\n ...BackupStats\n }\n }\n }\n": typeof types.BackupJobProgressDocument,
|
||||
"\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n": typeof types.GetConnectSettingsFormDocument,
|
||||
"\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n": typeof types.UpdateConnectSettingsDocument,
|
||||
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": typeof types.LogFilesDocument,
|
||||
@@ -68,17 +69,18 @@ const documents: Documents = {
|
||||
"\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n }\n }\n": types.CreateApiKeyDocument,
|
||||
"\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n": types.DeleteApiKeyDocument,
|
||||
"\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n": types.ApiKeyMetaDocument,
|
||||
"\n query BackupJobs($showSystemJobs: Boolean) {\n backup {\n id\n jobs(showSystemJobs: $showSystemJobs) {\n id\n group\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n": types.BackupJobsDocument,
|
||||
"\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n stats\n }\n }\n": types.BackupJobDocument,
|
||||
"\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": types.BackupJobConfigsDocument,
|
||||
"\n fragment BackupStats on RCloneJobStats {\n bytes\n speed\n eta\n elapsedTime\n percentage\n checks\n deletes\n errors\n fatalError\n lastError\n renames\n retryError\n serverSideCopies\n serverSideCopyBytes\n serverSideMoves\n serverSideMoveBytes\n totalBytes\n totalChecks\n totalTransfers\n transferTime\n transfers\n transferring\n checking\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n": types.BackupStatsFragmentDoc,
|
||||
"\n query BackupJobs {\n backup {\n id\n jobs {\n id\n group\n configId\n finished\n success\n error\n detailedStatus\n stats {\n ...BackupStats\n }\n }\n }\n }\n": types.BackupJobsDocument,
|
||||
"\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n configId\n finished\n success\n error\n detailedStatus\n stats {\n ...BackupStats\n }\n }\n }\n": types.BackupJobDocument,
|
||||
"\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n": types.BackupJobConfigsDocument,
|
||||
"\n query BackupJobConfigForm($input: BackupJobConfigFormInput) {\n backupJobConfigForm(input: $input) {\n id\n dataSchema\n uiSchema\n }\n }\n": types.BackupJobConfigFormDocument,
|
||||
"\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n }\n": types.CreateBackupJobConfigDocument,
|
||||
"\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": types.UpdateBackupJobConfigDocument,
|
||||
"\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n currentJobId\n }\n }\n }\n": types.CreateBackupJobConfigDocument,
|
||||
"\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n": types.UpdateBackupJobConfigDocument,
|
||||
"\n mutation DeleteBackupJobConfig($id: String!) {\n backup {\n deleteBackupJobConfig(id: $id)\n }\n }\n": types.DeleteBackupJobConfigDocument,
|
||||
"\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n": types.ToggleBackupJobConfigDocument,
|
||||
"\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n": types.ToggleBackupJobConfigDocument,
|
||||
"\n mutation TriggerBackupJob($id: PrefixedID!) {\n backup {\n triggerJob(id: $id) {\n status\n jobId\n }\n }\n }\n": types.TriggerBackupJobDocument,
|
||||
"\n mutation InitiateBackup($input: InitiateBackupInput!) {\n backup {\n initiateBackup(input: $input) {\n status\n jobId\n }\n }\n }\n": types.InitiateBackupDocument,
|
||||
"\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n": types.BackupJobProgressDocument,
|
||||
"\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n stats {\n ...BackupStats\n }\n }\n }\n": types.BackupJobProgressDocument,
|
||||
"\n query GetConnectSettingsForm {\n connect {\n id\n settings {\n id\n dataSchema\n uiSchema\n values {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n }\n }\n": types.GetConnectSettingsFormDocument,
|
||||
"\n mutation UpdateConnectSettings($input: ApiSettingsInput!) {\n updateApiSettings(input: $input) {\n sandbox\n extraOrigins\n accessType\n forwardType\n port\n ssoUserIds\n }\n }\n": types.UpdateConnectSettingsDocument,
|
||||
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": types.LogFilesDocument,
|
||||
@@ -151,15 +153,19 @@ export function graphql(source: "\n query ApiKeyMeta {\n apiKeyPossibleRoles
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query BackupJobs($showSystemJobs: Boolean) {\n backup {\n id\n jobs(showSystemJobs: $showSystemJobs) {\n id\n group\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n"): (typeof documents)["\n query BackupJobs($showSystemJobs: Boolean) {\n backup {\n id\n jobs(showSystemJobs: $showSystemJobs) {\n id\n group\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment BackupStats on RCloneJobStats {\n bytes\n speed\n eta\n elapsedTime\n percentage\n checks\n deletes\n errors\n fatalError\n lastError\n renames\n retryError\n serverSideCopies\n serverSideCopyBytes\n serverSideMoves\n serverSideMoveBytes\n totalBytes\n totalChecks\n totalTransfers\n transferTime\n transfers\n transferring\n checking\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n"): (typeof documents)["\n fragment BackupStats on RCloneJobStats {\n bytes\n speed\n eta\n elapsedTime\n percentage\n checks\n deletes\n errors\n fatalError\n lastError\n renames\n retryError\n serverSideCopies\n serverSideCopyBytes\n serverSideMoves\n serverSideMoveBytes\n totalBytes\n totalChecks\n totalTransfers\n transferTime\n transfers\n transferring\n checking\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n stats\n }\n }\n"): (typeof documents)["\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n stats\n }\n }\n"];
|
||||
export function graphql(source: "\n query BackupJobs {\n backup {\n id\n jobs {\n id\n group\n configId\n finished\n success\n error\n detailedStatus\n stats {\n ...BackupStats\n }\n }\n }\n }\n"): (typeof documents)["\n query BackupJobs {\n backup {\n id\n jobs {\n id\n group\n configId\n finished\n success\n error\n detailedStatus\n stats {\n ...BackupStats\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"): (typeof documents)["\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n configId\n finished\n success\n error\n detailedStatus\n stats {\n ...BackupStats\n }\n }\n }\n"): (typeof documents)["\n query BackupJob($jobId: PrefixedID!) {\n backupJob(jobId: $jobId) {\n id\n group\n configId\n finished\n success\n error\n detailedStatus\n stats {\n ...BackupStats\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n"): (typeof documents)["\n query BackupJobConfigs {\n backup {\n id\n configs {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -167,11 +173,11 @@ export function graphql(source: "\n query BackupJobConfigForm($input: BackupJob
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n }\n"): (typeof documents)["\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n currentJobId\n }\n }\n }\n"): (typeof documents)["\n mutation CreateBackupJobConfig($input: CreateBackupJobConfigInput!) {\n backup {\n createBackupJobConfig(input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n currentJobId\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateBackupJobConfig($id: String!, $input: UpdateBackupJobConfigInput!) {\n backup {\n updateBackupJobConfig(id: $id, input: $input) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -179,7 +185,7 @@ export function graphql(source: "\n mutation DeleteBackupJobConfig($id: String!
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"): (typeof documents)["\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n"): (typeof documents)["\n mutation ToggleBackupJobConfig($id: String!) {\n backup {\n toggleJobConfig(id: $id) {\n id\n name\n sourcePath\n remoteName\n destinationPath\n schedule\n enabled\n createdAt\n updatedAt\n lastRunAt\n lastRunStatus\n currentJobId\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -191,7 +197,7 @@ export function graphql(source: "\n mutation InitiateBackup($input: InitiateBac
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n"): (typeof documents)["\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n type\n stats\n formattedBytes\n formattedSpeed\n formattedElapsedTime\n formattedEta\n }\n }\n"];
|
||||
export function graphql(source: "\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n stats {\n ...BackupStats\n }\n }\n }\n"): (typeof documents)["\n subscription BackupJobProgress($jobId: PrefixedID!) {\n backupJobProgress(jobId: $jobId) {\n id\n stats {\n ...BackupStats\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -377,46 +377,17 @@ export type Backup = Node & {
|
||||
__typename?: 'Backup';
|
||||
configs: Array<BackupJobConfig>;
|
||||
id: Scalars['PrefixedID']['output'];
|
||||
jobs: Array<BackupJob>;
|
||||
jobs: Array<RCloneJob>;
|
||||
/** Get the status for the backup service */
|
||||
status: BackupStatus;
|
||||
};
|
||||
|
||||
|
||||
export type BackupJobsArgs = {
|
||||
showSystemJobs?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
};
|
||||
|
||||
export type BackupJob = {
|
||||
__typename?: 'BackupJob';
|
||||
/** Configuration ID that triggered this job */
|
||||
configId?: Maybe<Scalars['PrefixedID']['output']>;
|
||||
/** Detailed status of the job */
|
||||
detailedStatus?: Maybe<Scalars['String']['output']>;
|
||||
/** Formatted bytes transferred */
|
||||
formattedBytes?: Maybe<Scalars['String']['output']>;
|
||||
/** Formatted elapsed time */
|
||||
formattedElapsedTime?: Maybe<Scalars['String']['output']>;
|
||||
/** Formatted ETA */
|
||||
formattedEta?: Maybe<Scalars['String']['output']>;
|
||||
/** Formatted transfer speed */
|
||||
formattedSpeed?: Maybe<Scalars['String']['output']>;
|
||||
/** RClone group for the job */
|
||||
group?: Maybe<Scalars['String']['output']>;
|
||||
/** Job ID */
|
||||
id: Scalars['PrefixedID']['output'];
|
||||
/** Progress percentage (0-100) */
|
||||
progressPercentage?: Maybe<Scalars['Float']['output']>;
|
||||
/** Job status and statistics */
|
||||
stats: Scalars['JSON']['output'];
|
||||
/** Job type (e.g., sync/copy) */
|
||||
type: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type BackupJobConfig = Node & {
|
||||
__typename?: 'BackupJobConfig';
|
||||
/** When this config was created */
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
/** Current running job ID */
|
||||
currentJobId?: Maybe<Scalars['String']['output']>;
|
||||
/** Destination path on the remote */
|
||||
destinationPath: Scalars['String']['output'];
|
||||
/** Whether this backup job is enabled */
|
||||
@@ -458,8 +429,12 @@ export type BackupMutations = {
|
||||
createBackupJobConfig: BackupJobConfig;
|
||||
/** Delete a backup job configuration */
|
||||
deleteBackupJobConfig: Scalars['Boolean']['output'];
|
||||
/** Forget all finished backup jobs to clean up the job list */
|
||||
forgetFinishedBackupJobs: BackupStatus;
|
||||
/** Initiates a backup using a configured remote. */
|
||||
initiateBackup: BackupStatus;
|
||||
/** Stop all running backup jobs */
|
||||
stopAllBackupJobs: BackupStatus;
|
||||
/** Toggle a backup job configuration enabled/disabled */
|
||||
toggleJobConfig?: Maybe<BackupJobConfig>;
|
||||
/** Manually trigger a backup job using existing configuration */
|
||||
@@ -1349,7 +1324,7 @@ export type Query = {
|
||||
/** Get backup service information */
|
||||
backup: Backup;
|
||||
/** Get status of a specific backup job */
|
||||
backupJob?: Maybe<BackupJob>;
|
||||
backupJob?: Maybe<RCloneJob>;
|
||||
/** Get a specific backup job configuration */
|
||||
backupJobConfig?: Maybe<BackupJobConfig>;
|
||||
/** Get the JSON schema for backup job configuration form */
|
||||
@@ -1455,6 +1430,86 @@ export type RCloneDrive = {
|
||||
options: Scalars['JSON']['output'];
|
||||
};
|
||||
|
||||
export type RCloneJob = {
|
||||
__typename?: 'RCloneJob';
|
||||
/** Configuration ID that triggered this job */
|
||||
configId?: Maybe<Scalars['PrefixedID']['output']>;
|
||||
/** Detailed status of the job */
|
||||
detailedStatus?: Maybe<Scalars['String']['output']>;
|
||||
/** Error message if job failed */
|
||||
error?: Maybe<Scalars['String']['output']>;
|
||||
/** Whether the job is finished */
|
||||
finished?: Maybe<Scalars['Boolean']['output']>;
|
||||
/** RClone group for the job */
|
||||
group?: Maybe<Scalars['String']['output']>;
|
||||
/** Job ID */
|
||||
id: Scalars['PrefixedID']['output'];
|
||||
/** Progress percentage (0-100) */
|
||||
progressPercentage?: Maybe<Scalars['Float']['output']>;
|
||||
/** Job status and statistics */
|
||||
stats?: Maybe<RCloneJobStats>;
|
||||
/** Whether the job was successful */
|
||||
success?: Maybe<Scalars['Boolean']['output']>;
|
||||
};
|
||||
|
||||
export type RCloneJobStats = {
|
||||
__typename?: 'RCloneJobStats';
|
||||
/** Bytes transferred */
|
||||
bytes?: Maybe<Scalars['Float']['output']>;
|
||||
/** Currently checking files */
|
||||
checking?: Maybe<Scalars['JSON']['output']>;
|
||||
/** Number of checks completed */
|
||||
checks?: Maybe<Scalars['Float']['output']>;
|
||||
/** Number of deletes completed */
|
||||
deletes?: Maybe<Scalars['Float']['output']>;
|
||||
/** Elapsed time in seconds */
|
||||
elapsedTime?: Maybe<Scalars['Float']['output']>;
|
||||
/** Number of errors encountered */
|
||||
errors?: Maybe<Scalars['Float']['output']>;
|
||||
/** Estimated time to completion in seconds */
|
||||
eta?: Maybe<Scalars['Float']['output']>;
|
||||
/** Whether a fatal error occurred */
|
||||
fatalError?: Maybe<Scalars['Boolean']['output']>;
|
||||
/** Human-readable bytes transferred */
|
||||
formattedBytes?: Maybe<Scalars['String']['output']>;
|
||||
/** Human-readable elapsed time */
|
||||
formattedElapsedTime?: Maybe<Scalars['String']['output']>;
|
||||
/** Human-readable ETA */
|
||||
formattedEta?: Maybe<Scalars['String']['output']>;
|
||||
/** Human-readable transfer speed */
|
||||
formattedSpeed?: Maybe<Scalars['String']['output']>;
|
||||
/** Last error message */
|
||||
lastError?: Maybe<Scalars['String']['output']>;
|
||||
/** Progress percentage (0-100) */
|
||||
percentage?: Maybe<Scalars['Float']['output']>;
|
||||
/** Number of renames completed */
|
||||
renames?: Maybe<Scalars['Float']['output']>;
|
||||
/** Whether there is a retry error */
|
||||
retryError?: Maybe<Scalars['Boolean']['output']>;
|
||||
/** Number of server-side copies */
|
||||
serverSideCopies?: Maybe<Scalars['Float']['output']>;
|
||||
/** Bytes in server-side copies */
|
||||
serverSideCopyBytes?: Maybe<Scalars['Float']['output']>;
|
||||
/** Bytes in server-side moves */
|
||||
serverSideMoveBytes?: Maybe<Scalars['Float']['output']>;
|
||||
/** Number of server-side moves */
|
||||
serverSideMoves?: Maybe<Scalars['Float']['output']>;
|
||||
/** Transfer speed in bytes/sec */
|
||||
speed?: Maybe<Scalars['Float']['output']>;
|
||||
/** Total bytes to transfer */
|
||||
totalBytes?: Maybe<Scalars['Float']['output']>;
|
||||
/** Total checks to perform */
|
||||
totalChecks?: Maybe<Scalars['Float']['output']>;
|
||||
/** Total transfers to perform */
|
||||
totalTransfers?: Maybe<Scalars['Float']['output']>;
|
||||
/** Time spent transferring in seconds */
|
||||
transferTime?: Maybe<Scalars['Float']['output']>;
|
||||
/** Currently transferring files */
|
||||
transferring?: Maybe<Scalars['JSON']['output']>;
|
||||
/** Number of transfers completed */
|
||||
transfers?: Maybe<Scalars['Float']['output']>;
|
||||
};
|
||||
|
||||
/** RClone related mutations */
|
||||
export type RCloneMutations = {
|
||||
__typename?: 'RCloneMutations';
|
||||
@@ -1664,7 +1719,7 @@ export type Subscription = {
|
||||
__typename?: 'Subscription';
|
||||
arraySubscription: UnraidArray;
|
||||
/** Subscribe to real-time backup job progress updates */
|
||||
backupJobProgress?: Maybe<BackupJob>;
|
||||
backupJobProgress?: Maybe<RCloneJob>;
|
||||
displaySubscription: Display;
|
||||
infoSubscription: Info;
|
||||
logFile: LogFileContent;
|
||||
@@ -1758,6 +1813,7 @@ export type UnraidArray = Node & {
|
||||
export type UpdateBackupJobConfigInput = {
|
||||
destinationPath?: InputMaybe<Scalars['String']['input']>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
lastRunStatus?: InputMaybe<Scalars['String']['input']>;
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
rcloneOptions?: InputMaybe<Scalars['JSON']['input']>;
|
||||
remoteName?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -2127,24 +2183,30 @@ export type ApiKeyMetaQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type ApiKeyMetaQuery = { __typename?: 'Query', apiKeyPossibleRoles: Array<Role>, apiKeyPossiblePermissions: Array<{ __typename?: 'Permission', resource: Resource, actions: Array<string> }> };
|
||||
|
||||
export type BackupJobsQueryVariables = Exact<{
|
||||
showSystemJobs?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
}>;
|
||||
export type BackupStatsFragment = { __typename?: 'RCloneJobStats', bytes?: number | null, speed?: number | null, eta?: number | null, elapsedTime?: number | null, percentage?: number | null, checks?: number | null, deletes?: number | null, errors?: number | null, fatalError?: boolean | null, lastError?: string | null, renames?: number | null, retryError?: boolean | null, serverSideCopies?: number | null, serverSideCopyBytes?: number | null, serverSideMoves?: number | null, serverSideMoveBytes?: number | null, totalBytes?: number | null, totalChecks?: number | null, totalTransfers?: number | null, transferTime?: number | null, transfers?: number | null, transferring?: any | null, checking?: any | null, formattedBytes?: string | null, formattedSpeed?: string | null, formattedElapsedTime?: string | null, formattedEta?: string | null } & { ' $fragmentName'?: 'BackupStatsFragment' };
|
||||
|
||||
export type BackupJobsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type BackupJobsQuery = { __typename?: 'Query', backup: { __typename?: 'Backup', id: string, jobs: Array<{ __typename?: 'BackupJob', id: string, group?: string | null, stats: any, formattedBytes?: string | null, formattedSpeed?: string | null, formattedElapsedTime?: string | null, formattedEta?: string | null }> } };
|
||||
export type BackupJobsQuery = { __typename?: 'Query', backup: { __typename?: 'Backup', id: string, jobs: Array<{ __typename?: 'RCloneJob', id: string, group?: string | null, configId?: string | null, finished?: boolean | null, success?: boolean | null, error?: string | null, detailedStatus?: string | null, stats?: (
|
||||
{ __typename?: 'RCloneJobStats' }
|
||||
& { ' $fragmentRefs'?: { 'BackupStatsFragment': BackupStatsFragment } }
|
||||
) | null }> } };
|
||||
|
||||
export type BackupJobQueryVariables = Exact<{
|
||||
jobId: Scalars['PrefixedID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BackupJobQuery = { __typename?: 'Query', backupJob?: { __typename?: 'BackupJob', id: string, group?: string | null, stats: any } | null };
|
||||
export type BackupJobQuery = { __typename?: 'Query', backupJob?: { __typename?: 'RCloneJob', id: string, group?: string | null, configId?: string | null, finished?: boolean | null, success?: boolean | null, error?: string | null, detailedStatus?: string | null, stats?: (
|
||||
{ __typename?: 'RCloneJobStats' }
|
||||
& { ' $fragmentRefs'?: { 'BackupStatsFragment': BackupStatsFragment } }
|
||||
) | null } | null };
|
||||
|
||||
export type BackupJobConfigsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type BackupJobConfigsQuery = { __typename?: 'Query', backup: { __typename?: 'Backup', id: string, configs: Array<{ __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, lastRunAt?: string | null, lastRunStatus?: string | null }> } };
|
||||
export type BackupJobConfigsQuery = { __typename?: 'Query', backup: { __typename?: 'Backup', id: string, configs: Array<{ __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, lastRunAt?: string | null, lastRunStatus?: string | null, currentJobId?: string | null }> } };
|
||||
|
||||
export type BackupJobConfigFormQueryVariables = Exact<{
|
||||
input?: InputMaybe<BackupJobConfigFormInput>;
|
||||
@@ -2158,7 +2220,7 @@ export type CreateBackupJobConfigMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', createBackupJobConfig: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string } } };
|
||||
export type CreateBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', createBackupJobConfig: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, currentJobId?: string | null } } };
|
||||
|
||||
export type UpdateBackupJobConfigMutationVariables = Exact<{
|
||||
id: Scalars['String']['input'];
|
||||
@@ -2166,7 +2228,7 @@ export type UpdateBackupJobConfigMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', updateBackupJobConfig?: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, lastRunAt?: string | null, lastRunStatus?: string | null } | null } };
|
||||
export type UpdateBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', updateBackupJobConfig?: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, lastRunAt?: string | null, lastRunStatus?: string | null, currentJobId?: string | null } | null } };
|
||||
|
||||
export type DeleteBackupJobConfigMutationVariables = Exact<{
|
||||
id: Scalars['String']['input'];
|
||||
@@ -2180,7 +2242,7 @@ export type ToggleBackupJobConfigMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ToggleBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', toggleJobConfig?: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, lastRunAt?: string | null, lastRunStatus?: string | null } | null } };
|
||||
export type ToggleBackupJobConfigMutation = { __typename?: 'Mutation', backup: { __typename?: 'BackupMutations', toggleJobConfig?: { __typename?: 'BackupJobConfig', id: string, name: string, sourcePath: string, remoteName: string, destinationPath: string, schedule: string, enabled: boolean, createdAt: string, updatedAt: string, lastRunAt?: string | null, lastRunStatus?: string | null, currentJobId?: string | null } | null } };
|
||||
|
||||
export type TriggerBackupJobMutationVariables = Exact<{
|
||||
id: Scalars['PrefixedID']['input'];
|
||||
@@ -2201,7 +2263,10 @@ export type BackupJobProgressSubscriptionVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type BackupJobProgressSubscription = { __typename?: 'Subscription', backupJobProgress?: { __typename?: 'BackupJob', id: string, type: string, stats: any, formattedBytes?: string | null, formattedSpeed?: string | null, formattedElapsedTime?: string | null, formattedEta?: string | null } | null };
|
||||
export type BackupJobProgressSubscription = { __typename?: 'Subscription', backupJobProgress?: { __typename?: 'RCloneJob', id: string, stats?: (
|
||||
{ __typename?: 'RCloneJobStats' }
|
||||
& { ' $fragmentRefs'?: { 'BackupStatsFragment': BackupStatsFragment } }
|
||||
) | null } | null };
|
||||
|
||||
export type GetConnectSettingsFormQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -2390,6 +2455,7 @@ export type SetupRemoteAccessMutationVariables = Exact<{
|
||||
|
||||
export type SetupRemoteAccessMutation = { __typename?: 'Mutation', setupRemoteAccess: boolean };
|
||||
|
||||
export const BackupStatsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BackupStats"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RCloneJobStats"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bytes"}},{"kind":"Field","name":{"kind":"Name","value":"speed"}},{"kind":"Field","name":{"kind":"Name","value":"eta"}},{"kind":"Field","name":{"kind":"Name","value":"elapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"percentage"}},{"kind":"Field","name":{"kind":"Name","value":"checks"}},{"kind":"Field","name":{"kind":"Name","value":"deletes"}},{"kind":"Field","name":{"kind":"Name","value":"errors"}},{"kind":"Field","name":{"kind":"Name","value":"fatalError"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"renames"}},{"kind":"Field","name":{"kind":"Name","value":"retryError"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideCopies"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideCopyBytes"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideMoves"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideMoveBytes"}},{"kind":"Field","name":{"kind":"Name","value":"totalBytes"}},{"kind":"Field","name":{"kind":"Name","value":"totalChecks"}},{"kind":"Field","name":{"kind":"Name","value":"totalTransfers"}},{"kind":"Field","name":{"kind":"Name","value":"transferTime"}},{"kind":"Field","name":{"kind":"Name","value":"transfers"}},{"kind":"Field","name":{"kind":"Name","value":"transferring"}},{"kind":"Field","name":{"kind":"Name","value":"checking"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]} as unknown as DocumentNode<BackupStatsFragment, unknown>;
|
||||
export const NotificationFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Notification"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"formattedTimestamp"}}]}}]} as unknown as DocumentNode<NotificationFragmentFragment, unknown>;
|
||||
export const NotificationCountFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"NotificationCountFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationCounts"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"info"}},{"kind":"Field","name":{"kind":"Name","value":"warning"}},{"kind":"Field","name":{"kind":"Name","value":"alert"}}]}}]} as unknown as DocumentNode<NotificationCountFragmentFragment, unknown>;
|
||||
export const PartialCloudFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<PartialCloudFragment, unknown>;
|
||||
@@ -2399,17 +2465,17 @@ export const ApiKeysDocument = {"kind":"Document","definitions":[{"kind":"Operat
|
||||
export const CreateApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateApiKeyMutation, CreateApiKeyMutationVariables>;
|
||||
export const DeleteApiKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApiKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteApiKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"delete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<DeleteApiKeyMutation, DeleteApiKeyMutationVariables>;
|
||||
export const ApiKeyMetaDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ApiKeyMeta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKeyPossibleRoles"}},{"kind":"Field","name":{"kind":"Name","value":"apiKeyPossiblePermissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]} as unknown as DocumentNode<ApiKeyMetaQuery, ApiKeyMetaQueryVariables>;
|
||||
export const BackupJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"showSystemJobs"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"jobs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"showSystemJobs"},"value":{"kind":"Variable","name":{"kind":"Name","value":"showSystemJobs"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]}}]}}]} as unknown as DocumentNode<BackupJobsQuery, BackupJobsQueryVariables>;
|
||||
export const BackupJobDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJob"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJob"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"jobId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}}]}}]}}]} as unknown as DocumentNode<BackupJobQuery, BackupJobQueryVariables>;
|
||||
export const BackupJobConfigsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobConfigs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"configs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}}]}}]}}]}}]} as unknown as DocumentNode<BackupJobConfigsQuery, BackupJobConfigsQueryVariables>;
|
||||
export const BackupJobsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"jobs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"configId"}},{"kind":"Field","name":{"kind":"Name","value":"finished"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"detailedStatus"}},{"kind":"Field","name":{"kind":"Name","value":"stats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BackupStats"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BackupStats"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RCloneJobStats"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bytes"}},{"kind":"Field","name":{"kind":"Name","value":"speed"}},{"kind":"Field","name":{"kind":"Name","value":"eta"}},{"kind":"Field","name":{"kind":"Name","value":"elapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"percentage"}},{"kind":"Field","name":{"kind":"Name","value":"checks"}},{"kind":"Field","name":{"kind":"Name","value":"deletes"}},{"kind":"Field","name":{"kind":"Name","value":"errors"}},{"kind":"Field","name":{"kind":"Name","value":"fatalError"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"renames"}},{"kind":"Field","name":{"kind":"Name","value":"retryError"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideCopies"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideCopyBytes"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideMoves"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideMoveBytes"}},{"kind":"Field","name":{"kind":"Name","value":"totalBytes"}},{"kind":"Field","name":{"kind":"Name","value":"totalChecks"}},{"kind":"Field","name":{"kind":"Name","value":"totalTransfers"}},{"kind":"Field","name":{"kind":"Name","value":"transferTime"}},{"kind":"Field","name":{"kind":"Name","value":"transfers"}},{"kind":"Field","name":{"kind":"Name","value":"transferring"}},{"kind":"Field","name":{"kind":"Name","value":"checking"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]} as unknown as DocumentNode<BackupJobsQuery, BackupJobsQueryVariables>;
|
||||
export const BackupJobDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJob"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJob"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"jobId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"configId"}},{"kind":"Field","name":{"kind":"Name","value":"finished"}},{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"detailedStatus"}},{"kind":"Field","name":{"kind":"Name","value":"stats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BackupStats"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BackupStats"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RCloneJobStats"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bytes"}},{"kind":"Field","name":{"kind":"Name","value":"speed"}},{"kind":"Field","name":{"kind":"Name","value":"eta"}},{"kind":"Field","name":{"kind":"Name","value":"elapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"percentage"}},{"kind":"Field","name":{"kind":"Name","value":"checks"}},{"kind":"Field","name":{"kind":"Name","value":"deletes"}},{"kind":"Field","name":{"kind":"Name","value":"errors"}},{"kind":"Field","name":{"kind":"Name","value":"fatalError"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"renames"}},{"kind":"Field","name":{"kind":"Name","value":"retryError"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideCopies"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideCopyBytes"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideMoves"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideMoveBytes"}},{"kind":"Field","name":{"kind":"Name","value":"totalBytes"}},{"kind":"Field","name":{"kind":"Name","value":"totalChecks"}},{"kind":"Field","name":{"kind":"Name","value":"totalTransfers"}},{"kind":"Field","name":{"kind":"Name","value":"transferTime"}},{"kind":"Field","name":{"kind":"Name","value":"transfers"}},{"kind":"Field","name":{"kind":"Name","value":"transferring"}},{"kind":"Field","name":{"kind":"Name","value":"checking"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]} as unknown as DocumentNode<BackupJobQuery, BackupJobQueryVariables>;
|
||||
export const BackupJobConfigsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobConfigs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"configs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}},{"kind":"Field","name":{"kind":"Name","value":"currentJobId"}}]}}]}}]}}]} as unknown as DocumentNode<BackupJobConfigsQuery, BackupJobConfigsQueryVariables>;
|
||||
export const BackupJobConfigFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BackupJobConfigForm"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BackupJobConfigFormInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJobConfigForm"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}}]}}]}}]} as unknown as DocumentNode<BackupJobConfigFormQuery, BackupJobConfigFormQueryVariables>;
|
||||
export const CreateBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateBackupJobConfigInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createBackupJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]}}]} as unknown as DocumentNode<CreateBackupJobConfigMutation, CreateBackupJobConfigMutationVariables>;
|
||||
export const UpdateBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateBackupJobConfigInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateBackupJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateBackupJobConfigMutation, UpdateBackupJobConfigMutationVariables>;
|
||||
export const CreateBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateBackupJobConfigInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createBackupJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"currentJobId"}}]}}]}}]}}]} as unknown as DocumentNode<CreateBackupJobConfigMutation, CreateBackupJobConfigMutationVariables>;
|
||||
export const UpdateBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateBackupJobConfigInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateBackupJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}},{"kind":"Field","name":{"kind":"Name","value":"currentJobId"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateBackupJobConfigMutation, UpdateBackupJobConfigMutationVariables>;
|
||||
export const DeleteBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteBackupJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]}}]} as unknown as DocumentNode<DeleteBackupJobConfigMutation, DeleteBackupJobConfigMutationVariables>;
|
||||
export const ToggleBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ToggleBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toggleJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}}]}}]}}]}}]} as unknown as DocumentNode<ToggleBackupJobConfigMutation, ToggleBackupJobConfigMutationVariables>;
|
||||
export const ToggleBackupJobConfigDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ToggleBackupJobConfig"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toggleJobConfig"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePath"}},{"kind":"Field","name":{"kind":"Name","value":"remoteName"}},{"kind":"Field","name":{"kind":"Name","value":"destinationPath"}},{"kind":"Field","name":{"kind":"Name","value":"schedule"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastRunStatus"}},{"kind":"Field","name":{"kind":"Name","value":"currentJobId"}}]}}]}}]}}]} as unknown as DocumentNode<ToggleBackupJobConfigMutation, ToggleBackupJobConfigMutationVariables>;
|
||||
export const TriggerBackupJobDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TriggerBackupJob"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"triggerJob"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"jobId"}}]}}]}}]}}]} as unknown as DocumentNode<TriggerBackupJobMutation, TriggerBackupJobMutationVariables>;
|
||||
export const InitiateBackupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InitiateBackup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InitiateBackupInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"initiateBackup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"jobId"}}]}}]}}]}}]} as unknown as DocumentNode<InitiateBackupMutation, InitiateBackupMutationVariables>;
|
||||
export const BackupJobProgressDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"BackupJobProgress"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJobProgress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"jobId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"stats"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]}}]} as unknown as DocumentNode<BackupJobProgressSubscription, BackupJobProgressSubscriptionVariables>;
|
||||
export const BackupJobProgressDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"BackupJobProgress"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PrefixedID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"backupJobProgress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"jobId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"jobId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"stats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BackupStats"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BackupStats"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RCloneJobStats"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"bytes"}},{"kind":"Field","name":{"kind":"Name","value":"speed"}},{"kind":"Field","name":{"kind":"Name","value":"eta"}},{"kind":"Field","name":{"kind":"Name","value":"elapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"percentage"}},{"kind":"Field","name":{"kind":"Name","value":"checks"}},{"kind":"Field","name":{"kind":"Name","value":"deletes"}},{"kind":"Field","name":{"kind":"Name","value":"errors"}},{"kind":"Field","name":{"kind":"Name","value":"fatalError"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"renames"}},{"kind":"Field","name":{"kind":"Name","value":"retryError"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideCopies"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideCopyBytes"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideMoves"}},{"kind":"Field","name":{"kind":"Name","value":"serverSideMoveBytes"}},{"kind":"Field","name":{"kind":"Name","value":"totalBytes"}},{"kind":"Field","name":{"kind":"Name","value":"totalChecks"}},{"kind":"Field","name":{"kind":"Name","value":"totalTransfers"}},{"kind":"Field","name":{"kind":"Name","value":"transferTime"}},{"kind":"Field","name":{"kind":"Name","value":"transfers"}},{"kind":"Field","name":{"kind":"Name","value":"transferring"}},{"kind":"Field","name":{"kind":"Name","value":"checking"}},{"kind":"Field","name":{"kind":"Name","value":"formattedBytes"}},{"kind":"Field","name":{"kind":"Name","value":"formattedSpeed"}},{"kind":"Field","name":{"kind":"Name","value":"formattedElapsedTime"}},{"kind":"Field","name":{"kind":"Name","value":"formattedEta"}}]}}]} as unknown as DocumentNode<BackupJobProgressSubscription, BackupJobProgressSubscriptionVariables>;
|
||||
export const GetConnectSettingsFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectSettingsForm"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connect"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}},{"kind":"Field","name":{"kind":"Name","value":"values"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetConnectSettingsFormQuery, GetConnectSettingsFormQueryVariables>;
|
||||
export const UpdateConnectSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateConnectSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ApiSettingsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateApiSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sandbox"}},{"kind":"Field","name":{"kind":"Name","value":"extraOrigins"}},{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"ssoUserIds"}}]}}]}}]} as unknown as DocumentNode<UpdateConnectSettingsMutation, UpdateConnectSettingsMutationVariables>;
|
||||
export const LogFilesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"modifiedAt"}}]}}]}}]} as unknown as DocumentNode<LogFilesQuery, LogFilesQueryVariables>;
|
||||
|
||||
@@ -34,6 +34,29 @@ import { mergeAndDedup } from './merge';
|
||||
|
||||
const defaultCacheConfig: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
backup: {
|
||||
merge(_, incoming) {
|
||||
return incoming;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Backup: {
|
||||
fields: {
|
||||
jobs: {
|
||||
merge(_, incoming) {
|
||||
return incoming;
|
||||
},
|
||||
},
|
||||
configs: {
|
||||
merge(_, incoming) {
|
||||
return incoming;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Notifications: {
|
||||
fields: {
|
||||
list: {
|
||||
|
||||
Reference in New Issue
Block a user