add cos support(wip-2)

This commit is contained in:
Xheldon 2025-07-30 14:40:04 +08:00
parent a03e57fe44
commit b3ad2efb2b
5 changed files with 438 additions and 127 deletions

View file

@ -147,6 +147,14 @@ export class CosService {
// Generate signature for Tencent COS
const signature = await this.generateTencentSignature('PUT', remotePath, date, file.type);
console.log('Tencent COS upload debug:', {
url,
remotePath,
signature: signature.substring(0, 50) + '...',
fileSize: file.size,
fileType: file.type
});
// Convert File to ArrayBuffer for requestUrl
const fileBuffer = await file.arrayBuffer();
@ -154,7 +162,6 @@ export class CosService {
url: url,
method: 'PUT',
headers: {
'Date': date,
'Content-Type': file.type,
'Authorization': signature
},
@ -304,7 +311,6 @@ export class CosService {
url: url,
method: 'DELETE',
headers: {
'Date': date,
'Authorization': signature
}
});
@ -346,6 +352,16 @@ export class CosService {
private async generateAliyunSignature(method: string, path: string, date: string, contentType?: string): Promise<string> {
const stringToSign = `${method}\n\n${contentType || ''}\n${date}\n/${this.config.bucket}/${path}`;
console.log('Aliyun OSS signature debug:', {
method,
path,
date,
contentType,
stringToSign: stringToSign.replace(/\n/g, '\\n'),
bucket: this.config.bucket,
region: this.config.region
});
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
@ -356,29 +372,151 @@ export class CosService {
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign));
return btoa(String.fromCharCode(...new Uint8Array(signature)));
const signatureBase64 = btoa(String.fromCharCode(...new Uint8Array(signature)));
console.log('Generated Aliyun signature:', signatureBase64.substring(0, 20) + '...');
return signatureBase64;
}
/**
* Generate signature for Tencent COS
*/
private async generateTencentSignature(method: string, path: string, date: string, contentType?: string): Promise<string> {
// Simplified Tencent COS signature - in production, you might want to use their SDK
const stringToSign = `${method}\n/${this.config.bucket}/${path}\n\ndate=${date}&host=${this.config.bucket}.cos.${this.config.region}.myqcloud.com`;
// Tencent COS signature v5 implementation according to official documentation
const now = Math.floor(Date.now() / 1000);
const expireTime = now + 3600; // 1 hour expiry
const keyTime = `${now};${expireTime}`;
console.log('Tencent signature debug:', {
method,
path,
keyTime,
secretId: this.config.secretId.substring(0, 8) + '...',
bucket: this.config.bucket,
region: this.config.region
});
// Step 1: Generate SignKey using HMAC-SHA1(SecretKey, KeyTime)
const signKey = await this.hmacSha1(this.config.secretKey, keyTime);
// Step 2: Generate HttpString
const httpMethod = method.toLowerCase();
const uriPathname = `/${path}`;
const httpParameters = ''; // No query parameters for PUT
const httpHeaders = `host=${this.config.bucket}.cos.${this.config.region}.myqcloud.com`;
const httpString = `${httpMethod}\n${uriPathname}\n${httpParameters}\n${httpHeaders}\n`;
// Step 3: Generate StringToSign
const httpStringSha1 = await this.sha1Hash(httpString);
const stringToSign = `sha1\n${keyTime}\n${httpStringSha1}\n`;
// Step 4: Generate Signature using HMAC-SHA1(SignKey, StringToSign)
const signature = await this.hmacSha1(signKey, stringToSign);
// Step 5: Generate Authorization header
const authorization = `q-sign-algorithm=sha1&q-ak=${this.config.secretId}&q-sign-time=${keyTime}&q-key-time=${keyTime}&q-header-list=host&q-url-param-list=&q-signature=${signature}`;
console.log('Generated authorization:', authorization.substring(0, 100) + '...');
return authorization;
}
/**
* HMAC-SHA1 implementation
*/
private async hmacSha1(key: string, data: string): Promise<string> {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
const keyBuffer = encoder.encode(key);
const dataBuffer = encoder.encode(data);
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(this.config.secretKey),
keyBuffer,
{ name: 'HMAC', hash: 'SHA-1' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign));
const signatureB64 = btoa(String.fromCharCode(...new Uint8Array(signature)));
const signature = await crypto.subtle.sign('HMAC', cryptoKey, dataBuffer);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* SHA1 hash implementation
*/
private async sha1Hash(data: string): Promise<string> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-1', dataBuffer);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* SHA256 hash implementation
*/
private async sha256Hash(data: string): Promise<string> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* HMAC-SHA256 implementation (returns hex string)
*/
private async hmacSha256Hex(key: ArrayBuffer, data: string): Promise<string> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
return `q-sign-algorithm=sha1&q-ak=${this.config.secretId}&q-sign-time=0;32503651200&q-key-time=0;32503651200&q-header-list=date;host&q-url-param-list=&q-signature=${signatureB64}`;
const cryptoKey = await crypto.subtle.importKey(
'raw',
key,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, dataBuffer);
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
/**
* HMAC-SHA256 implementation (returns ArrayBuffer)
*/
private async hmacSha256(key: ArrayBuffer, data: string): Promise<ArrayBuffer> {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const cryptoKey = await crypto.subtle.importKey(
'raw',
key,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
return await crypto.subtle.sign('HMAC', cryptoKey, dataBuffer);
}
/**
* Generate AWS signature key
*/
private async getAWSSignatureKey(secretKey: string, dateStamp: string, region: string, service: string): Promise<ArrayBuffer> {
const encoder = new TextEncoder();
const kDate = await this.hmacSha256(encoder.encode(`AWS4${secretKey}`), dateStamp);
const kRegion = await this.hmacSha256(kDate, region);
const kService = await this.hmacSha256(kRegion, service);
const kSigning = await this.hmacSha256(kService, 'aws4_request');
return kSigning;
}
/**
@ -387,13 +525,42 @@ export class CosService {
private async generateAWSSignature(method: string, path: string, date: Date, contentType?: string): Promise<{ headers: Record<string, string> }> {
const isoDate = date.toISOString().replace(/[:\-]|\.\d{3}/g, '');
const dateStamp = isoDate.substring(0, 8);
const service = 's3';
const algorithm = 'AWS4-HMAC-SHA256';
// Step 1: Create canonical request
const host = `${this.config.bucket}.s3.${this.config.region}.amazonaws.com`;
const canonicalUri = `/${path}`;
const canonicalQueryString = '';
const canonicalHeaders = `host:${host}\nx-amz-content-sha256:UNSIGNED-PAYLOAD\nx-amz-date:${isoDate}\n`;
const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
const payloadHash = 'UNSIGNED-PAYLOAD';
const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
// Step 2: Create string to sign
const credentialScope = `${dateStamp}/${this.config.region}/${service}/aws4_request`;
const canonicalRequestHash = await this.sha256Hash(canonicalRequest);
const stringToSign = `${algorithm}\n${isoDate}\n${credentialScope}\n${canonicalRequestHash}`;
// Step 3: Calculate signature
const signingKey = await this.getAWSSignatureKey(this.config.secretKey, dateStamp, this.config.region, service);
const signature = await this.hmacSha256Hex(signingKey, stringToSign);
console.log('AWS S3 signature debug:', {
method,
path,
bucket: this.config.bucket,
region: this.config.region,
canonicalRequest: canonicalRequest.substring(0, 100) + '...',
stringToSign: stringToSign.substring(0, 100) + '...',
signature: signature.substring(0, 16) + '...'
});
// This is a simplified implementation
// In production, you should use AWS SDK or a complete V4 signature implementation
const headers = {
'X-Amz-Date': isoDate,
'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD',
'Authorization': `AWS4-HMAC-SHA256 Credential=${this.config.secretId}/${dateStamp}/${this.config.region}/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=placeholder`
'X-Amz-Content-Sha256': payloadHash,
'Authorization': `${algorithm} Credential=${this.config.secretId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
};
return { headers };
@ -403,13 +570,46 @@ export class CosService {
* Generate signature for Cloudflare R2 (S3 compatible)
*/
private async generateCloudflareSignature(method: string, path: string, date: Date, contentType?: string): Promise<{ headers: Record<string, string> }> {
// Similar to AWS S3 V4 signature
// Cloudflare R2 uses AWS S3 V4 signature with 'auto' region
const isoDate = date.toISOString().replace(/[:\-]|\.\d{3}/g, '');
const dateStamp = isoDate.substring(0, 8);
const service = 's3';
const algorithm = 'AWS4-HMAC-SHA256';
const region = 'auto'; // Cloudflare R2 uses 'auto' as region
// Step 1: Create canonical request
const host = new URL(this.config.endpoint).host;
const canonicalUri = `/${path}`;
const canonicalQueryString = '';
const canonicalHeaders = `host:${host}\nx-amz-content-sha256:UNSIGNED-PAYLOAD\nx-amz-date:${isoDate}\n`;
const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
const payloadHash = 'UNSIGNED-PAYLOAD';
const canonicalRequest = `${method}\n${canonicalUri}\n${canonicalQueryString}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
// Step 2: Create string to sign
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
const canonicalRequestHash = await this.sha256Hash(canonicalRequest);
const stringToSign = `${algorithm}\n${isoDate}\n${credentialScope}\n${canonicalRequestHash}`;
// Step 3: Calculate signature
const signingKey = await this.getAWSSignatureKey(this.config.secretKey, dateStamp, region, service);
const signature = await this.hmacSha256Hex(signingKey, stringToSign);
console.log('Cloudflare R2 signature debug:', {
method,
path,
endpoint: this.config.endpoint,
host,
canonicalRequest: canonicalRequest.substring(0, 100) + '...',
stringToSign: stringToSign.substring(0, 100) + '...',
signature: signature.substring(0, 16) + '...'
});
const headers = {
'X-Amz-Date': isoDate,
'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD',
'Authorization': `AWS4-HMAC-SHA256 Credential=${this.config.secretId}/${isoDate.substring(0, 8)}/auto/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=placeholder`
'X-Amz-Content-Sha256': payloadHash,
'Authorization': `${algorithm} Credential=${this.config.secretId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
};
return { headers };

View file

@ -49,7 +49,9 @@ const translations = {
'settings.clear.cache.button': '清空缓存',
// COS settings
'settings.cos.section.title': '图片上传设置',
'settings.image.section.title': '图片处理',
'settings.cos.provider.section.title': '云服务商设置',
'settings.image.upload.section.title': '图片上传设置',
'settings.cos.provider.name': '云存储服务商',
'settings.cos.provider.desc': '选择要使用的云对象存储服务商',
'settings.cos.provider.aliyun': '阿里云 OSS',
@ -82,6 +84,10 @@ const translations = {
'settings.cos.upload.path.name': '上传路径模板',
'settings.cos.upload.path.desc': '支持占位符:{PATH}(当前文件路径)、{FILENAME}(文件名)、{FOLDER}(文件夹名)、{YYYY}(年)、{MM}(月)、{DD}(日)',
'settings.cos.upload.path.placeholder': '例如images/{YYYY}/{MM}/{DD}',
'settings.cos.clear.name': '清空当前配置',
'settings.cos.clear.desc': '清空当前选中提供商的所有配置信息',
'settings.cos.clear.button': '清空配置',
'settings.cos.clear.success': '配置已清空',
'settings.cos.test.name': '测试COS配置',
'settings.cos.test.desc': '测试当前COS配置是否可用',
'settings.cos.test.button': '测试连接',
@ -243,7 +249,9 @@ const translations = {
'settings.clear.cache.button': 'Clear Cache',
// COS settings
'settings.cos.section.title': 'Image Upload Settings',
'settings.image.section.title': 'Image Processing',
'settings.cos.provider.section.title': 'Cloud Provider Settings',
'settings.image.upload.section.title': 'Image Upload Settings',
'settings.cos.provider.name': 'Cloud Storage Provider',
'settings.cos.provider.desc': 'Select the cloud object storage service to use',
'settings.cos.provider.aliyun': 'Aliyun OSS',
@ -276,6 +284,10 @@ const translations = {
'settings.cos.upload.path.name': 'Upload Path Template',
'settings.cos.upload.path.desc': 'Supports placeholders: {PATH} (current file path), {FILENAME} (file name), {FOLDER} (folder name), {YYYY} (year), {MM} (month), {DD} (day)',
'settings.cos.upload.path.placeholder': 'e.g.: images/{YYYY}/{MM}/{DD}',
'settings.cos.clear.name': 'Clear Current Configuration',
'settings.cos.clear.desc': 'Clear all configuration for the currently selected provider',
'settings.cos.clear.button': 'Clear Config',
'settings.cos.clear.success': 'Configuration cleared',
'settings.cos.test.name': 'Test COS Configuration',
'settings.cos.test.desc': 'Test if current COS configuration is available',
'settings.cos.test.button': 'Test Connection',

241
main.ts
View file

@ -1,5 +1,5 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, Menu, TFolder } from 'obsidian';
import { GitSyncSettings, DEFAULT_SETTINGS, SyncResult, CosConfig, ImagePasteContext } from './types';
import { GitSyncSettings, DEFAULT_SETTINGS, SyncResult, CosConfig, ImagePasteContext, CosProviderConfig } from './types';
import { GitHubService } from './github-service';
import { setLanguage, t, getSupportedLanguages, getActualLanguage } from './i18n-simple';
import { FileCacheService } from './file-cache';
@ -697,20 +697,51 @@ export default class GitSyncPlugin extends Plugin {
}
}
/**
* Get current COS configuration
*/
getCurrentCosConfig(): CosProviderConfig {
return this.settings.cosConfigs[this.settings.cosProvider];
}
/**
* Update current COS configuration
*/
updateCurrentCosConfig(updates: Partial<CosProviderConfig>): void {
const currentConfig = this.getCurrentCosConfig();
Object.assign(currentConfig, updates);
}
/**
* Clear current COS configuration
*/
clearCurrentCosConfig(): void {
const emptyConfig: CosProviderConfig = {
secretId: '',
secretKey: '',
bucket: '',
endpoint: '',
cdnUrl: '',
region: ''
};
this.settings.cosConfigs[this.settings.cosProvider] = emptyConfig;
}
/**
* Check if COS is properly configured
*/
private isCosConfigured(): boolean {
const currentConfig = this.getCurrentCosConfig();
const requiredFields = [
this.settings.cosSecretId,
this.settings.cosSecretKey,
this.settings.cosBucket,
this.settings.cosRegion // Region is required for all providers
currentConfig.secretId,
currentConfig.secretKey,
currentConfig.bucket,
currentConfig.region // Region is required for all providers
];
// Add provider-specific required fields
if (this.settings.cosProvider === 'cloudflare') {
requiredFields.push(this.settings.cosEndpoint); // Endpoint only required for Cloudflare R2
requiredFields.push(currentConfig.endpoint); // Endpoint only required for Cloudflare R2
}
return requiredFields.every(field => field && field.trim().length > 0);
@ -743,14 +774,15 @@ export default class GitSyncPlugin extends Plugin {
try {
// Create COS configuration
const currentConfig = this.getCurrentCosConfig();
const cosConfig: CosConfig = {
provider: this.settings.cosProvider,
secretId: this.settings.cosSecretId,
secretKey: this.settings.cosSecretKey,
bucket: this.settings.cosBucket,
endpoint: this.settings.cosEndpoint,
cdnUrl: this.settings.cosCdnUrl,
region: this.settings.cosRegion
secretId: currentConfig.secretId,
secretKey: currentConfig.secretKey,
bucket: currentConfig.bucket,
endpoint: currentConfig.endpoint,
cdnUrl: currentConfig.cdnUrl,
region: currentConfig.region
};
// Upload to COS
@ -921,12 +953,18 @@ class GitSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
// COS settings section
// Image processing main section
containerEl.createEl('h2', {
text: t('settings.cos.section.title'),
text: t('settings.image.section.title'),
cls: 'git-sync-section-title'
});
// Cloud provider settings subsection
containerEl.createEl('h3', {
text: t('settings.cos.provider.section.title'),
cls: 'git-sync-subsection-title'
});
// Cloud storage provider
new Setting(containerEl)
.setName(t('settings.cos.provider.name'))
@ -941,20 +979,23 @@ class GitSyncSettingTab extends PluginSettingTab {
dropdown.onChange(async (value) => {
this.plugin.settings.cosProvider = value as 'aliyun' | 'tencent' | 'aws' | 'cloudflare';
await this.plugin.saveSettings();
// Re-render settings to show/hide region field
// Re-render settings to show/hide region field and load saved config
this.display();
});
});
// Get current config for the selected provider
const currentConfig = this.plugin.getCurrentCosConfig();
// Secret ID
new Setting(containerEl)
.setName(t('settings.cos.secret.id.name') + ' *')
.setDesc(t('settings.cos.secret.id.desc'))
.addText(text => text
.setPlaceholder(t('settings.cos.secret.id.placeholder'))
.setValue(this.plugin.settings.cosSecretId)
.setValue(currentConfig.secretId)
.onChange(async (value) => {
this.plugin.settings.cosSecretId = value;
this.plugin.updateCurrentCosConfig({ secretId: value });
await this.plugin.saveSettings();
}));
@ -964,9 +1005,9 @@ class GitSyncSettingTab extends PluginSettingTab {
.setDesc(t('settings.cos.secret.key.desc'))
.addText(text => text
.setPlaceholder(t('settings.cos.secret.key.placeholder'))
.setValue(this.plugin.settings.cosSecretKey)
.setValue(currentConfig.secretKey)
.onChange(async (value) => {
this.plugin.settings.cosSecretKey = value;
this.plugin.updateCurrentCosConfig({ secretKey: value });
await this.plugin.saveSettings();
}));
@ -976,9 +1017,9 @@ class GitSyncSettingTab extends PluginSettingTab {
.setDesc(t('settings.cos.bucket.desc'))
.addText(text => text
.setPlaceholder(t('settings.cos.bucket.placeholder'))
.setValue(this.plugin.settings.cosBucket)
.setValue(currentConfig.bucket)
.onChange(async (value) => {
this.plugin.settings.cosBucket = value;
this.plugin.updateCurrentCosConfig({ bucket: value });
await this.plugin.saveSettings();
}));
@ -988,9 +1029,9 @@ class GitSyncSettingTab extends PluginSettingTab {
.setDesc(t('settings.cos.region.desc'))
.addText(text => text
.setPlaceholder(t('settings.cos.region.placeholder'))
.setValue(this.plugin.settings.cosRegion)
.setValue(currentConfig.region)
.onChange(async (value) => {
this.plugin.settings.cosRegion = value;
this.plugin.updateCurrentCosConfig({ region: value });
await this.plugin.saveSettings();
}));
@ -1001,9 +1042,9 @@ class GitSyncSettingTab extends PluginSettingTab {
.setDesc(t('settings.cos.endpoint.desc'))
.addText(text => text
.setPlaceholder(t('settings.cos.endpoint.placeholder'))
.setValue(this.plugin.settings.cosEndpoint)
.setValue(currentConfig.endpoint)
.onChange(async (value) => {
this.plugin.settings.cosEndpoint = value;
this.plugin.updateCurrentCosConfig({ endpoint: value });
await this.plugin.saveSettings();
}));
}
@ -1014,12 +1055,90 @@ class GitSyncSettingTab extends PluginSettingTab {
.setDesc(t('settings.cos.cdn.desc'))
.addText(text => text
.setPlaceholder(t('settings.cos.cdn.placeholder'))
.setValue(this.plugin.settings.cosCdnUrl)
.setValue(currentConfig.cdnUrl)
.onChange(async (value) => {
this.plugin.settings.cosCdnUrl = value;
this.plugin.updateCurrentCosConfig({ cdnUrl: value });
await this.plugin.saveSettings();
}));
// Clear current configuration button
new Setting(containerEl)
.setName(t('settings.cos.clear.name'))
.setDesc(t('settings.cos.clear.desc'))
.addButton(button => button
.setButtonText(t('settings.cos.clear.button'))
.setWarning()
.onClick(async () => {
this.plugin.clearCurrentCosConfig();
await this.plugin.saveSettings();
new Notice(t('settings.cos.clear.success'));
// Re-render to show cleared fields
this.display();
}));
// Test COS configuration
new Setting(containerEl)
.setName(t('settings.cos.test.name'))
.setDesc(t('settings.cos.test.desc'))
.addButton(button => button
.setButtonText(t('settings.cos.test.button'))
.onClick(async () => {
// Validate required fields based on provider
const requiredFields = [
currentConfig.secretId,
currentConfig.secretKey,
currentConfig.bucket,
currentConfig.region // Region is required for all providers
];
// Add provider-specific required fields
if (this.plugin.settings.cosProvider === 'cloudflare') {
requiredFields.push(currentConfig.endpoint); // Endpoint only required for Cloudflare R2
}
if (requiredFields.some(field => !field.trim())) {
new Notice(t('cos.error.config.invalid'));
return;
}
// Set loading state
button.setButtonText(t('settings.cos.test.loading'));
button.setDisabled(true);
try {
const cosConfig = {
provider: this.plugin.settings.cosProvider,
secretId: currentConfig.secretId,
secretKey: currentConfig.secretKey,
bucket: currentConfig.bucket,
endpoint: currentConfig.endpoint,
cdnUrl: currentConfig.cdnUrl,
region: currentConfig.region
};
const cosService = new (await import('./cos-service')).CosService(cosConfig);
const result = await cosService.testConnection();
if (result.success) {
new Notice(t('cos.test.success'));
} else {
new Notice(t('cos.test.failed', { error: result.message }));
}
} catch (error) {
console.error('COS test error:', error);
new Notice(t('cos.test.failed', { error: error.message }));
} finally {
button.setButtonText(t('settings.cos.test.button'));
button.setDisabled(false);
}
}));
// Image upload settings subsection
containerEl.createEl('h3', {
text: t('settings.image.upload.section.title'),
cls: 'git-sync-subsection-title'
});
// Keep images locally
new Setting(containerEl)
.setName(t('settings.cos.keep.local.name'))
@ -1059,75 +1178,7 @@ class GitSyncSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
// Test COS configuration
new Setting(containerEl)
.setName(t('settings.cos.test.name'))
.setDesc(t('settings.cos.test.desc'))
.addButton(button => button
.setButtonText(t('settings.cos.test.button'))
.onClick(async () => {
// Validate required fields based on provider
const requiredFields = [
this.plugin.settings.cosSecretId,
this.plugin.settings.cosSecretKey,
this.plugin.settings.cosBucket,
this.plugin.settings.cosRegion // Region is required for all providers
];
// Add provider-specific required fields
if (this.plugin.settings.cosProvider === 'cloudflare') {
requiredFields.push(this.plugin.settings.cosEndpoint); // Endpoint only required for Cloudflare R2
}
if (requiredFields.some(field => !field.trim())) {
new Notice(t('cos.error.config.invalid'));
return;
}
// Set loading state
button.setButtonText(t('settings.cos.test.loading'));
button.setDisabled(true);
try {
const cosConfig = {
provider: this.plugin.settings.cosProvider,
secretId: this.plugin.settings.cosSecretId,
secretKey: this.plugin.settings.cosSecretKey,
bucket: this.plugin.settings.cosBucket,
endpoint: this.plugin.settings.cosEndpoint,
cdnUrl: this.plugin.settings.cosCdnUrl,
region: this.plugin.settings.cosRegion
};
const cosService = new (await import('./cos-service')).CosService(cosConfig);
const result = await cosService.testConnection();
if (result.success) {
new Notice(t('cos.test.success'));
} else {
new Notice(t('cos.test.failed', { error: result.message }));
}
} catch (error) {
console.error('COS test error:', error);
new Notice(t('cos.test.failed', { error: error.message }));
} finally {
button.setButtonText(t('settings.cos.test.button'));
button.setDisabled(false);
}
}));
// Save settings button - put below basic settings area
const saveButtonContainer = containerEl.createEl('div', {
cls: 'git-sync-save-button-container'
});
const saveButton = saveButtonContainer.createEl('button', {
cls: 'mod-cta',
text: t('settings.save.button')
});
saveButton.addEventListener('click', async () => {
await this.plugin.saveSettings();
new Notice(t('notice.settings.saved'));
});
// Danger zone title
containerEl.createEl('h2', {

View file

@ -323,3 +323,14 @@
.sponsor {
color: var(--color-orange);
}
/* Subsection titles styling */
.git-sync-subsection-title {
color: var(--text-normal);
font-size: var(--font-ui-medium);
font-weight: var(--font-weight-medium);
margin-top: 24px;
margin-bottom: 12px;
padding-left: 8px;
border-left: 3px solid var(--text-accent);
}

View file

@ -1,3 +1,12 @@
export interface CosProviderConfig {
secretId: string;
secretKey: string;
bucket: string;
endpoint: string;
cdnUrl: string;
region: string;
}
export interface GitSyncSettings {
githubToken: string;
repositoryUrl: string; // Format: @https://github.com/user/repo/path/to/folder
@ -6,13 +15,13 @@ export interface GitSyncSettings {
language: 'zh' | 'en' | 'auto'; // Language setting, auto means follow system
// COS (Cloud Object Storage) settings
cosProvider: 'aliyun' | 'tencent' | 'aws' | 'cloudflare'; // Cloud service provider
cosSecretId: string; // Access Key ID / Secret ID
cosSecretKey: string; // Access Key Secret / Secret Key
cosBucket: string; // Bucket name
cosEndpoint: string; // Service endpoint
cosCdnUrl: string; // CDN domain for accessing uploaded files
cosRegion: string; // Region (for AWS/Cloudflare)
cosProvider: 'aliyun' | 'tencent' | 'aws' | 'cloudflare'; // Current selected provider
cosConfigs: {
aliyun: CosProviderConfig;
tencent: CosProviderConfig;
aws: CosProviderConfig;
cloudflare: CosProviderConfig;
};
// Local image storage settings
keepLocalImages: boolean; // Whether to keep images locally after upload
@ -31,12 +40,40 @@ export const DEFAULT_SETTINGS: GitSyncSettings = {
// COS default settings
cosProvider: 'aliyun',
cosSecretId: '',
cosSecretKey: '',
cosBucket: '',
cosEndpoint: '',
cosCdnUrl: '',
cosRegion: '',
cosConfigs: {
aliyun: {
secretId: '',
secretKey: '',
bucket: '',
endpoint: '',
cdnUrl: '',
region: ''
},
tencent: {
secretId: '',
secretKey: '',
bucket: '',
endpoint: '',
cdnUrl: '',
region: ''
},
aws: {
secretId: '',
secretKey: '',
bucket: '',
endpoint: '',
cdnUrl: '',
region: ''
},
cloudflare: {
secretId: '',
secretKey: '',
bucket: '',
endpoint: '',
cdnUrl: '',
region: ''
}
},
// Local image storage defaults
keepLocalImages: false,