Skip to content

Commit 15a874f

Browse files
refactor: not expose rclone to user and use batch upload based on env variables
1 parent d0c5606 commit 15a874f

File tree

6 files changed

+155
-115
lines changed

6 files changed

+155
-115
lines changed

.changeset/rclone-batch-upload.md

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,41 @@
22
"@opennextjs/cloudflare": minor
33
---
44

5-
feature: add --rcloneBatch flag for faster R2 cache uploads
5+
feature: optional batch upload for faster R2 cache population
66

7-
This adds a new optional `--rcloneBatch` flag that enables batch uploading to R2 using rclone instead of individual wrangler uploads. This significantly improves upload performance for large caches.
7+
This update adds optional batch upload support for R2 cache population, significantly improving upload performance for large caches when enabled via environment variables.
88

9-
**Supported commands:**
9+
**Key Changes:**
10+
11+
1. **Optional Batch Upload**: Configure R2 credentials via environment variables to enable faster batch uploads:
12+
13+
- `R2_ACCESS_KEY_ID`
14+
- `R2_SECRET_ACCESS_KEY`
15+
- `R2_ACCOUNT_ID`
16+
17+
2. **Automatic Detection**: When credentials are detected, batch upload is automatically used for better performance
18+
19+
3. **Smart Fallback**: If credentials are not configured, the CLI falls back to standard Wrangler uploads with a helpful message about enabling batch upload for better performance
20+
21+
**All deployment commands support batch upload:**
1022

1123
- `populateCache` - Explicit cache population
1224
- `deploy` - Deploy with cache population
1325
- `upload` - Upload version with cache population
1426
- `preview` - Preview with cache population
1527

16-
**Performance improvements:**
28+
**Performance Benefits (when batch upload is enabled):**
1729

18-
- Creates staging directory with all cache files organized by R2 keys
19-
- Uses rclone's parallel transfer capabilities (32 transfers, 16 checkers)
20-
- Reduces API calls to Cloudflare
30+
- Parallel transfer capabilities (32 concurrent transfers)
31+
- Significantly faster for large caches
32+
- Reduced API calls to Cloudflare
2133

2234
**Usage:**
2335

2436
```bash
25-
opennextjs-cloudflare deploy --rcloneBatch
26-
opennextjs-cloudflare populateCache remote --rcloneBatch
37+
# Enable batch upload by setting environment variables (recommended for large caches)
38+
export R2_ACCESS_KEY_ID=your_key
39+
export R2_SECRET_ACCESS_KEY=your_secret
40+
export R2_ACCOUNT_ID=your_account
41+
opennextjs-cloudflare deploy # batch upload automatically used
2742
```
28-
29-
**Requirements:**
30-
31-
- The `rclone.js` package (included as dependency) provides the binary
32-
- An rclone config file at `~/.config/rclone/rclone.conf` with R2 credentials (see README for setup instructions)
33-
34-
The original wrangler-based upload remains the default behavior for backward compatibility.

packages/cloudflare/README.md

