mirror of
https://github.com/xheldon/git-folder-sync.git
synced 2026-07-22 06:59:00 +00:00
add cos support(wip-2)
This commit is contained in:
parent
a03e57fe44
commit
b3ad2efb2b
5 changed files with 438 additions and 127 deletions
234
cos-service.ts
234
cos-service.ts
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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
241
main.ts
|
|
@ -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', {
|
||||
|
|
|
|||
11
styles.css
11
styles.css
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
63
types.ts
63
types.ts
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue