feat: add support for Cloudflare R2 storage provider#10
Conversation
WalkthroughIntroduces Cloudflare R2 storage support by adding new configuration fields ( Changes
Sequence DiagramsequenceDiagram
actor User
participant UI as Settings UI
participant Plugin as Pasterly Plugin
participant Factory as StorageProvider Factory
participant Provider as S3CompatibleStorageProvider
User->>UI: Input R2 credentials<br/>(account, bucket, keys, URL)
UI->>Plugin: onChange event with R2 settings
Plugin->>Plugin: Validate R2 credentials<br/>(required fields check)
alt Valid Credentials
Plugin->>Factory: createStorageProvider('r2', config)
Factory->>Factory: Compute R2 endpoint:<br/>https://{accountId}.r2.cloudflarestorage.com
Factory->>Provider: Initialize with R2 credentials<br/>& region: 'auto'
Provider-->>Plugin: Provider instance
Plugin-->>UI: Storage initialized
else Missing Fields
Plugin-->>UI: Show Notice & abort
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main.ts`:
- Around line 104-117: The R2 branch in the storage initialization (the else if
checking this.settings.storageType === 'r2') currently only validates account,
bucket and keys; add a required validation for the R2 public base URL (e.g.
this.settings.r2PublicBaseUrl) and ensure it is a publicly accessible domain
(custom domain or r2.dev pattern) before proceeding, returning a Notice if
missing/invalid to prevent fallback to the private *.r2.cloudflarestorage.com
endpoint; also update the UI/settings label/description for r2PublicBaseUrl to
mark it required and explain it must be a public custom domain or r2.dev URL so
embedded images are accessible.
In `@src/storageProviders.ts`:
- Around line 370-387: The R2 case should require and validate r2PublicBaseUrl
instead of falling back to the S3 API endpoint: in the 'r2' branch verify
config.r2PublicBaseUrl is present and non-empty and throw a clear Error if
missing, then construct the S3CompatibleStorageProvider using
config.r2PublicBaseUrl as publicBaseUrl (do not use
`https://${config.r2AccountId}.r2.cloudflarestorage.com` fallback); update any
validation messages to reference r2PublicBaseUrl and keep existing checks for
r2AccountId, r2BucketName, r2AccessKeyId and r2SecretAccessKey (refer to the
'r2' case and S3CompatibleStorageProvider / buildPublicUrl usage).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: dcc0e699-836d-4d81-a16c-a26b8802c89c
📒 Files selected for processing (3)
src/main.tssrc/storageProviders.tssrc/types.ts
| } else if (this.settings.storageType === 'r2') { | ||
| if (!this.settings.r2AccountId) { | ||
| new Notice('Please set your R2 Account ID in settings first.'); | ||
| return; | ||
| } | ||
| if (!this.settings.r2BucketName) { | ||
| new Notice('Please set your R2 Bucket Name in settings first.'); | ||
| return; | ||
| } | ||
| if (!this.settings.r2AccessKeyId || !this.settings.r2SecretAccessKey) { | ||
| new Notice('Please set your R2 credentials in settings first.'); | ||
| return; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Cloudflare R2 public buckets custom domains r2.dev public URL required for object access
💡 Result:
No, enabling a Cloudflare R2 public bucket with a custom domain does not require enabling or using the r2.dev public URL for object access. The two options—custom domains and r2.dev subdomains—are independent. You can expose your bucket solely via a custom domain you control without enabling r2.dev access. Key points from official Cloudflare R2 documentation: - Public buckets can be set up in two independent ways: custom domain or r2.dev subdomain (for non-production, rate-limited use). - "Enabling custom domains does not require enabling r2.dev access." - Custom domains enable advanced features like WAF, caching, access controls, and bot management, which are unavailable with r2.dev. - Disabling r2.dev public access is recommended when using custom domains with security products, but it's optional—r2.dev can remain disabled entirely. - Objects are accessible directly via the custom domain (e.g., https://yourdomain.com/object.txt) after connecting the domain in the R2 dashboard and adding the required CNAME record. To set up: 1. Add your domain as a zone in the same Cloudflare account. 2. In R2 bucket Settings > Custom Domains, add and connect the domain. 3. Public access via that domain is enabled independently of r2.dev. r2.dev is optional, development-only, and should be avoided for production or CNAME pointing.
Citations:
- 1: https://developers.cloudflare.com/r2/buckets/public-buckets/
- 2: https://developers.cloudflare.com/r2/buckets/public-buckets/index.md
- 3: https://developers.cloudflare.com/r2/data-access/public-buckets/
🏁 Script executed:
# First, find the storageProviders.ts file and check how r2PublicBaseUrl is used
fd -t f "storageProviders" -o -name "*storage*"Repository: easternkite/Pasterly
Length of output: 294
🏁 Script executed:
# Search for r2PublicBaseUrl usage in the codebase
rg "r2PublicBaseUrl" -A 3 -B 3Repository: easternkite/Pasterly
Length of output: 3326
🏁 Script executed:
# Also check for R2 provider implementation to see how it handles the public URL
rg "r2|R2" src/storageProviders.ts | head -50Repository: easternkite/Pasterly
Length of output: 1043
🏁 Script executed:
# Find the S3 provider implementation
fd -t f "\.ts$" | xargs rg "class.*S3.*Provider|publicBaseUrl" | head -40Repository: easternkite/Pasterly
Length of output: 718
🏁 Script executed:
# Look for URL generation logic in storage providers
rg "publicBaseUrl|getUrl|return.*http" src/storageProviders.ts -A 5 -B 2Repository: easternkite/Pasterly
Length of output: 2588
🏁 Script executed:
# Check if there's an S3 provider implementation file
fd -t f "\.(ts|js)$" src/ | xargs rg "publicBaseUrl.*null|null.*publicBaseUrl"Repository: easternkite/Pasterly
Length of output: 484
🏁 Script executed:
# Get the complete buildPublicUrl method
rg -A 20 "private buildPublicUrl" src/storageProviders.tsRepository: easternkite/Pasterly
Length of output: 911
🏁 Script executed:
# Get the complete buildPublicUrl method including the default case
rg -A 30 "private buildPublicUrl" src/storageProviders.tsRepository: easternkite/Pasterly
Length of output: 1269
🏁 Script executed:
# Let's check what the R2 endpoint looks like and how it's constructed
rg -B 5 -A 5 "r2.cloudflarestorage.com" src/storageProviders.tsRepository: easternkite/Pasterly
Length of output: 573
🏁 Script executed:
# Check if there's any documentation or comments about R2 public access
rg -i "r2|cloudflare" src/ -A 2 -B 2 | head -80Repository: easternkite/Pasterly
Length of output: 3700
🏁 Script executed:
# Let's verify the complete flow: check if there's any bucket policy or public access handling
rg "bucket|public|acl|policy" src/storageProviders.ts -i | head -20Repository: easternkite/Pasterly
Length of output: 1466
🏁 Script executed:
# Double-check: when endpoint is used in buildPublicUrl, what URL is constructed for R2?
# Let's trace through the logic for R2 with null publicBaseUrl
cat > /tmp/check_r2_url.js << 'EOF'
// R2 endpoint: https://ACCOUNT_ID.r2.cloudflarestorage.com
const endpoint = 'https://example123.r2.cloudflarestorage.com';
const bucketName = 'my-bucket';
const objectPath = 'pasterly/image.png';
// Simulate buildPublicUrl logic when publicBaseUrl is null
const endpointUrl = new URL(endpoint);
const cleanPathname = endpointUrl.pathname.replace(/\/+$/, '');
// R2 doesn't set forcePathStyle, so this branch:
endpointUrl.hostname = `${bucketName}.${endpointUrl.hostname}`;
const resultUrl = endpointUrl.toString() + '/' + objectPath;
console.log('Generated URL:', resultUrl);
console.log('Note: This uses the PRIVATE R2 API endpoint, not public access');
EOF
node /tmp/check_r2_url.jsRepository: easternkite/Pasterly
Length of output: 215
Require R2 Public Base URL validation before initializing.
R2 uploads require a public custom domain or R2.dev URL to generate accessible image links. Without one, the code falls back to the private R2 API endpoint (*.r2.cloudflarestorage.com), which cannot be accessed publicly and produces broken embedded images. The validation must be added, and the UI description should reflect that this field is required, not optional.
Proposed validation and UI copy update
} else if (this.settings.storageType === 'r2') {
if (!this.settings.r2AccountId) {
new Notice('Please set your R2 Account ID in settings first.');
return;
}
if (!this.settings.r2BucketName) {
new Notice('Please set your R2 Bucket Name in settings first.');
return;
}
if (!this.settings.r2AccessKeyId || !this.settings.r2SecretAccessKey) {
new Notice('Please set your R2 credentials in settings first.');
return;
}
+ if (!this.settings.r2PublicBaseUrl) {
+ new Notice('Please set your R2 Public Base URL in settings first.');
+ return;
+ }
} new Setting(containerEl)
.setName('R2 Public Base URL')
- .setDesc('Optional: Custom Domain or R2.dev URL (e.g., https://images.example.com)')
+ .setDesc('Required: Custom Domain or R2.dev URL used for returned image links (e.g., https://images.example.com)')
.addText(text => textAlso applies to: 514-524
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main.ts` around lines 104 - 117, The R2 branch in the storage
initialization (the else if checking this.settings.storageType === 'r2')
currently only validates account, bucket and keys; add a required validation for
the R2 public base URL (e.g. this.settings.r2PublicBaseUrl) and ensure it is a
publicly accessible domain (custom domain or r2.dev pattern) before proceeding,
returning a Notice if missing/invalid to prevent fallback to the private
*.r2.cloudflarestorage.com endpoint; also update the UI/settings
label/description for r2PublicBaseUrl to mark it required and explain it must be
a public custom domain or r2.dev URL so embedded images are accessible.
| case 'r2': | ||
| if (!config.r2AccountId) { | ||
| throw new Error('R2 account ID is required'); | ||
| } | ||
| if (!config.r2BucketName) { | ||
| throw new Error('R2 bucket name is required'); | ||
| } | ||
| if (!config.r2AccessKeyId || !config.r2SecretAccessKey) { | ||
| throw new Error('R2 access key ID and secret access key are required'); | ||
| } | ||
| return new S3CompatibleStorageProvider({ | ||
| bucketName: config.r2BucketName, | ||
| region: 'auto', | ||
| endpoint: `https://${config.r2AccountId}.r2.cloudflarestorage.com`, | ||
| accessKeyId: config.r2AccessKeyId, | ||
| secretAccessKey: config.r2SecretAccessKey, | ||
| publicBaseUrl: config.r2PublicBaseUrl || null, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Cloudflare R2 public access custom domain r2.dev S3 API endpoint authenticated object URL documentation
💡 Result:
Cloudflare R2 supports public access via custom domains or r2.dev subdomains for unauthenticated GET requests on public buckets. The S3 API, including authenticated object access and presigned URLs, uses a separate endpoint: https://<ACCOUNT_ID>.r2.cloudflarestorage.com/<BUCKET_NAME>/<OBJECT_KEY>. Custom domains are for public HTTP access only (read-only, no S3 compatibility or authentication via S3 API). Key documentation points: - Enable public buckets in R2 dashboard Settings > Public access: Add custom domain (must be in same account/zone) or enable r2.dev (dev-only, rate-limited, no advanced features like WAF/caching). - Custom domain setup: R2 auto-adds CNAME; use Origin Rules to route/rewrite from main site (e.g., example.com/images/* -> imagesbucket.example.com). - r2.dev: https://.r2.dev/<OBJECT_KEY> for direct public access. - S3 API endpoint is always *.r2.cloudflarestorage.com; presigned URLs generated for it (e.g., using AWS SDK with endpoint configured), explicitly not compatible with custom domains—for auth on custom domains, use WAF HMAC or Zero Trust. - No direct authenticated S3 access via custom domain or r2.dev; those are public HTTP endpoints. Examples: - Public custom domain object URL: https://imagesbucket.example.com/image.png - r2.dev public URL: https://abc123.r2.dev/image.png - S3 authenticated/presigned: https://<ACCOUNT_ID>.r2.cloudflarestorage.com/my-bucket/image.png?X-Amz-Algorithm=... (generated via SDK with R2 API token).
Citations:
- 1: https://developers.cloudflare.com/rules/origin-rules/tutorials/point-to-r2-bucket-with-custom-domain/
- 2: https://developers.cloudflare.com/r2/buckets/public-buckets/
- 3: https://community.cloudflare.com/t/s3-url-access-for-r2-with-custom-domain/646709
- 4: https://developers.cloudflare.com/r2/api/s3/presigned-urls/index.md
- 5: https://developers.cloudflare.com/r2/api/s3/presigned-urls/
- 6: https://developers.cloudflare.com/r2/get-started/s3/
🏁 Script executed:
# First, let's look at the file structure and find the S3CompatibleStorageProvider class
fd -t f "storageProviders" --type typescriptRepository: easternkite/Pasterly
Length of output: 241
🏁 Script executed:
# Read the storageProviders.ts file to understand the S3CompatibleStorageProvider implementation
wc -l src/storageProviders.tsRepository: easternkite/Pasterly
Length of output: 91
🏁 Script executed:
# Let's read the relevant sections - the R2 case and the S3CompatibleStorageProvider class
cat -n src/storageProviders.ts | sed -n '1,50p'Repository: easternkite/Pasterly
Length of output: 2174
🏁 Script executed:
# Search for the S3CompatibleStorageProvider class definition and buildPublicUrl method
ast-grep --pattern 'class S3CompatibleStorageProvider {
$$$
buildPublicUrl($$$) {
$$$
}
$$$
}'Repository: easternkite/Pasterly
Length of output: 46
🏁 Script executed:
# Also search for how publicBaseUrl is used in the buildPublicUrl method
rg -A 10 "buildPublicUrl" src/storageProviders.tsRepository: easternkite/Pasterly
Length of output: 930
🏁 Script executed:
# Get more context around buildPublicUrl to see the complete fallback logic
rg -B 5 -A 20 "buildPublicUrl" src/storageProviders.tsRepository: easternkite/Pasterly
Length of output: 1933
🏁 Script executed:
# Get the S3CompatibleStorageProvider class definition and constructor
rg -B 5 -A 30 "class S3CompatibleStorageProvider" src/storageProviders.tsRepository: easternkite/Pasterly
Length of output: 1489
🏁 Script executed:
# Check the factory function to see all R2-related config handling
sed -n '360,400p' src/storageProviders.tsRepository: easternkite/Pasterly
Length of output: 1441
Require r2PublicBaseUrl instead of falling back to the R2 S3 API endpoint.
When r2PublicBaseUrl is optional and null, buildPublicUrl() falls back to constructing URLs from the endpoint https://{accountId}.r2.cloudflarestorage.com. This endpoint is the S3 API for authenticated access, not a public HTTP endpoint. Generated URLs like https://accountid.r2.cloudflarestorage.com/bucket/image.png require S3 authentication and cannot be embedded in public markdown. Make r2PublicBaseUrl required with validation to ensure users provide a proper public access endpoint (custom domain or r2.dev subdomain).
Proposed factory validation
case 'r2':
if (!config.r2AccountId) {
throw new Error('R2 account ID is required');
}
if (!config.r2BucketName) {
throw new Error('R2 bucket name is required');
}
if (!config.r2AccessKeyId || !config.r2SecretAccessKey) {
throw new Error('R2 access key ID and secret access key are required');
}
+ if (!config.r2PublicBaseUrl) {
+ throw new Error('R2 public base URL is required for returned image URLs');
+ }
return new S3CompatibleStorageProvider({
bucketName: config.r2BucketName,
region: 'auto',
endpoint: `https://${config.r2AccountId}.r2.cloudflarestorage.com`,
accessKeyId: config.r2AccessKeyId,
secretAccessKey: config.r2SecretAccessKey,
- publicBaseUrl: config.r2PublicBaseUrl || null,
+ publicBaseUrl: config.r2PublicBaseUrl,
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case 'r2': | |
| if (!config.r2AccountId) { | |
| throw new Error('R2 account ID is required'); | |
| } | |
| if (!config.r2BucketName) { | |
| throw new Error('R2 bucket name is required'); | |
| } | |
| if (!config.r2AccessKeyId || !config.r2SecretAccessKey) { | |
| throw new Error('R2 access key ID and secret access key are required'); | |
| } | |
| return new S3CompatibleStorageProvider({ | |
| bucketName: config.r2BucketName, | |
| region: 'auto', | |
| endpoint: `https://${config.r2AccountId}.r2.cloudflarestorage.com`, | |
| accessKeyId: config.r2AccessKeyId, | |
| secretAccessKey: config.r2SecretAccessKey, | |
| publicBaseUrl: config.r2PublicBaseUrl || null, | |
| }); | |
| case 'r2': | |
| if (!config.r2AccountId) { | |
| throw new Error('R2 account ID is required'); | |
| } | |
| if (!config.r2BucketName) { | |
| throw new Error('R2 bucket name is required'); | |
| } | |
| if (!config.r2AccessKeyId || !config.r2SecretAccessKey) { | |
| throw new Error('R2 access key ID and secret access key are required'); | |
| } | |
| if (!config.r2PublicBaseUrl) { | |
| throw new Error('R2 public base URL is required for returned image URLs'); | |
| } | |
| return new S3CompatibleStorageProvider({ | |
| bucketName: config.r2BucketName, | |
| region: 'auto', | |
| endpoint: `https://${config.r2AccountId}.r2.cloudflarestorage.com`, | |
| accessKeyId: config.r2AccessKeyId, | |
| secretAccessKey: config.r2SecretAccessKey, | |
| publicBaseUrl: config.r2PublicBaseUrl, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/storageProviders.ts` around lines 370 - 387, The R2 case should require
and validate r2PublicBaseUrl instead of falling back to the S3 API endpoint: in
the 'r2' branch verify config.r2PublicBaseUrl is present and non-empty and throw
a clear Error if missing, then construct the S3CompatibleStorageProvider using
config.r2PublicBaseUrl as publicBaseUrl (do not use
`https://${config.r2AccountId}.r2.cloudflarestorage.com` fallback); update any
validation messages to reference r2PublicBaseUrl and keep existing checks for
r2AccountId, r2BucketName, r2AccessKeyId and r2SecretAccessKey (refer to the
'r2' case and S3CompatibleStorageProvider / buildPublicUrl usage).
Add support for R2's free storage protocol
Summary by CodeRabbit