mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat(repositories): rclone backends
This commit is contained in:
@@ -21,11 +21,17 @@ RUN apk add --no-cache curl bzip2
|
|||||||
RUN echo "Building for ${TARGETARCH}"
|
RUN echo "Building for ${TARGETARCH}"
|
||||||
RUN if [ "${TARGETARCH}" = "arm64" ]; then \
|
RUN if [ "${TARGETARCH}" = "arm64" ]; then \
|
||||||
curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_arm64.bz2; \
|
curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_arm64.bz2; \
|
||||||
|
curl -O https://downloads.rclone.org/rclone-current-linux-arm64.zip; \
|
||||||
|
unzip rclone-current-linux-arm64.zip; \
|
||||||
elif [ "${TARGETARCH}" = "amd64" ]; then \
|
elif [ "${TARGETARCH}" = "amd64" ]; then \
|
||||||
curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_amd64.bz2; \
|
curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_amd64.bz2; \
|
||||||
|
curl -O https://downloads.rclone.org/rclone-current-linux-amd64.zip; \
|
||||||
|
unzip rclone-current-linux-amd64.zip; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
RUN bzip2 -d restic.bz2 && chmod +x restic
|
RUN bzip2 -d restic.bz2 && chmod +x restic
|
||||||
|
RUN mv rclone-*-linux-*/rclone /deps/rclone && chmod +x /deps/rclone
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------
|
# ------------------------------
|
||||||
# DEVELOPMENT
|
# DEVELOPMENT
|
||||||
@@ -37,6 +43,7 @@ ENV NODE_ENV="development"
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=deps /deps/restic /usr/local/bin/restic
|
COPY --from=deps /deps/restic /usr/local/bin/restic
|
||||||
|
COPY --from=deps /deps/rclone /usr/local/bin/rclone
|
||||||
COPY ./package.json ./bun.lock ./
|
COPY ./package.json ./bun.lock ./
|
||||||
COPY ./packages/schemas/package.json ./packages/schemas/package.json
|
COPY ./packages/schemas/package.json ./packages/schemas/package.json
|
||||||
COPY ./apps/client/package.json ./apps/client/package.json
|
COPY ./apps/client/package.json ./apps/client/package.json
|
||||||
@@ -76,6 +83,7 @@ ENV NODE_ENV="production"
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=deps /deps/restic /usr/local/bin/restic
|
COPY --from=deps /deps/restic /usr/local/bin/restic
|
||||||
|
COPY --from=deps /deps/rclone /usr/local/bin/rclone
|
||||||
COPY --from=builder /app/apps/server/dist ./
|
COPY --from=builder /app/apps/server/dist ./
|
||||||
COPY --from=builder /app/apps/server/drizzle ./assets/migrations
|
COPY --from=builder /app/apps/server/drizzle ./assets/migrations
|
||||||
COPY --from=builder /app/apps/client/dist/client ./assets/frontend
|
COPY --from=builder /app/apps/client/dist/client ./assets/frontend
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
doctorRepositoryDto,
|
doctorRepositoryDto,
|
||||||
getRepositoryDto,
|
getRepositoryDto,
|
||||||
getSnapshotDetailsDto,
|
getSnapshotDetailsDto,
|
||||||
|
listRcloneRemotesDto,
|
||||||
listRepositoriesDto,
|
listRepositoriesDto,
|
||||||
listSnapshotFilesDto,
|
listSnapshotFilesDto,
|
||||||
listSnapshotFilesQuery,
|
listSnapshotFilesQuery,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
type RestoreSnapshotDto,
|
type RestoreSnapshotDto,
|
||||||
} from "./repositories.dto";
|
} from "./repositories.dto";
|
||||||
import { repositoriesService } from "./repositories.service";
|
import { repositoriesService } from "./repositories.service";
|
||||||
|
import { getRcloneRemoteInfo, listRcloneRemotes } from "../../utils/rclone";
|
||||||
|
|
||||||
export const repositoriesController = new Hono()
|
export const repositoriesController = new Hono()
|
||||||
.get("/", listRepositoriesDto, async (c) => {
|
.get("/", listRepositoriesDto, async (c) => {
|
||||||
@@ -125,4 +127,19 @@ export const repositoriesController = new Hono()
|
|||||||
const result = await repositoriesService.doctorRepository(name);
|
const result = await repositoriesService.doctorRepository(name);
|
||||||
|
|
||||||
return c.json<DoctorRepositoryDto>(result, 200);
|
return c.json<DoctorRepositoryDto>(result, 200);
|
||||||
|
})
|
||||||
|
.get("/rclone-remotes", listRcloneRemotesDto, async (c) => {
|
||||||
|
const remoteNames = await listRcloneRemotes();
|
||||||
|
|
||||||
|
const remotes = await Promise.all(
|
||||||
|
remoteNames.map(async (name) => {
|
||||||
|
const info = await getRcloneRemoteInfo(name);
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
type: info?.type ?? "unknown",
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(remotes);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -305,3 +305,29 @@ export const doctorRepositoryDto = describeRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List rclone available remotes
|
||||||
|
*/
|
||||||
|
const rcloneRemoteSchema = type({
|
||||||
|
name: "string",
|
||||||
|
type: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
const listRcloneRemotesResponse = rcloneRemoteSchema.array();
|
||||||
|
|
||||||
|
export const listRcloneRemotesDto = describeRoute({
|
||||||
|
description: "List all configured rclone remotes on the host system",
|
||||||
|
tags: ["Rclone"],
|
||||||
|
operationId: "listRcloneRemotes",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "List of rclone remotes",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(listRcloneRemotesResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
81
apps/server/src/utils/rclone.ts
Normal file
81
apps/server/src/utils/rclone.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { $ } from "bun";
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all configured rclone remotes
|
||||||
|
* @returns Array of remote names
|
||||||
|
*/
|
||||||
|
export async function listRcloneRemotes(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const result = await $`rclone listremotes`.quiet();
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.error(`Failed to list rclone remotes: ${result.stderr}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse output - each line is a remote name ending with ":"
|
||||||
|
const remotes = result.stdout
|
||||||
|
.toString()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.endsWith(":"))
|
||||||
|
.map((line) => line.slice(0, -1)); // Remove trailing ":"
|
||||||
|
|
||||||
|
return remotes;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error listing rclone remotes: ${error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information about a specific rclone remote
|
||||||
|
* @param remote Remote name
|
||||||
|
* @returns Remote type and configuration info
|
||||||
|
*/
|
||||||
|
export async function getRcloneRemoteInfo(
|
||||||
|
remote: string,
|
||||||
|
): Promise<{ type: string; config: Record<string, string> } | null> {
|
||||||
|
try {
|
||||||
|
const result = await $`rclone config show ${remote}`.quiet();
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
logger.error(`Failed to get info for remote ${remote}: ${result.stderr}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the output to extract type and config
|
||||||
|
const output = result.stdout.toString();
|
||||||
|
const lines = output
|
||||||
|
.split("\n")
|
||||||
|
.map((l) => l.trim())
|
||||||
|
.filter((l) => l);
|
||||||
|
|
||||||
|
const config: Record<string, string> = {};
|
||||||
|
let type = "unknown";
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes("=")) {
|
||||||
|
const parts = line.split("=");
|
||||||
|
const key = parts[0];
|
||||||
|
if (!key) continue;
|
||||||
|
|
||||||
|
const valueParts = parts.slice(1);
|
||||||
|
const value = valueParts.join("=").trim();
|
||||||
|
const cleanKey = key.trim();
|
||||||
|
|
||||||
|
if (cleanKey === "type") {
|
||||||
|
type = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
config[cleanKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type, config };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error getting remote info for ${remote}: ${error}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,8 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
|
|||||||
return `gs:${config.bucket}:/`;
|
return `gs:${config.bucket}:/`;
|
||||||
case "azure":
|
case "azure":
|
||||||
return `azure:${config.container}:/`;
|
return `azure:${config.container}:/`;
|
||||||
|
case "rclone":
|
||||||
|
return `rclone:${config.remote}:${config.path}`;
|
||||||
default: {
|
default: {
|
||||||
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
|
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const REPOSITORY_BACKENDS = {
|
|||||||
s3: "s3",
|
s3: "s3",
|
||||||
gcs: "gcs",
|
gcs: "gcs",
|
||||||
azure: "azure",
|
azure: "azure",
|
||||||
|
rclone: "rclone",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
|
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
|
||||||
@@ -37,10 +38,17 @@ export const azureRepositoryConfigSchema = type({
|
|||||||
endpointSuffix: "string?",
|
endpointSuffix: "string?",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const rcloneRepositoryConfigSchema = type({
|
||||||
|
backend: "'rclone'",
|
||||||
|
remote: "string",
|
||||||
|
path: "string",
|
||||||
|
});
|
||||||
|
|
||||||
export const repositoryConfigSchema = s3RepositoryConfigSchema
|
export const repositoryConfigSchema = s3RepositoryConfigSchema
|
||||||
.or(localRepositoryConfigSchema)
|
.or(localRepositoryConfigSchema)
|
||||||
.or(gcsRepositoryConfigSchema)
|
.or(gcsRepositoryConfigSchema)
|
||||||
.or(azureRepositoryConfigSchema);
|
.or(azureRepositoryConfigSchema)
|
||||||
|
.or(rcloneRepositoryConfigSchema);
|
||||||
|
|
||||||
export type RepositoryConfig = typeof repositoryConfigSchema.infer;
|
export type RepositoryConfig = typeof repositoryConfigSchema.infer;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user