Skip to content

feat: add support for Cloudflare R2 storage provider#10

Open
leolionart wants to merge 1 commit into
easternkite:masterfrom
leolionart:feat/cloudflare-r2-support
Open

feat: add support for Cloudflare R2 storage provider#10
leolionart wants to merge 1 commit into
easternkite:masterfrom
leolionart:feat/cloudflare-r2-support

Conversation

@leolionart
Copy link
Copy Markdown
Contributor

@leolionart leolionart commented Apr 19, 2026

Add support for R2's free storage protocol

Summary by CodeRabbit

  • New Features
    • Cloudflare R2 is now available as a storage provider option alongside existing storage backends.
    • New R2 configuration fields allow you to specify account ID, bucket name, API credentials, and an optional custom public base URL.
    • Required settings are validated automatically with clear notifications if any credentials are missing.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Walkthrough

Introduces Cloudflare R2 storage support by adding new configuration fields (r2AccountId, r2BucketName, r2AccessKeyId, r2SecretAccessKey, r2PublicBaseUrl), implementing R2 provider initialization using S3-compatible endpoint, adding UI controls for R2 settings, and validating required credentials during setup.

Changes

Cohort / File(s) Summary
Type Definitions
src/types.ts
Extended StorageType union to include 'r2'; added R2 configuration fields to PasterlySettings; initialized new R2 fields in DEFAULT_SETTINGS.
Storage Provider Factory
src/storageProviders.ts
Extended createStorageProvider function signature to accept 'r2' type; implemented R2 branch that computes Cloudflare R2 endpoint (https://{accountId}.r2.cloudflarestorage.com) and returns S3CompatibleStorageProvider with validated credentials.
Plugin Initialization & UI
src/main.ts
Added R2 storage type branch in initializeStorage with credential validation; extended settings UI dropdown to include 'r2' option; added conditional rendering of R2-specific input fields (account ID, bucket name, access/secret keys, public base URL); normalized and persisted r2PublicBaseUrl on changes.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #9: Introduces S3-compatible storage provider foundation that this PR extends by adding dedicated R2 branching and configuration in the factory and initialization logic.

Poem

🐰 wiggles nose with delight
R2 arrives with credentials in hand,
S3-compatible, perfectly planned,
Validation checks keep us secure and sound,
Another storage wonder has been found! 🪣

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding Cloudflare R2 as a new storage provider option, which is reflected across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between bff44c6 and 96d0606.

📒 Files selected for processing (3)
  • src/main.ts
  • src/storageProviders.ts
  • src/types.ts

Comment thread src/main.ts
Comment on lines +104 to 117
} 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;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 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 3

Repository: 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 -50

Repository: easternkite/Pasterly

Length of output: 1043


🏁 Script executed:

# Find the S3 provider implementation
fd -t f "\.ts$" | xargs rg "class.*S3.*Provider|publicBaseUrl" | head -40

Repository: 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 2

Repository: 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.ts

Repository: easternkite/Pasterly

Length of output: 911


🏁 Script executed:

# Get the complete buildPublicUrl method including the default case
rg -A 30 "private buildPublicUrl" src/storageProviders.ts

Repository: 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.ts

Repository: 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 -80

Repository: 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 -20

Repository: 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.js

Repository: 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 => text

Also 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.

Comment thread src/storageProviders.ts
Comment on lines +370 to +387
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,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 Script executed:

# First, let's look at the file structure and find the S3CompatibleStorageProvider class
fd -t f "storageProviders" --type typescript

Repository: easternkite/Pasterly

Length of output: 241


🏁 Script executed:

# Read the storageProviders.ts file to understand the S3CompatibleStorageProvider implementation
wc -l src/storageProviders.ts

Repository: 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.ts

Repository: 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.ts

Repository: easternkite/Pasterly

Length of output: 1933


🏁 Script executed:

# Get the S3CompatibleStorageProvider class definition and constructor
rg -B 5 -A 30 "class S3CompatibleStorageProvider" src/storageProviders.ts

Repository: 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.ts

Repository: 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.

Suggested change
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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant