diff --git a/src/main.ts b/src/main.ts index 13ff047..7d5acf2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -101,6 +101,19 @@ export default class Pasterly extends Plugin { new Notice('Please set your S3 credentials in settings first.'); return; } + } 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; + } } this.storageProvider = createStorageProvider(this.settings.storageType, { @@ -117,6 +130,11 @@ export default class Pasterly extends Plugin { s3SessionToken: this.settings.s3SessionToken, s3PublicBaseUrl: this.settings.s3PublicBaseUrl, s3ForcePathStyle: this.settings.s3ForcePathStyle, + r2AccountId: this.settings.r2AccountId, + r2BucketName: this.settings.r2BucketName, + r2AccessKeyId: this.settings.r2AccessKeyId, + r2SecretAccessKey: this.settings.r2SecretAccessKey, + r2PublicBaseUrl: this.settings.r2PublicBaseUrl, }); } catch (error) { console.error('Failed to initialize storage provider:', error); @@ -206,14 +224,18 @@ export default class Pasterly extends Plugin { const normalizedCdnBaseUrl = normalizeOptionalBaseUrl(this.settings.gcsCdnBaseUrl); const normalizedS3Endpoint = normalizeOptionalBaseUrl(this.settings.s3Endpoint); const normalizedS3PublicBaseUrl = normalizeOptionalBaseUrl(this.settings.s3PublicBaseUrl); + const normalizedR2PublicBaseUrl = normalizeOptionalBaseUrl(this.settings.r2PublicBaseUrl); + if ( normalizedCdnBaseUrl !== this.settings.gcsCdnBaseUrl || normalizedS3Endpoint !== this.settings.s3Endpoint || - normalizedS3PublicBaseUrl !== this.settings.s3PublicBaseUrl + normalizedS3PublicBaseUrl !== this.settings.s3PublicBaseUrl || + normalizedR2PublicBaseUrl !== this.settings.r2PublicBaseUrl ) { this.settings.gcsCdnBaseUrl = normalizedCdnBaseUrl; this.settings.s3Endpoint = normalizedS3Endpoint; this.settings.s3PublicBaseUrl = normalizedS3PublicBaseUrl; + this.settings.r2PublicBaseUrl = normalizedR2PublicBaseUrl; await this.saveSettings(); } } @@ -242,9 +264,10 @@ class PasterlySettingTab extends PluginSettingTab { .addDropdown(dropdown => dropdown .addOption('firebase', 'Firebase Storage') .addOption('gcs', 'Google Cloud Storage') - .addOption('s3', 'S3-compatible Storage (AWS S3 / R2)') + .addOption('s3', 'S3-compatible Storage (AWS S3 / MinIO)') + .addOption('r2', 'Cloudflare R2') .setValue(this.plugin.settings.storageType) - .onChange(async (value: 'firebase' | 'gcs' | 's3') => { + .onChange(async (value: 'firebase' | 'gcs' | 's3' | 'r2') => { this.plugin.settings.storageType = value; await this.plugin.saveSettings(); this.plugin.debouncedInitializeStorage(); @@ -436,6 +459,71 @@ class PasterlySettingTab extends PluginSettingTab { })); } + if (this.plugin.settings.storageType === 'r2') { + new Setting(containerEl) + .setName('R2 Account ID') + .setDesc('Cloudflare Account ID (found in R2 dashboard)') + .addText(text => text + .setPlaceholder('your-account-id') + .setValue(this.plugin.settings.r2AccountId) + .onChange(async (value) => { + this.plugin.settings.r2AccountId = value.trim(); + await this.plugin.saveSettings(); + this.plugin.debouncedInitializeStorage(); + })); + + new Setting(containerEl) + .setName('R2 Bucket Name') + .setDesc('Name of your R2 bucket') + .addText(text => text + .setPlaceholder('my-bucket-name') + .setValue(this.plugin.settings.r2BucketName) + .onChange(async (value) => { + this.plugin.settings.r2BucketName = value.trim(); + await this.plugin.saveSettings(); + this.plugin.debouncedInitializeStorage(); + })); + + new Setting(containerEl) + .setName('R2 Access Key ID') + .setDesc('R2 API Token Access Key ID') + .addText(text => text + .setPlaceholder('access-key-id') + .setValue(this.plugin.settings.r2AccessKeyId) + .onChange(async (value) => { + this.plugin.settings.r2AccessKeyId = value.trim(); + await this.plugin.saveSettings(); + this.plugin.debouncedInitializeStorage(); + })); + + new Setting(containerEl) + .setName('R2 Secret Access Key') + .setDesc('R2 API Token Secret Access Key') + .addText(text => { + text + .setPlaceholder('••••••••') + .setValue(this.plugin.settings.r2SecretAccessKey) + .onChange(async (value) => { + this.plugin.settings.r2SecretAccessKey = value.trim(); + await this.plugin.saveSettings(); + this.plugin.debouncedInitializeStorage(); + }); + text.inputEl.type = 'password'; + }); + + new Setting(containerEl) + .setName('R2 Public Base URL') + .setDesc('Optional: Custom Domain or R2.dev URL (e.g., https://images.example.com)') + .addText(text => text + .setPlaceholder('https://images.example.com') + .setValue(this.plugin.settings.r2PublicBaseUrl) + .onChange(async (value) => { + this.plugin.settings.r2PublicBaseUrl = normalizeOptionalBaseUrl(value); + await this.plugin.saveSettings(); + this.plugin.debouncedInitializeStorage(); + })); + } + // Common Settings new Setting(containerEl) .setName('Fixed Size') diff --git a/src/storageProviders.ts b/src/storageProviders.ts index f9dca6f..0f6221c 100644 --- a/src/storageProviders.ts +++ b/src/storageProviders.ts @@ -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, + }); + default: throw new Error(`Unknown storage type: ${type}`); } diff --git a/src/types.ts b/src/types.ts index b776782..33b96c7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,7 @@ export interface StorageProvider { /** * Storage type enumeration */ -export type StorageType = 'firebase' | 'gcs' | 's3'; +export type StorageType = 'firebase' | 'gcs' | 's3' | 'r2'; /** * Settings interface for the Pasterly plugin @@ -43,6 +43,13 @@ export interface PasterlySettings { s3SessionToken: string; s3PublicBaseUrl: string; s3ForcePathStyle: boolean; + + // Cloudflare R2 settings + r2AccountId: string; + r2BucketName: string; + r2AccessKeyId: string; + r2SecretAccessKey: string; + r2PublicBaseUrl: string; } export const DEFAULT_SETTINGS: PasterlySettings = { @@ -61,4 +68,9 @@ export const DEFAULT_SETTINGS: PasterlySettings = { s3SessionToken: '', s3PublicBaseUrl: '', s3ForcePathStyle: false, + r2AccountId: '', + r2BucketName: '', + r2AccessKeyId: '', + r2SecretAccessKey: '', + r2PublicBaseUrl: '', };