Lines changed: 11 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -56,54 +56,21 @@ Deploy your application to production with the following:
5656
bun opennextjs-cloudflare build && bun opennextjs-cloudflare deploy
5757
```
5858

59-
## CLI Options
59+
### Batch Cache Population (Optional, Recommended)
6060

61-
### Batch Cache Population (rclone)
62-
63-
The `--rcloneBatch` flag enables faster R2 cache uploads using rclone batch mode.
64-
This flag is supported by the following commands:
65-
66-
- `populateCache` - Explicitly populate cache
67-
- `deploy` - Deploy and populate cache
68-
- `upload` - Upload version and populate cache
69-
- `preview` - Preview and populate cache
70-
71-
**Usage:**
61+
For improved performance with large caches, you can enable batch upload by providing R2 credentials via environment variables:
7262

7363
```bash
74-
# Standalone cache population
75-
npx opennextjs-cloudflare populateCache local --rcloneBatch
76-
npx opennextjs-cloudflare populateCache remote --rcloneBatch
77-
78-
# During deployment
79-
npx opennextjs-cloudflare deploy --rcloneBatch
80-
81-
# During upload
82-
npx opennextjs-cloudflare upload --rcloneBatch
83-
84-
# During preview
85-
npx opennextjs-cloudflare preview --rcloneBatch
64+
export R2_ACCESS_KEY_ID=your_access_key_id
65+
export R2_SECRET_ACCESS_KEY=your_secret_access_key
66+
export R2_ACCOUNT_ID=your_account_id
8667
```
8768

88-
**Requirements:**
89-
90-
1. The `rclone.js` package (included as a dependency) provides the rclone binary automatically
91-
2. An rclone configuration file is required at `~/.config/rclone/rclone.conf` with your R2 credentials
92-
93-
**rclone Configuration:**
94-
95-
Create or update `~/.config/rclone/rclone.conf` with your R2 bucket configuration:
96-
97-
```ini
98-
[r2]
99-
type = s3
100-
provider = Cloudflare
101-
access_key_id = YOUR_ACCESS_KEY_ID
102-
secret_access_key = YOUR_SECRET_ACCESS_KEY
103-
endpoint = https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com
104-
acl = private
105-
```
69+
**Benefits:**
10670

107-
See [Cloudflare's rclone documentation](https://developers.cloudflare.com/r2/examples/rclone/) for more details.
71+
- Significantly faster uploads for large caches using parallel transfers
72+
- Reduced API calls to Cloudflare
73+
- Automatically enabled when credentials are provided
10874

109-
**Default:** `false` (uses standard wrangler-based uploads)
75+
**Fallback:**
76+
If these environment variables are not set, the CLI will use standard Wrangler uploads. Both methods work correctly - batch upload is simply faster for large caches.

packages/cloudflare/src/cli/commands/deploy.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ import {
1919
*
2020
* @param args
2121
*/
22-
export async function deployCommand(
23-
args: WithWranglerArgs<{ cacheChunkSize?: number; rcloneBatch?: boolean }>
24-
): Promise<void> {
22+
export async function deployCommand(args: WithWranglerArgs<{ cacheChunkSize?: number }>): Promise<void> {
2523
printHeaders("deploy");
2624

2725
const { config } = await retrieveCompiledConfig();
@@ -42,7 +40,6 @@ export async function deployCommand(
4240
wranglerConfigPath: args.wranglerConfigPath,
4341
cacheChunkSize: args.cacheChunkSize,
4442
shouldUsePreviewId: false,
45-
rcloneBatch: args.rcloneBatch,
4643
});
4744

4845
runWrangler(

packages/cloudflare/src/cli/commands/populate-cache.ts

Lines changed: 117 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { spawnSync } from "node:child_process";
22
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
34
import path from "node:path";
45

56
import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
@@ -128,52 +129,117 @@ type PopulateCacheOptions = {
128129
* Instructs Wrangler to use the preview namespace or ID defined in the Wrangler config for the remote target.
129130
*/
130131
shouldUsePreviewId: boolean;
132+
};
133+
134+
/**
135+
* Check if R2 credentials are available via environment variables for batch upload
136+
*/
137+
function hasR2EnvCredentials(): boolean {
138+
return !!(process.env.R2_ACCESS_KEY_ID && process.env.R2_SECRET_ACCESS_KEY && process.env.R2_ACCOUNT_ID);
139+
}
140+
141+
/**
142+
* Create a temporary configuration file for batch upload from environment variables
143+
* @returns Path to the temporary config file or null if env vars not available
144+
*/
145+
function createTempRcloneConfig(): string | null {
146+
const accessKey = process.env.R2_ACCESS_KEY_ID;
147+
const secretKey = process.env.R2_SECRET_ACCESS_KEY;
148+
const accountId = process.env.R2_ACCOUNT_ID;
149+
150+
if (!accessKey || !secretKey || !accountId) {
151+
return null;
152+
}
153+
154+
const tempDir = tmpdir();
155+
const tempConfigPath = path.join(tempDir, `rclone-config-${Date.now()}.conf`);
156+
157+
const configContent = `[r2]
158+
type = s3
159+
provider = Cloudflare
160+
access_key_id = ${accessKey}
161+
secret_access_key = ${secretKey}
162+
endpoint = https://${accountId}.r2.cloudflarestorage.com
163+
acl = private
164+
`;
165+
131166
/**
132-
* Use rclone for batch uploading to R2 instead of individual wrangler uploads.
167+
* 0o600 is an octal number (the 0o prefix indicates octal in JavaScript)
168+
* that represents Unix file permissions:
169+
*
170+
* - 6 (owner): read (4) + write (2) = readable and writable by the file owner
171+
* - 0 (group): no permissions for the group
172+
* - 0 (others): no permissions for anyone else
133173
*
134-
* @default false
174+
* In symbolic notation, this is: rw-------
135175
*/
136-
rcloneBatch?: boolean;
137-
};
176+
writeFileSync(tempConfigPath, configContent, { mode: 0o600 });
177+
return tempConfigPath;
178+
}
138179

139-
async function populateR2IncrementalCacheWithRclone(
180+
/**
181+
* Populate R2 incremental cache using batch upload for better performance
182+
* Uses parallel transfers to significantly speed up cache population
183+
*/
184+
async function populateR2IncrementalCacheWithBatchUpload(
140185
options: BuildOptions,
141186
bucket: string,
142187
prefix: string | undefined,
143188
assets: CacheAsset[]
144189
) {
145-
logger.info("\nPopulating R2 incremental cache using rclone batch upload...");
146-
147-
// Create a staging dir with proper key paths
148-
const stagingDir = path.join(options.outputDir, ".r2-staging");
149-
rmSync(stagingDir, { recursive: true, force: true });
150-
mkdirSync(stagingDir, { recursive: true });
190+
logger.info("\nPopulating R2 incremental cache using batch upload...");
151191

152-
for (const { fullPath, key, buildId, isFetch } of assets) {
153-
const cacheKey = computeCacheKey(key, {
154-
prefix,
155-
buildId,
156-
cacheType: isFetch ? "fetch" : "cache",
157-
});
158-
const destPath = path.join(stagingDir, cacheKey);
159-
mkdirSync(path.dirname(destPath), { recursive: true });
160-
copyFileSync(fullPath, destPath);
192+
// Create temporary config from env vars - required for batch upload
193+
const tempConfigPath = createTempRcloneConfig();
194+
if (!tempConfigPath) {
195+
throw new Error(
196+
"R2 credentials not found. Please set R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and R2_ACCOUNT_ID environment variables to enable batch upload."
197+
);
161198
}
162199

163-
// Use rclone to sync the R2
164-
const remote = `r2:${bucket}`;
165-
const args = ["copy", stagingDir, remote, "--progress", "--transfers=32", "--checkers=16"];
200+
const env = { ...process.env };
201+
env.RCLONE_CONFIG = tempConfigPath;
202+
logger.info("Using batch upload with R2 credentials from environment variables");
166203

167-
const result = spawnSync("rclone", args, {
168-
stdio: "inherit",
169-
env: process.env,
170-
});
204+
try {
205+
// Create a staging dir with proper key paths
206+
const stagingDir = path.join(options.outputDir, ".r2-staging");
207+
rmSync(stagingDir, { recursive: true, force: true });
208+
mkdirSync(stagingDir, { recursive: true });
171209

172-
if (result.status !== 0) {
173-
throw new Error("rclone copy failed");
174-
}
210+
for (const { fullPath, key, buildId, isFetch } of assets) {
211+
const cacheKey = computeCacheKey(key, {
212+
prefix,
213+
buildId,
214+
cacheType: isFetch ? "fetch" : "cache",
215+
});
216+
const destPath = path.join(stagingDir, cacheKey);
217+
mkdirSync(path.dirname(destPath), { recursive: true });
218+
copyFileSync(fullPath, destPath);
219+
}
175220

176-
logger.info(`Successfully uploaded ${assets.length} assets to R2`);
221+
// Use rclone to sync the R2
222+
const remote = `r2:${bucket}`;
223+
const args = ["copy", stagingDir, remote, "--progress", "--transfers=32", "--checkers=16"];
224+
225+
const result = spawnSync("rclone", args, {
226+
stdio: "inherit",
227+
env,
228+
});
229+
230+
if (result.status !== 0) {
231+
throw new Error("Batch upload failed");
232+
}
233+
234+
logger.info(`Successfully uploaded ${assets.length} assets to R2 using batch upload`);
235+
} finally {
236+
try {
237+
// Cleanup temporary config file
238+
rmSync(tempConfigPath);
239+
} catch {
240+
console.warn(`Failed to remove temporary config at ${tempConfigPath}`);
241+
}
242+
}
177243
}
178244

179245
async function populateR2IncrementalCache(
@@ -198,10 +264,23 @@ async function populateR2IncrementalCache(
198264

199265
const assets = getCacheAssets(options);
200266

201-
if (populateCacheOptions.rcloneBatch) {
202-
return populateR2IncrementalCacheWithRclone(options, bucket, prefix, assets);
267+
// Use batch upload if R2 credentials are available (optional but recommended)
268+
const canUseBatchUpload = hasR2EnvCredentials();
269+
270+
if (canUseBatchUpload) {
271+
try {
272+
return await populateR2IncrementalCacheWithBatchUpload(options, bucket, prefix, assets);
273+
} catch (error) {
274+
logger.warn(`Batch upload failed: ${error instanceof Error ? error.message : error}`);
275+
logger.info("Falling back to standard uploads...");
276+
}
277+
} else {
278+
logger.info(
279+
"Using standard cache uploads. For faster performance with large caches, set R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, and R2_ACCOUNT_ID environment variables to enable batch upload."
280+
);
203281
}
204282

283+
// Standard upload fallback (using Wrangler)
205284
for (const { fullPath, key, buildId, isFetch } of tqdm(assets)) {
206285
const cacheKey = computeCacheKey(key, {
207286
prefix,
@@ -380,7 +459,7 @@ export async function populateCache(
380459
*/
381460
async function populateCacheCommand(
382461
target: "local" | "remote",
383-
args: WithWranglerArgs<{ cacheChunkSize?: number; rcloneBatch?: boolean }>
462+
args: WithWranglerArgs<{ cacheChunkSize?: number }>
384463
) {
385464
printHeaders(`populate cache - ${target}`);
386465

@@ -395,7 +474,6 @@ async function populateCacheCommand(
395474
wranglerConfigPath: args.wranglerConfigPath,
396475
cacheChunkSize: args.cacheChunkSize,
397476
shouldUsePreviewId: false,
398-
rcloneBatch: args.rcloneBatch,
399477
});
400478
}
401479

@@ -424,14 +502,8 @@ export function addPopulateCacheCommand<T extends yargs.Argv>(y: T) {
424502
}
425503

426504
export function withPopulateCacheOptions<T extends yargs.Argv>(args: T) {
427-
return withWranglerOptions(args)
428-
.options("cacheChunkSize", {
429-
type: "number",
430-
desc: "Number of entries per chunk when populating the cache",
431-
})
432-
.options("rcloneBatch", {
433-
type: "boolean",
434-
desc: "Use rclone for batch uploading to R2 (faster for large caches)",
435-
default: false,
436-
});
505+
return withWranglerOptions(args).options("cacheChunkSize", {
506+
type: "number",
507+
desc: "Number of entries per chunk when populating the cache",
508+
});
437509
}

packages/cloudflare/src/cli/commands/preview.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
* @param args
1818
*/
1919
export async function previewCommand(
20-
args: WithWranglerArgs<{ cacheChunkSize?: number; rcloneBatch?: boolean; remote: boolean }>
20+
args: WithWranglerArgs<{ cacheChunkSize?: number; remote: boolean }>
2121
): Promise<void> {
2222
printHeaders("preview");
2323

@@ -32,7 +32,6 @@ export async function previewCommand(
3232
wranglerConfigPath: args.wranglerConfigPath,
3333
cacheChunkSize: args.cacheChunkSize,
3434
shouldUsePreviewId: args.remote,
35-
rcloneBatch: args.rcloneBatch,
3635
});
3736

3837
runWrangler(options, ["dev", ...args.wranglerArgs], { logging: "all" });

packages/cloudflare/src/cli/commands/upload.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ import {
1919
*
2020
* @param args
2121
*/
22-
export async function uploadCommand(
23-
args: WithWranglerArgs<{ cacheChunkSize?: number; rcloneBatch?: boolean }>
24-
): Promise<void> {
22+
export async function uploadCommand(args: WithWranglerArgs<{ cacheChunkSize?: number }>): Promise<void> {
2523
printHeaders("upload");
2624

2725
const { config } = await retrieveCompiledConfig();
@@ -42,7 +40,6 @@ export async function uploadCommand(
4240
wranglerConfigPath: args.wranglerConfigPath,
4341
cacheChunkSize: args.cacheChunkSize,
4442
shouldUsePreviewId: false,
45-
rcloneBatch: args.rcloneBatch,
4643
});
4744

4845
runWrangler(

0 commit comments

Comments
 (0)