feat: add support for provided shortId in volume, repository and backup and pass shortIds to service creation from config import
This commit is contained in:
parent
37d04d3b61
commit
cc9982b764
5 changed files with 103 additions and 25 deletions
|
|
@ -14,7 +14,7 @@ import { notificationsService } from "../notifications/notifications.service";
|
|||
import { repoMutex } from "../../core/repository-mutex";
|
||||
import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/utils/backend-compatibility";
|
||||
import path from "node:path";
|
||||
import { generateShortId } from "~/server/utils/id";
|
||||
import { generateShortId, isValidShortId } from "~/server/utils/id";
|
||||
|
||||
const runningBackups = new Map<number, AbortController>();
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ const getSchedule = async (scheduleId: number) => {
|
|||
return schedule;
|
||||
};
|
||||
|
||||
const createSchedule = async (data: CreateBackupScheduleBody) => {
|
||||
const createSchedule = async (data: CreateBackupScheduleBody, providedShortId?: string) => {
|
||||
if (!cron.validate(data.cronExpression)) {
|
||||
throw new BadRequestError("Invalid cron expression");
|
||||
}
|
||||
|
|
@ -95,6 +95,25 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||
throw new ConflictError("A backup schedule with this name already exists");
|
||||
}
|
||||
|
||||
// Use provided shortId if valid, otherwise generate a new one
|
||||
let shortId: string;
|
||||
if (providedShortId) {
|
||||
if (!isValidShortId(providedShortId)) {
|
||||
throw new BadRequestError(`Invalid shortId format: '${providedShortId}'. Must be 8 base64url characters.`);
|
||||
}
|
||||
const shortIdInUse = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.shortId, providedShortId),
|
||||
});
|
||||
if (shortIdInUse) {
|
||||
throw new ConflictError(
|
||||
`Schedule shortId '${providedShortId}' is already in use by schedule '${shortIdInUse.name}'`,
|
||||
);
|
||||
}
|
||||
shortId = providedShortId;
|
||||
} else {
|
||||
shortId = generateShortId();
|
||||
}
|
||||
|
||||
const volume = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.id, data.volumeId),
|
||||
});
|
||||
|
|
@ -127,7 +146,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||
includePatterns: data.includePatterns ?? [],
|
||||
oneFileSystem: data.oneFileSystem,
|
||||
nextBackupAt: nextBackupAt,
|
||||
shortId: generateShortId(),
|
||||
shortId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,9 @@ async function importVolumes(volumes: unknown[]): Promise<ImportResult> {
|
|||
continue;
|
||||
}
|
||||
|
||||
await volumeService.createVolume(v.name, v.config as BackendConfig);
|
||||
// Pass shortId from config if provided (for IaC reproducibility)
|
||||
const shortId = typeof v.shortId === "string" ? v.shortId : undefined;
|
||||
await volumeService.createVolume(v.name, v.config as BackendConfig, shortId);
|
||||
logger.info(`Initialized volume from config: ${v.name}`);
|
||||
result.succeeded++;
|
||||
|
||||
|
|
@ -257,10 +259,13 @@ async function importRepositories(repositories: unknown[]): Promise<ImportResult
|
|||
r.compressionMode === "auto" || r.compressionMode === "off" || r.compressionMode === "max"
|
||||
? r.compressionMode
|
||||
: undefined;
|
||||
// Pass shortId from config if provided (for IaC reproducibility)
|
||||
const shortId = typeof r.shortId === "string" ? r.shortId : undefined;
|
||||
await repoServiceModule.repositoriesService.createRepository(
|
||||
r.name,
|
||||
r.config as RepositoryConfig,
|
||||
compressionMode,
|
||||
shortId,
|
||||
);
|
||||
logger.info(`Initialized repository from config: ${r.name}`);
|
||||
result.succeeded++;
|
||||
|
|
@ -514,18 +519,23 @@ async function importBackupSchedules(backupSchedules: unknown[]): Promise<Import
|
|||
|
||||
try {
|
||||
const retentionPolicy = isRecord(s.retentionPolicy) ? (s.retentionPolicy as RetentionPolicy) : undefined;
|
||||
const createdSchedule = await backupServiceModule.backupsService.createSchedule({
|
||||
name: scheduleName,
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
enabled: typeof s.enabled === "boolean" ? s.enabled : true,
|
||||
cronExpression: s.cronExpression,
|
||||
retentionPolicy,
|
||||
excludePatterns: asStringArray(s.excludePatterns),
|
||||
excludeIfPresent: asStringArray(s.excludeIfPresent),
|
||||
includePatterns: asStringArray(s.includePatterns),
|
||||
oneFileSystem: typeof s.oneFileSystem === "boolean" ? s.oneFileSystem : undefined,
|
||||
});
|
||||
// Pass shortId from config if provided (for IaC reproducibility)
|
||||
const providedShortId = typeof s.shortId === "string" ? s.shortId : undefined;
|
||||
const createdSchedule = await backupServiceModule.backupsService.createSchedule(
|
||||
{
|
||||
name: scheduleName,
|
||||
volumeId: volume.id,
|
||||
repositoryId: repository.id,
|
||||
enabled: typeof s.enabled === "boolean" ? s.enabled : true,
|
||||
cronExpression: s.cronExpression,
|
||||
retentionPolicy,
|
||||
excludePatterns: asStringArray(s.excludePatterns),
|
||||
excludeIfPresent: asStringArray(s.excludeIfPresent),
|
||||
includePatterns: asStringArray(s.includePatterns),
|
||||
oneFileSystem: typeof s.oneFileSystem === "boolean" ? s.oneFileSystem : undefined,
|
||||
},
|
||||
providedShortId,
|
||||
);
|
||||
logger.info(`Initialized backup schedule from config: ${scheduleName}`);
|
||||
result.succeeded++;
|
||||
scheduleId = createdSchedule.id;
|
||||
|
|
@ -568,7 +578,7 @@ async function attachScheduleMirrors(
|
|||
scheduleId: number,
|
||||
scheduleName: string,
|
||||
mirrors: unknown[],
|
||||
repoByName: Map<string, { id: string; name: string }>,
|
||||
repoByName: Map<string, { id: string; name: string; config: RepositoryConfig }>,
|
||||
backupServiceModule: typeof import("../backups/backups.service"),
|
||||
): Promise<ImportResult> {
|
||||
const result: ImportResult = { succeeded: 0, skipped: 0, warnings: 0, errors: 0 };
|
||||
|
|
@ -576,7 +586,11 @@ async function attachScheduleMirrors(
|
|||
const existingMirrors = await backupServiceModule.backupsService.getMirrors(scheduleId);
|
||||
const existingRepoIds = new Set(existingMirrors.map((m) => m.repositoryId));
|
||||
|
||||
const mirrorConfigs: Array<{ repositoryId: string; repositoryName: string; enabled: boolean }> = [];
|
||||
const mirrorConfigs: Array<{
|
||||
repositoryId: string;
|
||||
repositoryName: string;
|
||||
enabled: boolean;
|
||||
}> = [];
|
||||
|
||||
for (const m of mirrors) {
|
||||
if (!isRecord(m)) continue;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ConflictError, InternalServerError, NotFoundError } from "http-errors-e
|
|||
import { db } from "../../db/db";
|
||||
import { repositoriesTable } from "../../db/schema";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
import { generateShortId } from "../../utils/id";
|
||||
import { generateShortId, isValidShortId } from "../../utils/id";
|
||||
import { restic } from "../../utils/restic";
|
||||
import { cryptoUtils } from "../../utils/crypto";
|
||||
import { repoMutex } from "../../core/repository-mutex";
|
||||
|
|
@ -62,9 +62,32 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
|
|||
return encryptedConfig as RepositoryConfig;
|
||||
};
|
||||
|
||||
const createRepository = async (name: string, config: RepositoryConfig, compressionMode?: CompressionMode) => {
|
||||
const createRepository = async (
|
||||
name: string,
|
||||
config: RepositoryConfig,
|
||||
compressionMode?: CompressionMode,
|
||||
providedShortId?: string,
|
||||
) => {
|
||||
const id = crypto.randomUUID();
|
||||
const shortId = generateShortId();
|
||||
|
||||
// Use provided shortId if valid, otherwise generate a new one
|
||||
let shortId: string;
|
||||
if (providedShortId) {
|
||||
if (!isValidShortId(providedShortId)) {
|
||||
throw new Error(`Invalid shortId format: '${providedShortId}'. Must be 8 base64url characters.`);
|
||||
}
|
||||
const shortIdInUse = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.shortId, providedShortId),
|
||||
});
|
||||
if (shortIdInUse) {
|
||||
throw new ConflictError(
|
||||
`Repository shortId '${providedShortId}' is already in use by repository '${shortIdInUse.name}'`,
|
||||
);
|
||||
}
|
||||
shortId = providedShortId;
|
||||
} else {
|
||||
shortId = generateShortId();
|
||||
}
|
||||
|
||||
let processedConfig = config;
|
||||
if (config.backend === "local" && !config.isExistingRepository) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { db } from "../../db/db";
|
|||
import { volumesTable } from "../../db/schema";
|
||||
import { cryptoUtils } from "../../utils/crypto";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
import { generateShortId } from "../../utils/id";
|
||||
import { generateShortId, isValidShortId } from "../../utils/id";
|
||||
import { getStatFs, type StatFs } from "../../utils/mountinfo";
|
||||
import { withTimeout } from "../../utils/timeout";
|
||||
import { createVolumeBackend } from "../backends/backend";
|
||||
|
|
@ -48,7 +48,7 @@ const listVolumes = async () => {
|
|||
return volumes;
|
||||
};
|
||||
|
||||
const createVolume = async (name: string, backendConfig: BackendConfig) => {
|
||||
const createVolume = async (name: string, backendConfig: BackendConfig, providedShortId?: string) => {
|
||||
const slug = slugify(name, { lower: true, strict: true });
|
||||
|
||||
const existing = await db.query.volumesTable.findFirst({
|
||||
|
|
@ -59,7 +59,22 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
|
|||
throw new ConflictError("Volume already exists");
|
||||
}
|
||||
|
||||
const shortId = generateShortId();
|
||||
// Use provided shortId if valid, otherwise generate a new one
|
||||
let shortId: string;
|
||||
if (providedShortId) {
|
||||
if (!isValidShortId(providedShortId)) {
|
||||
throw new Error(`Invalid shortId format: '${providedShortId}'. Must be 8 base64url characters.`);
|
||||
}
|
||||
const shortIdInUse = await db.query.volumesTable.findFirst({
|
||||
where: eq(volumesTable.shortId, providedShortId),
|
||||
});
|
||||
if (shortIdInUse) {
|
||||
throw new ConflictError(`Volume shortId '${providedShortId}' is already in use by volume '${shortIdInUse.name}'`);
|
||||
}
|
||||
shortId = providedShortId;
|
||||
} else {
|
||||
shortId = generateShortId();
|
||||
}
|
||||
const encryptedConfig = await encryptSensitiveFields(backendConfig);
|
||||
|
||||
const [created] = await db
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import crypto from "node:crypto";
|
||||
|
||||
export const generateShortId = (length = 8): string => {
|
||||
const SHORT_ID_LENGTH = 8;
|
||||
|
||||
export const generateShortId = (length = SHORT_ID_LENGTH): string => {
|
||||
const bytesNeeded = Math.ceil((length * 3) / 4);
|
||||
return crypto.randomBytes(bytesNeeded).toString("base64url").slice(0, length);
|
||||
};
|
||||
|
||||
export const isValidShortId = (value: string, length = SHORT_ID_LENGTH): boolean => {
|
||||
const regex = new RegExp(`^[A-Za-z0-9_-]{${length}}$`);
|
||||
return regex.test(value);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue