mirror of
https://github.com/xheldon/git-folder-sync.git
synced 2026-07-22 06:59:00 +00:00
add cos support(wip-1)
This commit is contained in:
parent
4004668604
commit
a03e57fe44
4 changed files with 1013 additions and 4 deletions
457
cos-service.ts
Normal file
457
cos-service.ts
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
import { CosConfig, CosUploadResult } from './types';
|
||||
import { t } from './i18n-simple';
|
||||
import { requestUrl } from 'obsidian';
|
||||
|
||||
export class CosService {
|
||||
private config: CosConfig;
|
||||
|
||||
constructor(config: CosConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to COS
|
||||
*/
|
||||
async uploadFile(file: File, remotePath: string): Promise<CosUploadResult> {
|
||||
try {
|
||||
switch (this.config.provider) {
|
||||
case 'aliyun':
|
||||
return await this.uploadToAliyun(file, remotePath);
|
||||
case 'tencent':
|
||||
return await this.uploadToTencent(file, remotePath);
|
||||
case 'aws':
|
||||
return await this.uploadToAWS(file, remotePath);
|
||||
case 'cloudflare':
|
||||
return await this.uploadToCloudflare(file, remotePath);
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
message: t('cos.error.unsupported.provider', { provider: this.config.provider })
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('COS upload error:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: t('cos.error.upload.failed', { error: error.message })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test COS configuration
|
||||
*/
|
||||
async testConnection(): Promise<CosUploadResult> {
|
||||
try {
|
||||
// Create a small test file
|
||||
const testContent = 'test';
|
||||
const testFile = new File([testContent], 'test.txt', { type: 'text/plain' });
|
||||
const testPath = `test-${Date.now()}.txt`;
|
||||
|
||||
const result = await this.uploadFile(testFile, testPath);
|
||||
|
||||
if (result.success) {
|
||||
// Try to delete the test file after successful upload
|
||||
await this.deleteFile(testPath).catch(console.warn);
|
||||
return {
|
||||
success: true,
|
||||
message: t('cos.test.success')
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('COS test connection error:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: t('cos.test.failed', { error: error.message })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete file from COS
|
||||
*/
|
||||
private async deleteFile(remotePath: string): Promise<void> {
|
||||
switch (this.config.provider) {
|
||||
case 'aliyun':
|
||||
await this.deleteFromAliyun(remotePath);
|
||||
break;
|
||||
case 'tencent':
|
||||
await this.deleteFromTencent(remotePath);
|
||||
break;
|
||||
case 'aws':
|
||||
await this.deleteFromAWS(remotePath);
|
||||
break;
|
||||
case 'cloudflare':
|
||||
await this.deleteFromCloudflare(remotePath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload to Aliyun OSS
|
||||
*/
|
||||
private async uploadToAliyun(file: File, remotePath: string): Promise<CosUploadResult> {
|
||||
// For Aliyun OSS, construct endpoint from region
|
||||
const endpoint = `oss-${this.config.region}.aliyuncs.com`;
|
||||
const url = `https://${this.config.bucket}.${endpoint}/${remotePath}`;
|
||||
const date = new Date().toUTCString();
|
||||
|
||||
// Generate signature for Aliyun OSS
|
||||
const signature = await this.generateAliyunSignature('PUT', remotePath, date, file.type);
|
||||
|
||||
// Convert File to ArrayBuffer for requestUrl
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Date': date,
|
||||
'Content-Type': file.type,
|
||||
'Authorization': `OSS ${this.config.secretId}:${signature}`
|
||||
},
|
||||
body: fileBuffer
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
const finalUrl = this.config.cdnUrl ?
|
||||
`${this.config.cdnUrl}/${remotePath}` : url;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: t('cos.upload.success'),
|
||||
url: finalUrl,
|
||||
key: remotePath
|
||||
};
|
||||
} else {
|
||||
const errorText = response.text;
|
||||
return {
|
||||
success: false,
|
||||
message: t('cos.error.aliyun.upload.failed', {
|
||||
status: response.status,
|
||||
error: errorText
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload to Tencent COS
|
||||
*/
|
||||
private async uploadToTencent(file: File, remotePath: string): Promise<CosUploadResult> {
|
||||
const url = `https://${this.config.bucket}.cos.${this.config.region}.myqcloud.com/${remotePath}`;
|
||||
const date = new Date().toUTCString();
|
||||
|
||||
// Generate signature for Tencent COS
|
||||
const signature = await this.generateTencentSignature('PUT', remotePath, date, file.type);
|
||||
|
||||
// Convert File to ArrayBuffer for requestUrl
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Date': date,
|
||||
'Content-Type': file.type,
|
||||
'Authorization': signature
|
||||
},
|
||||
body: fileBuffer
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
const finalUrl = this.config.cdnUrl ?
|
||||
`${this.config.cdnUrl}/${remotePath}` : url;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: t('cos.upload.success'),
|
||||
url: finalUrl,
|
||||
key: remotePath
|
||||
};
|
||||
} else {
|
||||
const errorText = response.text;
|
||||
return {
|
||||
success: false,
|
||||
message: t('cos.error.tencent.upload.failed', {
|
||||
status: response.status,
|
||||
error: errorText
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload to AWS S3
|
||||
*/
|
||||
private async uploadToAWS(file: File, remotePath: string): Promise<CosUploadResult> {
|
||||
const url = `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${remotePath}`;
|
||||
const date = new Date();
|
||||
|
||||
// Generate signature for AWS S3 (V4)
|
||||
const signature = await this.generateAWSSignature('PUT', remotePath, date, file.type);
|
||||
|
||||
// Convert File to ArrayBuffer for requestUrl
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': file.type,
|
||||
...signature.headers
|
||||
},
|
||||
body: fileBuffer
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
const finalUrl = this.config.cdnUrl ?
|
||||
`${this.config.cdnUrl}/${remotePath}` : url;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: t('cos.upload.success'),
|
||||
url: finalUrl,
|
||||
key: remotePath
|
||||
};
|
||||
} else {
|
||||
const errorText = response.text;
|
||||
return {
|
||||
success: false,
|
||||
message: t('cos.error.aws.upload.failed', {
|
||||
status: response.status,
|
||||
error: errorText
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload to Cloudflare R2 (using S3 compatible API)
|
||||
*/
|
||||
private async uploadToCloudflare(file: File, remotePath: string): Promise<CosUploadResult> {
|
||||
const url = `${this.config.endpoint}/${remotePath}`;
|
||||
const date = new Date();
|
||||
|
||||
// Generate signature for Cloudflare R2 (S3 compatible)
|
||||
const signature = await this.generateCloudflareSignature('PUT', remotePath, date, file.type);
|
||||
|
||||
// Convert File to ArrayBuffer for requestUrl
|
||||
const fileBuffer = await file.arrayBuffer();
|
||||
|
||||
const response = await requestUrl({
|
||||
url: url,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': file.type,
|
||||
...signature.headers
|
||||
},
|
||||
body: fileBuffer
|
||||
});
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
const finalUrl = this.config.cdnUrl ?
|
||||
`${this.config.cdnUrl}/${remotePath}` : url;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: t('cos.upload.success'),
|
||||
url: finalUrl,
|
||||
key: remotePath
|
||||
};
|
||||
} else {
|
||||
const errorText = response.text;
|
||||
return {
|
||||
success: false,
|
||||
message: t('cos.error.cloudflare.upload.failed', {
|
||||
status: response.status,
|
||||
error: errorText
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete from Aliyun OSS
|
||||
*/
|
||||
private async deleteFromAliyun(remotePath: string): Promise<void> {
|
||||
const endpoint = `oss-${this.config.region}.aliyuncs.com`;
|
||||
const url = `https://${this.config.bucket}.${endpoint}/${remotePath}`;
|
||||
const date = new Date().toUTCString();
|
||||
const signature = await this.generateAliyunSignature('DELETE', remotePath, date);
|
||||
|
||||
await requestUrl({
|
||||
url: url,
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Date': date,
|
||||
'Authorization': `OSS ${this.config.secretId}:${signature}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete from Tencent COS
|
||||
*/
|
||||
private async deleteFromTencent(remotePath: string): Promise<void> {
|
||||
const url = `https://${this.config.bucket}.cos.${this.config.region}.myqcloud.com/${remotePath}`;
|
||||
const date = new Date().toUTCString();
|
||||
const signature = await this.generateTencentSignature('DELETE', remotePath, date);
|
||||
|
||||
await requestUrl({
|
||||
url: url,
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Date': date,
|
||||
'Authorization': signature
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete from AWS S3
|
||||
*/
|
||||
private async deleteFromAWS(remotePath: string): Promise<void> {
|
||||
const url = `https://${this.config.bucket}.s3.${this.config.region}.amazonaws.com/${remotePath}`;
|
||||
const date = new Date();
|
||||
const signature = await this.generateAWSSignature('DELETE', remotePath, date);
|
||||
|
||||
await requestUrl({
|
||||
url: url,
|
||||
method: 'DELETE',
|
||||
headers: signature.headers
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete from Cloudflare R2
|
||||
*/
|
||||
private async deleteFromCloudflare(remotePath: string): Promise<void> {
|
||||
const url = `${this.config.endpoint}/${remotePath}`;
|
||||
const date = new Date();
|
||||
const signature = await this.generateCloudflareSignature('DELETE', remotePath, date);
|
||||
|
||||
await requestUrl({
|
||||
url: url,
|
||||
method: 'DELETE',
|
||||
headers: signature.headers
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate signature for Aliyun OSS
|
||||
*/
|
||||
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}`;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(this.config.secretKey),
|
||||
{ name: 'HMAC', hash: 'SHA-1' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(stringToSign));
|
||||
return btoa(String.fromCharCode(...new Uint8Array(signature)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
encoder.encode(this.config.secretKey),
|
||||
{ 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)));
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate signature for AWS S3 V4
|
||||
*/
|
||||
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);
|
||||
|
||||
// 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`
|
||||
};
|
||||
|
||||
return { headers };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const isoDate = date.toISOString().replace(/[:\-]|\.\d{3}/g, '');
|
||||
|
||||
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`
|
||||
};
|
||||
|
||||
return { headers };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse image upload path template
|
||||
*/
|
||||
export function parseImagePath(template: string, context: {
|
||||
currentFilePath: string;
|
||||
fileName: string;
|
||||
date?: Date
|
||||
}): string {
|
||||
const date = context.date || new Date();
|
||||
const year = date.getFullYear().toString();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
|
||||
// Extract path components from current file path
|
||||
const pathParts = context.currentFilePath.split('/');
|
||||
const fileName = pathParts.pop()?.replace(/\.[^/.]+$/, '') || ''; // Remove extension
|
||||
const folderPath = pathParts.join('/');
|
||||
const folderName = pathParts[pathParts.length - 1] || '';
|
||||
|
||||
// Use regex to replace placeholders in curly braces
|
||||
let result = template
|
||||
.replace(/\{YYYY\}/g, year)
|
||||
.replace(/\{MM\}/g, month)
|
||||
.replace(/\{DD\}/g, day)
|
||||
.replace(/\{PATH\}/g, folderPath)
|
||||
.replace(/\{FILENAME\}/g, fileName)
|
||||
.replace(/\{FOLDER\}/g, folderName);
|
||||
|
||||
// Ensure the path ends with the image file name
|
||||
if (!result.endsWith('/')) {
|
||||
result += '/';
|
||||
}
|
||||
result += context.fileName;
|
||||
|
||||
// Clean up the path (remove double slashes, etc.)
|
||||
result = result.replace(/\/+/g, '/').replace(/^\//, '');
|
||||
|
||||
return result;
|
||||
}
|
||||
108
i18n-simple.ts
108
i18n-simple.ts
|
|
@ -48,6 +48,45 @@ const translations = {
|
|||
'settings.clear.cache.desc': '清空所有文件状态缓存,下次查看时将重新从GitHub获取',
|
||||
'settings.clear.cache.button': '清空缓存',
|
||||
|
||||
// COS settings
|
||||
'settings.cos.section.title': '图片上传设置',
|
||||
'settings.cos.provider.name': '云存储服务商',
|
||||
'settings.cos.provider.desc': '选择要使用的云对象存储服务商',
|
||||
'settings.cos.provider.aliyun': '阿里云 OSS',
|
||||
'settings.cos.provider.tencent': '腾讯云 COS',
|
||||
'settings.cos.provider.aws': 'AWS S3',
|
||||
'settings.cos.provider.cloudflare': 'Cloudflare R2',
|
||||
'settings.cos.secret.id.name': 'Access Key ID / Secret ID',
|
||||
'settings.cos.secret.id.desc': '云存储服务的访问密钥ID',
|
||||
'settings.cos.secret.id.placeholder': '输入Access Key ID或Secret ID',
|
||||
'settings.cos.secret.key.name': 'Access Key Secret / Secret Key',
|
||||
'settings.cos.secret.key.desc': '云存储服务的访问密钥',
|
||||
'settings.cos.secret.key.placeholder': '输入Access Key Secret或Secret Key',
|
||||
'settings.cos.bucket.name': 'Bucket名称',
|
||||
'settings.cos.bucket.desc': '存储桶名称',
|
||||
'settings.cos.bucket.placeholder': '输入bucket名称',
|
||||
'settings.cos.endpoint.name': 'Endpoint',
|
||||
'settings.cos.endpoint.desc': '服务端点地址(仅Cloudflare R2需要)',
|
||||
'settings.cos.endpoint.placeholder': '例如:https://accountid.r2.cloudflarestorage.com',
|
||||
'settings.cos.region.name': 'Region',
|
||||
'settings.cos.region.desc': '区域设置(所有服务商都需要)',
|
||||
'settings.cos.region.placeholder': '例如:cn-hangzhou、ap-beijing、us-east-1',
|
||||
'settings.cos.cdn.name': 'CDN地址',
|
||||
'settings.cos.cdn.desc': 'CDN域名,用于生成访问链接',
|
||||
'settings.cos.cdn.placeholder': '例如:https://cdn.example.com',
|
||||
'settings.cos.keep.local.name': '保留图片在本地',
|
||||
'settings.cos.keep.local.desc': '上传后是否在本地保留图片副本',
|
||||
'settings.cos.local.path.name': '本地存储路径',
|
||||
'settings.cos.local.path.desc': '本地保存图片的目录(相对于vault根目录)',
|
||||
'settings.cos.local.path.placeholder': '例如:assets',
|
||||
'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.test.name': '测试COS配置',
|
||||
'settings.cos.test.desc': '测试当前COS配置是否可用',
|
||||
'settings.cos.test.button': '测试连接',
|
||||
'settings.cos.test.loading': '测试中...',
|
||||
|
||||
// Notification messages
|
||||
'notice.settings.saved': '设置已保存',
|
||||
'notice.config.required': '请先配置GitHub Token和仓库地址',
|
||||
|
|
@ -143,6 +182,21 @@ const translations = {
|
|||
'path.info.folder': ' 的 {path} 文件夹下',
|
||||
'path.info.root': ' 的根目录下',
|
||||
'path.info.error': '路径格式不正确,请使用: https://github.com/用户名/仓库名/路径 或 用户名/仓库名/路径',
|
||||
|
||||
// COS upload messages
|
||||
'cos.upload.success': '图片上传成功',
|
||||
'cos.upload.failed': '图片上传失败:{error}',
|
||||
'cos.upload.uploading': '正在上传图片...',
|
||||
'cos.test.success': 'COS配置测试成功',
|
||||
'cos.test.failed': 'COS配置测试失败:{error}',
|
||||
'cos.error.unsupported.provider': '不支持的云存储服务商:{provider}',
|
||||
'cos.error.upload.failed': '上传失败:{error}',
|
||||
'cos.error.aliyun.upload.failed': '阿里云OSS上传失败({status}):{error}',
|
||||
'cos.error.tencent.upload.failed': '腾讯云COS上传失败({status}):{error}',
|
||||
'cos.error.aws.upload.failed': 'AWS S3上传失败({status}):{error}',
|
||||
'cos.error.cloudflare.upload.failed': 'Cloudflare R2上传失败({status}):{error}',
|
||||
'cos.error.config.invalid': 'COS配置不完整,请检查配置项',
|
||||
'cos.paste.placeholder': '',
|
||||
},
|
||||
en: {
|
||||
// Settings interface
|
||||
|
|
@ -188,6 +242,45 @@ const translations = {
|
|||
'settings.clear.cache.desc': 'Clear all file status cache, will fetch from GitHub on next view',
|
||||
'settings.clear.cache.button': 'Clear Cache',
|
||||
|
||||
// COS settings
|
||||
'settings.cos.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',
|
||||
'settings.cos.provider.tencent': 'Tencent COS',
|
||||
'settings.cos.provider.aws': 'AWS S3',
|
||||
'settings.cos.provider.cloudflare': 'Cloudflare R2',
|
||||
'settings.cos.secret.id.name': 'Access Key ID / Secret ID',
|
||||
'settings.cos.secret.id.desc': 'Access key ID for cloud storage service',
|
||||
'settings.cos.secret.id.placeholder': 'Enter Access Key ID or Secret ID',
|
||||
'settings.cos.secret.key.name': 'Access Key Secret / Secret Key',
|
||||
'settings.cos.secret.key.desc': 'Access key secret for cloud storage service',
|
||||
'settings.cos.secret.key.placeholder': 'Enter Access Key Secret or Secret Key',
|
||||
'settings.cos.bucket.name': 'Bucket Name',
|
||||
'settings.cos.bucket.desc': 'Storage bucket name',
|
||||
'settings.cos.bucket.placeholder': 'Enter bucket name',
|
||||
'settings.cos.endpoint.name': 'Endpoint',
|
||||
'settings.cos.endpoint.desc': 'Service endpoint address (only required for Cloudflare R2)',
|
||||
'settings.cos.endpoint.placeholder': 'e.g.: https://accountid.r2.cloudflarestorage.com',
|
||||
'settings.cos.region.name': 'Region',
|
||||
'settings.cos.region.desc': 'Region setting (required for all providers)',
|
||||
'settings.cos.region.placeholder': 'e.g.: cn-hangzhou, ap-beijing, us-east-1',
|
||||
'settings.cos.cdn.name': 'CDN URL',
|
||||
'settings.cos.cdn.desc': 'CDN domain for generating access links',
|
||||
'settings.cos.cdn.placeholder': 'e.g.: https://cdn.example.com',
|
||||
'settings.cos.keep.local.name': 'Keep Images Locally',
|
||||
'settings.cos.keep.local.desc': 'Whether to keep a local copy of images after upload',
|
||||
'settings.cos.local.path.name': 'Local Storage Path',
|
||||
'settings.cos.local.path.desc': 'Directory to save images locally (relative to vault root)',
|
||||
'settings.cos.local.path.placeholder': 'e.g.: assets',
|
||||
'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.test.name': 'Test COS Configuration',
|
||||
'settings.cos.test.desc': 'Test if current COS configuration is available',
|
||||
'settings.cos.test.button': 'Test Connection',
|
||||
'settings.cos.test.loading': 'Testing...',
|
||||
|
||||
// Notification messages
|
||||
'notice.settings.saved': 'Settings saved',
|
||||
'notice.config.required': 'Please configure GitHub Token and repository address first',
|
||||
|
|
@ -283,6 +376,21 @@ const translations = {
|
|||
'path.info.folder': ' under {path} folder',
|
||||
'path.info.root': ' under root directory',
|
||||
'path.info.error': 'Path format is incorrect, please use: https://github.com/username/repo/path or username/repo/path',
|
||||
|
||||
// COS upload messages
|
||||
'cos.upload.success': 'Image uploaded successfully',
|
||||
'cos.upload.failed': 'Image upload failed: {error}',
|
||||
'cos.upload.uploading': 'Uploading image...',
|
||||
'cos.test.success': 'COS configuration test successful',
|
||||
'cos.test.failed': 'COS configuration test failed: {error}',
|
||||
'cos.error.unsupported.provider': 'Unsupported cloud storage provider: {provider}',
|
||||
'cos.error.upload.failed': 'Upload failed: {error}',
|
||||
'cos.error.aliyun.upload.failed': 'Aliyun OSS upload failed ({status}): {error}',
|
||||
'cos.error.tencent.upload.failed': 'Tencent COS upload failed ({status}): {error}',
|
||||
'cos.error.aws.upload.failed': 'AWS S3 upload failed ({status}): {error}',
|
||||
'cos.error.cloudflare.upload.failed': 'Cloudflare R2 upload failed ({status}): {error}',
|
||||
'cos.error.config.invalid': 'COS configuration is incomplete, please check settings',
|
||||
'cos.paste.placeholder': '',
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
390
main.ts
390
main.ts
|
|
@ -1,8 +1,9 @@
|
|||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, Menu, TFolder } from 'obsidian';
|
||||
import { GitSyncSettings, DEFAULT_SETTINGS, SyncResult } from './types';
|
||||
import { GitSyncSettings, DEFAULT_SETTINGS, SyncResult, CosConfig, ImagePasteContext } from './types';
|
||||
import { GitHubService } from './github-service';
|
||||
import { setLanguage, t, getSupportedLanguages, getActualLanguage } from './i18n-simple';
|
||||
import { FileCacheService } from './file-cache';
|
||||
import { CosService, parseImagePath } from './cos-service';
|
||||
|
||||
export default class GitSyncPlugin extends Plugin {
|
||||
settings: GitSyncSettings;
|
||||
|
|
@ -58,6 +59,9 @@ export default class GitSyncPlugin extends Plugin {
|
|||
})
|
||||
);
|
||||
|
||||
// Listen for paste events to handle image uploads
|
||||
this.registerDomEvent(document, 'paste', this.handlePasteEvent.bind(this));
|
||||
|
||||
// Add sync button to note editing interface
|
||||
this.addCommand({
|
||||
id: 'show-sync-menu',
|
||||
|
|
@ -354,8 +358,25 @@ export default class GitSyncPlugin extends Plugin {
|
|||
const allFiles = this.app.vault.getFiles();
|
||||
console.log('All files in vault:', allFiles.map(f => f.path));
|
||||
|
||||
// Filter out files in .obsidian folder
|
||||
const filteredFiles = allFiles.filter(file => !file.path.startsWith('.obsidian'));
|
||||
// Filter out files in .obsidian folder and local image path (if configured)
|
||||
const filteredFiles = allFiles.filter(file => {
|
||||
// Always exclude .obsidian folder
|
||||
if (file.path.startsWith('.obsidian')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude local image path if keepLocalImages is enabled and localImagePath is set
|
||||
if (this.settings.keepLocalImages && this.settings.localImagePath) {
|
||||
const normalizedImagePath = this.settings.localImagePath.replace(/^\/+|\/+$/g, ''); // Remove leading/trailing slashes
|
||||
if (normalizedImagePath && file.path.startsWith(normalizedImagePath + '/')) {
|
||||
console.log(`Excluding file from sync (in image directory): ${file.path}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
console.log('Filtered files:', filteredFiles.map(f => f.path));
|
||||
|
||||
return filteredFiles;
|
||||
|
|
@ -633,6 +654,174 @@ export default class GitSyncPlugin extends Plugin {
|
|||
return date.toLocaleDateString('zh-CN');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle paste events for image upload
|
||||
*/
|
||||
async handlePasteEvent(event: ClipboardEvent): Promise<void> {
|
||||
// Only handle paste events in markdown editor
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (!activeView || !this.currentFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clipboard contains files
|
||||
const clipboardData = event.clipboardData;
|
||||
if (!clipboardData || !clipboardData.files || clipboardData.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if COS is configured
|
||||
if (!this.isCosConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each file in clipboard
|
||||
for (let i = 0; i < clipboardData.files.length; i++) {
|
||||
const file = clipboardData.files[i];
|
||||
|
||||
// Only handle image files
|
||||
if (!file.type.startsWith('image/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prevent default paste behavior for images
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
await this.handleImageUpload(file, activeView.editor);
|
||||
} catch (error) {
|
||||
console.error('Error handling image upload:', error);
|
||||
new Notice(t('cos.upload.failed', { error: error.message }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if COS is properly configured
|
||||
*/
|
||||
private isCosConfigured(): boolean {
|
||||
const requiredFields = [
|
||||
this.settings.cosSecretId,
|
||||
this.settings.cosSecretKey,
|
||||
this.settings.cosBucket,
|
||||
this.settings.cosRegion // 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
|
||||
}
|
||||
|
||||
return requiredFields.every(field => field && field.trim().length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle single image upload
|
||||
*/
|
||||
private async handleImageUpload(file: File, editor: Editor): Promise<void> {
|
||||
// Generate unique filename
|
||||
const timestamp = Date.now();
|
||||
const extension = file.name.split('.').pop() || 'png';
|
||||
const fileName = `image-${timestamp}.${extension}`;
|
||||
|
||||
// Parse upload path
|
||||
const remotePath = parseImagePath(this.settings.imageUploadPath, {
|
||||
currentFilePath: this.currentFile!.path,
|
||||
fileName: fileName
|
||||
});
|
||||
|
||||
// Create placeholder text
|
||||
const placeholder = t('cos.paste.placeholder', { filename: fileName });
|
||||
const cursorPos = editor.getCursor();
|
||||
|
||||
// Insert placeholder at cursor position
|
||||
editor.replaceRange(placeholder, cursorPos);
|
||||
|
||||
// Show upload notification
|
||||
const uploadNotice = new Notice(t('cos.upload.uploading'), 0);
|
||||
|
||||
try {
|
||||
// Create COS configuration
|
||||
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
|
||||
};
|
||||
|
||||
// Upload to COS
|
||||
const cosService = new CosService(cosConfig);
|
||||
const uploadResult = await cosService.uploadFile(file, remotePath);
|
||||
|
||||
if (uploadResult.success && uploadResult.url) {
|
||||
// Replace placeholder with actual image link
|
||||
const imageMarkdown = ``;
|
||||
const currentContent = editor.getValue();
|
||||
const updatedContent = currentContent.replace(placeholder, imageMarkdown);
|
||||
editor.setValue(updatedContent);
|
||||
|
||||
// Save local copy if enabled
|
||||
if (this.settings.keepLocalImages && this.settings.localImagePath) {
|
||||
await this.saveLocalImageCopy(file, fileName);
|
||||
}
|
||||
|
||||
// Hide upload notification and show success
|
||||
uploadNotice.hide();
|
||||
new Notice(t('cos.upload.success'));
|
||||
|
||||
console.log('Image uploaded successfully:', uploadResult.url);
|
||||
} else {
|
||||
throw new Error(uploadResult.message || 'Upload failed');
|
||||
}
|
||||
} catch (error) {
|
||||
// Remove placeholder on error
|
||||
const currentContent = editor.getValue();
|
||||
const updatedContent = currentContent.replace(placeholder, '');
|
||||
editor.setValue(updatedContent);
|
||||
|
||||
// Hide upload notification and show error
|
||||
uploadNotice.hide();
|
||||
new Notice(t('cos.upload.failed', { error: error.message }));
|
||||
|
||||
console.error('Image upload failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save image copy locally if enabled
|
||||
*/
|
||||
private async saveLocalImageCopy(file: File, fileName: string): Promise<void> {
|
||||
try {
|
||||
const localPath = parseImagePath(this.settings.localImagePath, {
|
||||
currentFilePath: this.currentFile!.path,
|
||||
fileName: fileName
|
||||
});
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
const dirPath = localPath.substring(0, localPath.lastIndexOf('/'));
|
||||
if (dirPath) {
|
||||
try {
|
||||
await this.app.vault.createFolder(dirPath);
|
||||
} catch (error) {
|
||||
// Folder might already exist, ignore error
|
||||
}
|
||||
}
|
||||
|
||||
// Convert File to ArrayBuffer
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
// Save file to vault
|
||||
await this.app.vault.createBinary(localPath, arrayBuffer);
|
||||
|
||||
console.log('Local image copy saved:', localPath);
|
||||
} catch (error) {
|
||||
console.warn('Failed to save local image copy:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GitSyncSettingTab extends PluginSettingTab {
|
||||
|
|
@ -732,6 +921,201 @@ class GitSyncSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// COS settings section
|
||||
containerEl.createEl('h2', {
|
||||
text: t('settings.cos.section.title'),
|
||||
cls: 'git-sync-section-title'
|
||||
});
|
||||
|
||||
// Cloud storage provider
|
||||
new Setting(containerEl)
|
||||
.setName(t('settings.cos.provider.name'))
|
||||
.setDesc(t('settings.cos.provider.desc'))
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption('aliyun', t('settings.cos.provider.aliyun'));
|
||||
dropdown.addOption('tencent', t('settings.cos.provider.tencent'));
|
||||
dropdown.addOption('aws', t('settings.cos.provider.aws'));
|
||||
dropdown.addOption('cloudflare', t('settings.cos.provider.cloudflare'));
|
||||
|
||||
dropdown.setValue(this.plugin.settings.cosProvider);
|
||||
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
|
||||
this.display();
|
||||
});
|
||||
});
|
||||
|
||||
// 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)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cosSecretId = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Secret Key
|
||||
new Setting(containerEl)
|
||||
.setName(t('settings.cos.secret.key.name') + ' *')
|
||||
.setDesc(t('settings.cos.secret.key.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t('settings.cos.secret.key.placeholder'))
|
||||
.setValue(this.plugin.settings.cosSecretKey)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cosSecretKey = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Bucket
|
||||
new Setting(containerEl)
|
||||
.setName(t('settings.cos.bucket.name') + ' *')
|
||||
.setDesc(t('settings.cos.bucket.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t('settings.cos.bucket.placeholder'))
|
||||
.setValue(this.plugin.settings.cosBucket)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cosBucket = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Region (required for all providers)
|
||||
new Setting(containerEl)
|
||||
.setName(t('settings.cos.region.name') + ' *')
|
||||
.setDesc(t('settings.cos.region.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t('settings.cos.region.placeholder'))
|
||||
.setValue(this.plugin.settings.cosRegion)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cosRegion = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Endpoint (only for Cloudflare R2)
|
||||
if (this.plugin.settings.cosProvider === 'cloudflare') {
|
||||
new Setting(containerEl)
|
||||
.setName(t('settings.cos.endpoint.name') + ' *')
|
||||
.setDesc(t('settings.cos.endpoint.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t('settings.cos.endpoint.placeholder'))
|
||||
.setValue(this.plugin.settings.cosEndpoint)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cosEndpoint = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
// CDN URL
|
||||
new Setting(containerEl)
|
||||
.setName(t('settings.cos.cdn.name'))
|
||||
.setDesc(t('settings.cos.cdn.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t('settings.cos.cdn.placeholder'))
|
||||
.setValue(this.plugin.settings.cosCdnUrl)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.cosCdnUrl = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Keep images locally
|
||||
new Setting(containerEl)
|
||||
.setName(t('settings.cos.keep.local.name'))
|
||||
.setDesc(t('settings.cos.keep.local.desc'))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.keepLocalImages)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.keepLocalImages = value;
|
||||
await this.plugin.saveSettings();
|
||||
// Re-render to show/hide local path setting
|
||||
this.display();
|
||||
}));
|
||||
|
||||
// Local storage path (only when keepLocalImages is true)
|
||||
if (this.plugin.settings.keepLocalImages) {
|
||||
new Setting(containerEl)
|
||||
.setName(t('settings.cos.local.path.name'))
|
||||
.setDesc(t('settings.cos.local.path.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t('settings.cos.local.path.placeholder'))
|
||||
.setValue(this.plugin.settings.localImagePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.localImagePath = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
|
||||
// Upload path template
|
||||
new Setting(containerEl)
|
||||
.setName(t('settings.cos.upload.path.name'))
|
||||
.setDesc(t('settings.cos.upload.path.desc'))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t('settings.cos.upload.path.placeholder'))
|
||||
.setValue(this.plugin.settings.imageUploadPath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.imageUploadPath = value;
|
||||
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'
|
||||
|
|
|
|||
62
types.ts
62
types.ts
|
|
@ -4,6 +4,22 @@ export interface GitSyncSettings {
|
|||
lastSyncTime: number;
|
||||
showRibbonIcon: boolean; // Whether to show sidebar button
|
||||
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)
|
||||
|
||||
// Local image storage settings
|
||||
keepLocalImages: boolean; // Whether to keep images locally after upload
|
||||
localImagePath: string; // Local path for storing images (relative to vault root)
|
||||
|
||||
// Image upload path template
|
||||
imageUploadPath: string; // Path template with placeholders: PATH, FILENAME, FOLDER, YYYY, MM, DD
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: GitSyncSettings = {
|
||||
|
|
@ -11,7 +27,23 @@ export const DEFAULT_SETTINGS: GitSyncSettings = {
|
|||
repositoryUrl: '',
|
||||
lastSyncTime: 0,
|
||||
showRibbonIcon: true,
|
||||
language: 'auto' // Follow Obsidian language setting
|
||||
language: 'auto', // Follow Obsidian language setting
|
||||
|
||||
// COS default settings
|
||||
cosProvider: 'aliyun',
|
||||
cosSecretId: '',
|
||||
cosSecretKey: '',
|
||||
cosBucket: '',
|
||||
cosEndpoint: '',
|
||||
cosCdnUrl: '',
|
||||
cosRegion: '',
|
||||
|
||||
// Local image storage defaults
|
||||
keepLocalImages: false,
|
||||
localImagePath: 'assets',
|
||||
|
||||
// Image upload path template default
|
||||
imageUploadPath: 'images/{YYYY}/{MM}/{DD}'
|
||||
};
|
||||
|
||||
export interface GitHubFile {
|
||||
|
|
@ -55,4 +87,32 @@ export interface FileCacheManager {
|
|||
clearCache(): void;
|
||||
isCacheValid(cache: FileCache, maxAge?: number): boolean;
|
||||
getAllCachedFiles(): FileCache[];
|
||||
}
|
||||
|
||||
// COS upload result
|
||||
export interface CosUploadResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
url?: string; // Final accessible URL
|
||||
key?: string; // Object key in COS
|
||||
}
|
||||
|
||||
// COS configuration for different providers
|
||||
export interface CosConfig {
|
||||
provider: 'aliyun' | 'tencent' | 'aws' | 'cloudflare';
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
bucket: string;
|
||||
endpoint: string;
|
||||
cdnUrl: string;
|
||||
region: string;
|
||||
}
|
||||
|
||||
// Image paste context
|
||||
export interface ImagePasteContext {
|
||||
file: File;
|
||||
fileName: string;
|
||||
currentFilePath: string; // Current markdown file path
|
||||
localPath?: string; // Local storage path (if keepLocalImages is true)
|
||||
remotePath: string; // Remote COS path
|
||||
}
|
||||
Loading…
Reference in a new issue