Files
cherry-studio/src/main/services/RemoteStorage.ts
T
George·Dong 3f5901766d feat: Add S3 Backup (#6802)
* chore: import opendal

* feat: 添加S3备份支持及相关设置界面

- 在IpcChannel中新增S3备份相关IPC事件,支持备份、恢复、
  列表、删除文件及连接检测
- 在ipc主进程注册对应的S3备份处理函数,集成backupManager
- 新增S3设置页面,支持配置Endpoint、Region、Bucket、AccessKey等
  参数,并提供同步和备份策略的UI控制
- 删除未使用的RemoteStorage.ts,简化代码库

提升备份功能的灵活性,支持S3作为远程存储目标

* feat(S3 Backup): 完善S3备份功能

- 支持自动备份
- 优化设置前端
- 优化备份恢复代码

* feat(i18n): add S3 storage translations

* feat(settings): 优化数据设置页面和S3设置页面UI

* feat(settings): optimize S3 settings state structure and update usage

* refactor: simplify S3 backup and restore modal logic

* feat(s3 backup): improve S3 settings defaults and modal props

* fix(i18n): optimize S3 access key translations

* feat(backup): optimize logging and progress reporting

* fix(settings): set S3 maxBackups as unlimited by default

* chore(package): restore opendal dependency in package.json

---------

Co-authored-by: suyao <sy20010504@gmail.com>
2025-06-28 22:19:37 +08:00

84 lines
2.7 KiB
TypeScript

import Logger from 'electron-log'
import type { Operator as OperatorType } from 'opendal'
const { Operator } = require('opendal')
export default class S3Storage {
public instance: OperatorType | undefined
/**
*
* @param scheme is the scheme for opendal services. Available value includes "azblob", "azdls", "cos", "gcs", "obs", "oss", "s3", "webdav", "webhdfs", "aliyun-drive", "alluxio", "azfile", "dropbox", "gdrive", "onedrive", "postgresql", "mysql", "redis", "swift", "mongodb", "alluxio", "b2", "seafile", "upyun", "koofr", "yandex-disk"
* @param options is the options for given opendal services. Valid options depend on the scheme. Checkout https://docs.rs/opendal/latest/opendal/services/index.html for all valid options.
*
* For example, use minio as remote storage:
*
* ```typescript
* const storage = new S3Storage('s3', {
* endpoint: 'http://localhost:9000',
* region: 'us-east-1',
* bucket: 'testbucket',
* access_key_id: 'user',
* secret_access_key: 'password',
* root: '/path/to/basepath',
* })
* ```
*/
constructor(scheme: string, options?: Record<string, string> | undefined | null) {
this.instance = new Operator(scheme, options)
this.putFileContents = this.putFileContents.bind(this)
this.getFileContents = this.getFileContents.bind(this)
}
public putFileContents = async (filename: string, data: string | Buffer) => {
if (!this.instance) {
return new Error('RemoteStorage client not initialized')
}
try {
return await this.instance.write(filename, data)
} catch (error) {
Logger.error('[RemoteStorage] Error putting file contents:', error)
throw error
}
}
public getFileContents = async (filename: string) => {
if (!this.instance) {
throw new Error('RemoteStorage client not initialized')
}
try {
return await this.instance.read(filename)
} catch (error) {
Logger.error('[RemoteStorage] Error getting file contents:', error)
throw error
}
}
public deleteFile = async (filename: string) => {
if (!this.instance) {
throw new Error('RemoteStorage client not initialized')
}
try {
return await this.instance.delete(filename)
} catch (error) {
Logger.error('[RemoteStorage] Error deleting file:', error)
throw error
}
}
public checkConnection = async () => {
if (!this.instance) {
throw new Error('RemoteStorage client not initialized')
}
try {
// 检查根目录是否可访问
return await this.instance.stat('/')
} catch (error) {
Logger.error('[RemoteStorage] Error checking connection:', error)
throw error
}
}
}