diff --git a/.changeset/recovered-rclone-batch-upload.md b/.changeset/recovered-rclone-batch-upload.md
new file mode 100644
index 000000000..a23739d56
--- /dev/null
+++ b/.changeset/recovered-rclone-batch-upload.md
@@ -0,0 +1,39 @@
+---
+"@opennextjs/cloudflare": minor
+---
+
+feature: add opt-in batch upload via `rclone` for fast R2 cache population.
+
+**Key Changes:**
+
+1. **Optional `rclone` Upload**: Install the optional `rclone.js` peer dependency and pass `--rclone` to opt in to `rclone` based batch uploads.
+
+ - `R2_ACCESS_KEY_ID`
+ - `R2_SECRET_ACCESS_KEY`
+ - `CF_ACCOUNT_ID`
+
+2. **Explicit Opt-in**: The existing worker-based population path remains the default. `rclone` is only loaded when `--rclone` is used for a remote cache.
+
+3. **Clear Errors**: The CLI reports missing credentials or a missing `rclone.js` installation when the option is used.
+
+**Usage:**
+
+Install `rclone.js`, then add the secrets in a `.env`/`.dev.vars` file in your project root:
+
+```bash
+pnpm add rclone.js
+pnpm approve-builds # select rclone.js
+pnpm rebuild rclone.js
+R2_ACCESS_KEY_ID=your_key
+R2_SECRET_ACCESS_KEY=your_secret
+CF_ACCOUNT_ID=your_account
+
+opennextjs-cloudflare deploy --rclone
+```
+
+You can also set the environment variables for CI builds.
+
+**Notes:**
+
+- You can follow documentation for creating API tokens with appropriate permissions for R2 access.
+- `rclone` may not be supported on all platforms.
diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json
index 42d9d8357..bf96fb500 100644
--- a/packages/cloudflare/package.json
+++ b/packages/cloudflare/package.json
@@ -70,6 +70,7 @@
"@types/mock-fs": "catalog:",
"@types/node": "catalog:",
"@types/picomatch": "^4.0.0",
+ "@types/rclone.js": "^0.6.1",
"@types/yargs": "catalog:",
"diff": "^8.0.2",
"esbuild": "catalog:",
@@ -81,6 +82,7 @@
"mock-fs": "catalog:",
"next": "catalog:",
"picomatch": "^4.0.2",
+ "rclone.js": "^0.6.6",
"rimraf": "catalog:",
"typescript": "catalog:",
"typescript-eslint": "catalog:",
@@ -88,6 +90,12 @@
},
"peerDependencies": {
"next": ">=15.5.18 <16 || >=16.2.6",
+ "rclone.js": "^0.6.6",
"wrangler": "catalog:"
+ },
+ "peerDependenciesMeta": {
+ "rclone.js": {
+ "optional": true
+ }
}
}
diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts
index cfde3167b..df1694024 100644
--- a/packages/cloudflare/src/api/cloudflare-context.ts
+++ b/packages/cloudflare/src/api/cloudflare-context.ts
@@ -83,8 +83,13 @@ declare global {
CF_PREVIEW_DOMAIN?: string;
// Should have the `Workers Scripts:Read` permission
CF_WORKERS_SCRIPTS_API_TOKEN?: string;
- // Cloudflare account id - needed for skew protection
+
+ // Cloudflare account id - needed for skew protection and R2 batch population
CF_ACCOUNT_ID?: string;
+
+ // R2 S3 API credentials used by `rclone`
+ R2_ACCESS_KEY_ID?: string;
+ R2_SECRET_ACCESS_KEY?: string;
}
}
diff --git a/packages/cloudflare/src/cli/commands/deploy.ts b/packages/cloudflare/src/cli/commands/deploy.ts
index b8a8f6ccc..0362f5e17 100644
--- a/packages/cloudflare/src/cli/commands/deploy.ts
+++ b/packages/cloudflare/src/cli/commands/deploy.ts
@@ -20,7 +20,9 @@ import {
*
* @param args
*/
-export async function deployCommand(args: WithWranglerArgs<{ cacheChunkSize?: number }>): Promise {
+export async function deployCommand(
+ args: WithWranglerArgs<{ cacheChunkSize?: number; rclone: boolean }>
+): Promise {
printHeaders("deploy");
const { config } = await retrieveCompiledConfig();
@@ -45,6 +47,7 @@ export async function deployCommand(args: WithWranglerArgs<{ cacheChunkSize?: nu
environment: args.env,
wranglerConfigPath: args.wranglerConfigPath,
cacheChunkSize: args.cacheChunkSize,
+ useRclone: args.rclone,
shouldUsePreviewId: false,
},
envVars
diff --git a/packages/cloudflare/src/cli/commands/populate-cache.spec.ts b/packages/cloudflare/src/cli/commands/populate-cache.spec.ts
index cf85bbe29..9ecf082a2 100644
--- a/packages/cloudflare/src/cli/commands/populate-cache.spec.ts
+++ b/packages/cloudflare/src/cli/commands/populate-cache.spec.ts
@@ -1,8 +1,10 @@
+import { EventEmitter } from "node:events";
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
import mockFs from "mock-fs";
+import rclone from "rclone.js";
import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest";
import type { Unstable_Config as WranglerConfig } from "wrangler";
import { unstable_startWorker } from "wrangler";
@@ -12,6 +14,7 @@ import r2IncrementalCache from "../../api/overrides/incremental-cache/r2-increme
import { ensureR2Bucket } from "../utils/ensure-r2-bucket.js";
import {
getCacheAssets,
+ loadRclone,
MAX_REQUEST_RETRIES,
MAX_RETRY_DELAY_MS,
populateCache,
@@ -104,6 +107,18 @@ vi.mock("./utils/helpers.js", () => ({
}));
vi.mock("../utils/ensure-r2-bucket.js");
+vi.mock("rclone.js", () => ({
+ default: {
+ copy: vi.fn(() => {
+ const child = new EventEmitter();
+ queueMicrotask(() => child.emit("close", 0, null));
+ return child;
+ }),
+ promises: {
+ version: vi.fn(async () => ""),
+ },
+ },
+}));
vi.mock("wrangler");
describe("populateCache", () => {
@@ -224,6 +239,153 @@ describe("populateCache", () => {
}
);
+ test("remote with rclone - uploads staged cache entries without starting a worker", async () => {
+ setupMockFileSystem();
+
+ await populateCache(
+ buildOptions,
+ config,
+ wranglerConfig,
+ { target: "remote", shouldUsePreviewId: false, useRclone: true, cacheChunkSize: 20 },
+ {
+ R2_ACCESS_KEY_ID: "access-key",
+ R2_SECRET_ACCESS_KEY: "secret-key",
+ CF_ACCOUNT_ID: "account-id",
+ } as WorkerEnvVar
+ );
+
+ expect(rclone.copy).toHaveBeenCalledWith(
+ expect.any(String),
+ "r2:test-bucket",
+ expect.objectContaining({
+ progress: true,
+ transfers: 20,
+ checkers: 10,
+ env: expect.objectContaining({ RCLONE_CONFIG: expect.any(String) }),
+ stdio: "inherit",
+ })
+ );
+ expect(unstable_startWorker).not.toHaveBeenCalled();
+ expect(ensureR2Bucket).not.toHaveBeenCalled();
+ });
+
+ test("remote with rclone - uses the default transfer concurrency", async () => {
+ setupMockFileSystem();
+
+ await populateCache(
+ buildOptions,
+ config,
+ wranglerConfig,
+ { target: "remote", shouldUsePreviewId: false, useRclone: true },
+ {
+ R2_ACCESS_KEY_ID: "access-key",
+ R2_SECRET_ACCESS_KEY: "secret-key",
+ CF_ACCOUNT_ID: "account-id",
+ } as WorkerEnvVar
+ );
+
+ expect(rclone.copy).toHaveBeenCalledWith(
+ expect.any(String),
+ "r2:test-bucket",
+ expect.objectContaining({ transfers: 25, checkers: 12 })
+ );
+ });
+
+ test("remote with rclone - rejects a failed rclone process", async () => {
+ setupMockFileSystem();
+ vi.mocked(rclone.copy).mockImplementationOnce(() => {
+ const child = new EventEmitter();
+ queueMicrotask(() => child.emit("close", 7, null));
+ return child as ReturnType;
+ });
+
+ await expect(
+ populateCache(
+ buildOptions,
+ config,
+ wranglerConfig,
+ { target: "remote", shouldUsePreviewId: false, useRclone: true },
+ {
+ R2_ACCESS_KEY_ID: "access-key",
+ R2_SECRET_ACCESS_KEY: "secret-key",
+ CF_ACCOUNT_ID: "account-id",
+ } as WorkerEnvVar
+ )
+ ).rejects.toThrow("rclone exited with code 7");
+ });
+
+ test("local with rclone - rejects the unsupported option", async () => {
+ setupMockFileSystem();
+
+ await expect(
+ populateCache(
+ buildOptions,
+ config,
+ wranglerConfig,
+ { target: "local", shouldUsePreviewId: false, useRclone: true },
+ envVars
+ )
+ ).rejects.toThrow("The `--rclone` option can only be used when populating a remote R2 cache.");
+ });
+
+ test("remote with rclone - rejects missing R2 credentials", async () => {
+ setupMockFileSystem();
+
+ await expect(
+ populateCache(
+ buildOptions,
+ config,
+ wranglerConfig,
+ { target: "remote", shouldUsePreviewId: false, useRclone: true },
+ envVars
+ )
+ ).rejects.toThrow(
+ "R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and CF_ACCOUNT_ID must be provided to use `rclone`"
+ );
+ });
+
+ test("remote with rclone - rejects cache prefixes that escape the staging directory", async () => {
+ setupMockFileSystem();
+
+ await expect(
+ populateCache(
+ buildOptions,
+ config,
+ wranglerConfig,
+ { target: "remote", shouldUsePreviewId: false, useRclone: true },
+ {
+ R2_ACCESS_KEY_ID: "access-key",
+ R2_SECRET_ACCESS_KEY: "secret-key",
+ CF_ACCOUNT_ID: "account-id",
+ NEXT_INC_CACHE_R2_PREFIX: "../escaped",
+ } as WorkerEnvVar
+ )
+ ).rejects.toThrow("Cannot stage R2 cache key outside the temporary directory");
+ });
+
+ test("remote with rclone - explains how to install the rclone executable with pnpm", async () => {
+ setupMockFileSystem();
+ vi.mocked(rclone.promises.version).mockRejectedValueOnce(
+ Object.assign(new Error("spawn rclone ENOENT"), { code: "ENOENT" })
+ );
+
+ await expect(
+ populateCache(
+ buildOptions,
+ config,
+ wranglerConfig,
+ { target: "remote", shouldUsePreviewId: false, useRclone: true },
+ {
+ R2_ACCESS_KEY_ID: "access-key",
+ R2_SECRET_ACCESS_KEY: "secret-key",
+ CF_ACCOUNT_ID: "account-id",
+ } as WorkerEnvVar
+ )
+ ).rejects.toThrow(
+ "pnpm users must allow its install script with `pnpm approve-builds`, select `rclone.js`, then run `pnpm rebuild rclone.js`"
+ );
+ });
+
test("remote - exits when bucket provisioning fails", async () => {
setupMockFileSystem();
vi.mocked(ensureR2Bucket).mockResolvedValueOnce({
@@ -465,3 +627,19 @@ describe("populateCache", () => {
});
});
});
+
+describe("loadRclone", () => {
+ test("reports how to install the optional peer dependency", async () => {
+ const missingModuleError = Object.assign(new Error("Cannot find package 'rclone.js'"), {
+ code: "ERR_MODULE_NOT_FOUND",
+ });
+
+ await expect(
+ loadRclone(async () => {
+ throw missingModuleError;
+ })
+ ).rejects.toThrow(
+ "The `--rclone` option requires the optional `rclone.js` peer dependency. Install it in your project before using this option."
+ );
+ });
+});
diff --git a/packages/cloudflare/src/cli/commands/populate-cache.ts b/packages/cloudflare/src/cli/commands/populate-cache.ts
index b6291bd16..60c781a12 100644
--- a/packages/cloudflare/src/cli/commands/populate-cache.ts
+++ b/packages/cloudflare/src/cli/commands/populate-cache.ts
@@ -72,7 +72,7 @@ export const BACKOFF_FACTOR = (MAX_RETRY_DELAY_MS / BASE_RETRY_DELAY_MS) ** (1 /
*/
async function populateCacheCommand(
target: "local" | "remote",
- args: WithWranglerArgs<{ cacheChunkSize?: number }>
+ args: WithWranglerArgs<{ cacheChunkSize?: number; rclone: boolean }>
) {
printHeaders(`populate cache - ${target}`);
@@ -97,6 +97,7 @@ async function populateCacheCommand(
environment: args.env,
wranglerConfigPath: args.wranglerConfigPath,
cacheChunkSize: args.cacheChunkSize,
+ useRclone: args.rclone,
shouldUsePreviewId: false,
},
envVars
@@ -218,11 +219,15 @@ export type PopulateCacheOptions = {
/**
* Number of concurrent requests when populating the cache.
* For KV this is the chunk size passed to `wrangler kv bulk put`.
- * For R2 this is the number of concurrent requests to the local worker.
+ * For R2 this is the number of concurrent requests to the local worker or rclone transfers.
*
* @default 25
*/
cacheChunkSize?: number;
+ /**
+ * Whether to use `rclone` instead of the worker-based R2 cache population path.
+ */
+ useRclone?: boolean;
/**
* Instructs Wrangler to use the preview namespace or ID defined in the Wrangler config for the remote target.
*/
@@ -269,6 +274,21 @@ async function populateR2IncrementalCache(
return;
}
+ if (populateCacheOptions.useRclone) {
+ if (populateCacheOptions.target !== "remote") {
+ throw new Error("The `--rclone` option can only be used when populating a remote R2 cache.");
+ }
+
+ await populateR2IncrementalCacheWithRclone(
+ bucketName,
+ prefix,
+ assets,
+ envVars,
+ populateCacheOptions.cacheChunkSize
+ );
+ return;
+ }
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const handlerPath = path.join(currentDir, "../workers/r2-cache.js");
const isRemote = populateCacheOptions.target === "remote";
@@ -330,6 +350,167 @@ async function populateR2IncrementalCache(
logger.info(`Successfully populated cache with ${assets.length} entries`);
}
+async function populateR2IncrementalCacheWithRclone(
+ bucketName: string,
+ prefix: string | undefined,
+ assets: CacheAsset[],
+ envVars: WorkerEnvVar,
+ cacheChunkSize?: number
+) {
+ const accessKey = envVars.R2_ACCESS_KEY_ID;
+ const secretKey = envVars.R2_SECRET_ACCESS_KEY;
+ const accountId = envVars.CF_ACCOUNT_ID;
+
+ if (!accessKey || !secretKey || !accountId) {
+ throw new Error(
+ "R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and CF_ACCOUNT_ID must be provided to use `rclone`"
+ );
+ }
+
+ const rclone = await loadRclone();
+
+ logger.info("\nPopulating remote R2 incremental cache using `rclone`...");
+
+ const configDir = await fsp.mkdtemp(path.join(os.tmpdir(), "rclone-config-"));
+ const configPath = path.join(configDir, "rclone.conf");
+ const stagingDir = await fsp.mkdtemp(path.join(os.tmpdir(), "r2-staging-"));
+ const transfers = Math.max(1, cacheChunkSize ?? 25);
+ const checkers = Math.max(1, Math.floor(transfers / 2));
+
+ try {
+ await fsp.writeFile(
+ configPath,
+ `[r2]\ntype = s3\nprovider = Cloudflare\naccess_key_id = ${accessKey}\nsecret_access_key = ${secretKey}\nendpoint = https://${accountId}.r2.cloudflarestorage.com\nacl = private\n`,
+ { mode: 0o600 }
+ );
+ const rcloneEnv = {
+ ...process.env,
+ RCLONE_CONFIG: configPath,
+ };
+ await ensureRcloneExecutable(rclone, rcloneEnv);
+
+ await stageCacheAssets(assets, stagingDir, prefix, transfers);
+
+ await runRcloneCopy(rclone, stagingDir, `r2:${bucketName}`, {
+ progress: true,
+ transfers,
+ checkers,
+ env: rcloneEnv,
+ stdio: "inherit",
+ });
+ } finally {
+ await Promise.allSettled([
+ fsp.rm(stagingDir, { recursive: true, force: true }),
+ fsp.rm(configDir, { recursive: true, force: true }),
+ ]);
+ }
+
+ logger.info(`Successfully populated cache with ${assets.length} entries`);
+}
+
+async function runRcloneCopy(
+ rclone: typeof import("rclone.js"),
+ source: string,
+ destination: string,
+ options: Record
+) {
+ await new Promise((resolve, reject) => {
+ const child = rclone.copy(source, destination, options);
+
+ child.once("error", reject);
+ child.once("close", (code, signal) => {
+ if (code === 0) {
+ resolve();
+ return;
+ }
+
+ reject(
+ new Error(
+ `rclone exited ${signal ? `after receiving signal ${signal}` : `with code ${code ?? "unknown"}`}`
+ )
+ );
+ });
+ });
+}
+
+async function stageCacheAssets(
+ assets: CacheAsset[],
+ stagingDir: string,
+ prefix: string | undefined,
+ maxConcurrency: number
+) {
+ const pending = new Set>();
+
+ for (const asset of assets) {
+ const task = stageCacheAsset(asset, stagingDir, prefix).finally(() => pending.delete(task));
+ pending.add(task);
+
+ if (pending.size >= maxConcurrency) {
+ await Promise.race(pending);
+ }
+ }
+
+ await Promise.all(pending);
+}
+
+async function stageCacheAsset(asset: CacheAsset, stagingDir: string, prefix: string | undefined) {
+ const cacheKey = computeCacheKey(asset.key, {
+ prefix,
+ buildId: asset.buildId,
+ cacheType: asset.isFetch ? "fetch" : "cache",
+ });
+ const destination = path.resolve(stagingDir, cacheKey);
+ const relativeDestination = path.relative(stagingDir, destination);
+
+ if (relativeDestination.startsWith(`..${path.sep}`) || path.isAbsolute(relativeDestination)) {
+ throw new Error(`Cannot stage R2 cache key outside the temporary directory: ${JSON.stringify(cacheKey)}`);
+ }
+
+ await fsp.mkdir(path.dirname(destination), { recursive: true });
+
+ try {
+ await fsp.link(asset.fullPath, destination);
+ } catch (error) {
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EXDEV") {
+ throw error;
+ }
+
+ await fsp.copyFile(asset.fullPath, destination);
+ }
+}
+
+async function ensureRcloneExecutable(rclone: typeof import("rclone.js"), env: NodeJS.ProcessEnv) {
+ try {
+ await rclone.promises.version({ env });
+ } catch (error) {
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
+ throw new Error(
+ "The `rclone.js` executable is unavailable. pnpm users must allow its install script with `pnpm approve-builds`, select `rclone.js`, then run `pnpm rebuild rclone.js`."
+ );
+ }
+
+ throw error;
+ }
+}
+
+export async function loadRclone(
+ importRclone: () => Promise<{ default: typeof import("rclone.js") }> = () => import("rclone.js")
+) {
+ try {
+ return (await importRclone()).default;
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ ("code" in error ? error.code === "ERR_MODULE_NOT_FOUND" : error.message.includes("rclone.js"))
+ ) {
+ throw new Error(
+ "The `--rclone` option requires the optional `rclone.js` peer dependency. Install it in your project before using this option."
+ );
+ }
+ throw error;
+ }
+}
+
/**
* Sends cache entries to the R2 worker, one entry per request.
*
@@ -655,8 +836,14 @@ export function addPopulateCacheCommand(y: T) {
}
export function withPopulateCacheOptions(args: T) {
- return withWranglerOptions(args).options("cacheChunkSize", {
- type: "number",
- desc: "Number of entries per chunk when populating the cache",
- });
+ return withWranglerOptions(args)
+ .options("cacheChunkSize", {
+ type: "number",
+ desc: "Number of concurrent cache entries to process",
+ })
+ .options("rclone", {
+ type: "boolean",
+ default: false,
+ desc: "Use rclone to populate a remote R2 incremental cache",
+ });
}
diff --git a/packages/cloudflare/src/cli/commands/preview.ts b/packages/cloudflare/src/cli/commands/preview.ts
index a26af25df..83a45f648 100644
--- a/packages/cloudflare/src/cli/commands/preview.ts
+++ b/packages/cloudflare/src/cli/commands/preview.ts
@@ -19,7 +19,7 @@ import {
* @param args
*/
export async function previewCommand(
- args: WithWranglerArgs<{ cacheChunkSize?: number; remote: boolean }>
+ args: WithWranglerArgs<{ cacheChunkSize?: number; remote: boolean; rclone: boolean }>
): Promise {
printHeaders("preview");
@@ -44,6 +44,7 @@ export async function previewCommand(
environment: args.env,
wranglerConfigPath: args.wranglerConfigPath,
cacheChunkSize: args.cacheChunkSize,
+ useRclone: args.rclone,
shouldUsePreviewId: args.remote,
},
envVars
diff --git a/packages/cloudflare/src/cli/commands/upload.ts b/packages/cloudflare/src/cli/commands/upload.ts
index 095d0e76e..b09f066e5 100644
--- a/packages/cloudflare/src/cli/commands/upload.ts
+++ b/packages/cloudflare/src/cli/commands/upload.ts
@@ -20,7 +20,9 @@ import {
*
* @param args
*/
-export async function uploadCommand(args: WithWranglerArgs<{ cacheChunkSize?: number }>): Promise {
+export async function uploadCommand(
+ args: WithWranglerArgs<{ cacheChunkSize?: number; rclone: boolean }>
+): Promise {
printHeaders("upload");
const { config } = await retrieveCompiledConfig();
@@ -47,6 +49,7 @@ export async function uploadCommand(args: WithWranglerArgs<{ cacheChunkSize?: nu
environment: args.env,
wranglerConfigPath: args.wranglerConfigPath,
cacheChunkSize: args.cacheChunkSize,
+ useRclone: args.rclone,
shouldUsePreviewId: false,
},
envVars
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 60cb6ad00..78f29f69d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1126,6 +1126,9 @@ importers:
'@types/picomatch':
specifier: ^4.0.0
version: 4.0.0
+ '@types/rclone.js':
+ specifier: ^0.6.1
+ version: 0.6.3
'@types/yargs':
specifier: 'catalog:'
version: 17.0.33
@@ -1159,6 +1162,9 @@ importers:
picomatch:
specifier: ^4.0.2
version: 4.0.2
+ rclone.js:
+ specifier: ^0.6.6
+ version: 0.6.6
rimraf:
specifier: 'catalog:'
version: 6.0.1
@@ -4557,6 +4563,9 @@ packages:
'@types/range-parser@1.2.7':
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
+ '@types/rclone.js@0.6.3':
+ resolution: {integrity: sha512-BssKAAVRY//fxGKso8SatyOwiD7X0toDofNnVxZlIXmN7UHrn2UBTxldNAjgUvWA91qJyeEPfKmeJpZVhLugXg==}
+
'@types/react-dom@19.0.6':
resolution: {integrity: sha512-lo6MuY+rFr8kIiFnr+7TzO+Av0wUPcEcepiPV4epGP0eTQpkDfp9czudg73isV8UxKauCUNlL1N8fXhcnx4iBw==}
peerDependencies:
@@ -4845,6 +4854,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ adm-zip@0.5.17:
+ resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==}
+ engines: {node: '>=12.0'}
+
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
@@ -8389,6 +8402,13 @@ packages:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
+ rclone.js@0.6.6:
+ resolution: {integrity: sha512-Dxh34cab/fNjFq5SSm0fYLNkGzG2cQSBy782UW9WwxJCEiVO4cGXkvaXcNlgv817dK8K8PuQ+NHUqSAMMhWujQ==}
+ engines: {node: '>=12'}
+ cpu: [arm, arm64, mips, mipsel, x32, x64]
+ os: [darwin, freebsd, linux, openbsd, sunos, win32]
+ hasBin: true
+
react-dom@19.0.6:
resolution: {integrity: sha512-0dbe9G8x3ewsAkbetkL7qEqQ0zQNbQWV7il9kVD0R2haq3DT/3/3ZZqRH8QeMqMejTCFOvl5n8pbwKGiKvZhFQ==}
peerDependencies:
@@ -13792,7 +13812,7 @@ snapshots:
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
- '@types/node': 20.14.10
+ '@types/node': 22.12.0
'@types/caseless@0.12.5':
optional: true
@@ -13804,7 +13824,7 @@ snapshots:
'@types/connect@3.4.38':
dependencies:
- '@types/node': 20.14.10
+ '@types/node': 22.12.0
'@types/debug@4.1.12':
dependencies:
@@ -13818,7 +13838,7 @@ snapshots:
'@types/express-serve-static-core@4.19.6':
dependencies:
- '@types/node': 20.14.10
+ '@types/node': 22.12.0
'@types/qs': 6.9.18
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@@ -13843,7 +13863,7 @@ snapshots:
'@types/jsonwebtoken@9.0.8':
dependencies:
'@types/ms': 0.7.34
- '@types/node': 20.14.10
+ '@types/node': 22.12.0
'@types/long@4.0.2':
optional: true
@@ -13893,6 +13913,10 @@ snapshots:
'@types/range-parser@1.2.7': {}
+ '@types/rclone.js@0.6.3':
+ dependencies:
+ '@types/node': 22.12.0
+
'@types/react-dom@19.0.6(@types/react@19.0.6)':
dependencies:
'@types/react': 19.0.6
@@ -13912,7 +13936,7 @@ snapshots:
'@types/request@2.48.12':
dependencies:
'@types/caseless': 0.12.5
- '@types/node': 20.14.10
+ '@types/node': 22.12.0
'@types/tough-cookie': 4.0.5
form-data: 2.5.2
optional: true
@@ -13920,12 +13944,12 @@ snapshots:
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
- '@types/node': 20.14.10
+ '@types/node': 22.12.0
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
- '@types/node': 20.14.10
+ '@types/node': 22.12.0
'@types/send': 0.17.4
'@types/tough-cookie@4.0.5':
@@ -13935,7 +13959,7 @@ snapshots:
'@types/ws@8.5.14':
dependencies:
- '@types/node': 20.14.10
+ '@types/node': 22.12.0
'@types/yargs-parser@21.0.3': {}
@@ -14559,6 +14583,8 @@ snapshots:
acorn@8.15.0: {}
+ adm-zip@0.5.17: {}
+
agent-base@6.0.2:
dependencies:
debug: 4.4.3
@@ -18986,7 +19012,7 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
- '@types/node': 20.14.10
+ '@types/node': 22.12.0
long: 5.2.4
proxy-addr@2.0.7:
@@ -19060,6 +19086,11 @@ snapshots:
minimist: 1.2.8
strip-json-comments: 2.0.1
+ rclone.js@0.6.6:
+ dependencies:
+ adm-zip: 0.5.17
+ mri: 1.2.0
+
react-dom@19.0.6(react@19.0.6):
dependencies:
react: 19.0.6