-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add support for Cloudflare R2 storage provider #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -302,7 +302,7 @@ export class S3CompatibleStorageProvider implements StorageProvider { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Factory function to create the appropriate storage provider | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function createStorageProvider( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'firebase' | 'gcs' | 's3', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'firebase' | 'gcs' | 's3' | 'r2', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firebaseBucketUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gcsBucketName?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -317,6 +317,11 @@ export function createStorageProvider( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s3SessionToken?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s3PublicBaseUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s3ForcePathStyle?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r2AccountId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r2BucketName?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r2AccessKeyId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r2SecretAccessKey?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| r2PublicBaseUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): StorageProvider { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch (type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -362,6 +367,25 @@ export function createStorageProvider( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| forcePathStyle: config.s3ForcePathStyle || false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+370
to
+387
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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 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 When 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Unknown storage type: ${type}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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:
Repository: easternkite/Pasterly
Length of output: 294
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 3326
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 1043
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 718
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 2588
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 484
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 911
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 1269
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 573
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 3700
🏁 Script executed:
Repository: easternkite/Pasterly
Length of output: 1466
🏁 Script executed:
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 => textAlso applies to: 514-524
🤖 Prompt for AI Agents