mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 07:19:02 +08:00
fix: backup and restore
This commit is contained in:
parent
d37231dafe
commit
3e8a00f3a5
@ -4,6 +4,10 @@ import { app } from 'electron'
|
|||||||
import Store from 'electron-store'
|
import Store from 'electron-store'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
isDev && app.setPath('userData', app.getPath('userData') + 'Dev')
|
||||||
|
|
||||||
const getDataPath = () => {
|
const getDataPath = () => {
|
||||||
const dataPath = path.join(app.getPath('userData'), 'Data')
|
const dataPath = path.join(app.getPath('userData'), 'Data')
|
||||||
if (!fs.existsSync(dataPath)) {
|
if (!fs.existsSync(dataPath)) {
|
||||||
|
|||||||
@ -56,10 +56,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
|
|
||||||
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options))
|
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options))
|
||||||
ipcMain.handle('file:upload', async (_, file: FileType) => await fileManager.uploadFile(file))
|
ipcMain.handle('file:upload', async (_, file: FileType) => await fileManager.uploadFile(file))
|
||||||
|
ipcMain.handle('file:clear', async () => await fileManager.clear())
|
||||||
ipcMain.handle('file:delete', async (_, fileId: string) => {
|
ipcMain.handle('file:delete', async (_, fileId: string) => {
|
||||||
await fileManager.deleteFile(fileId)
|
await fileManager.deleteFile(fileId)
|
||||||
return { success: true }
|
return { success: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('minapp', (_, args) => {
|
ipcMain.handle('minapp', (_, args) => {
|
||||||
createMinappWindow({
|
createMinappWindow({
|
||||||
url: args.url,
|
url: args.url,
|
||||||
|
|||||||
@ -134,6 +134,11 @@ class File {
|
|||||||
async deleteFile(id: string): Promise<void> {
|
async deleteFile(id: string): Promise<void> {
|
||||||
await fs.promises.unlink(path.join(this.storageDir, id))
|
await fs.promises.unlink(path.join(this.storageDir, id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
await fs.promises.rmdir(this.storageDir, { recursive: true })
|
||||||
|
await this.initStorageDir()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default File
|
export default File
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||||
import logger from 'electron-log'
|
import logger from 'electron-log'
|
||||||
import { writeFile } from 'fs'
|
import { writeFileSync } from 'fs'
|
||||||
import { readFile } from 'fs/promises'
|
import { readFile } from 'fs/promises'
|
||||||
|
|
||||||
import { FileTypes } from '../../renderer/src/types'
|
import { FileTypes } from '../../renderer/src/types'
|
||||||
@ -19,11 +19,7 @@ export async function saveFile(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.canceled && result.filePath) {
|
if (!result.canceled && result.filePath) {
|
||||||
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
|
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
|
||||||
if (err) {
|
|
||||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||||
|
|||||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@ -25,6 +25,7 @@ declare global {
|
|||||||
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||||
upload: (file: FileType) => Promise<FileType>
|
upload: (file: FileType) => Promise<FileType>
|
||||||
delete: (fileId: string) => Promise<{ success: boolean }>
|
delete: (fileId: string) => Promise<{ success: boolean }>
|
||||||
|
clear: () => Promise<void>
|
||||||
}
|
}
|
||||||
image: {
|
image: {
|
||||||
base64: (filePath: string) => Promise<{ mime: string; base64: string; data: string }>
|
base64: (filePath: string) => Promise<{ mime: string; base64: string; data: string }>
|
||||||
|
|||||||
@ -12,14 +12,15 @@ const api = {
|
|||||||
openFile: (options?: { decompress: boolean }) => ipcRenderer.invoke('open-file', options),
|
openFile: (options?: { decompress: boolean }) => ipcRenderer.invoke('open-file', options),
|
||||||
reload: () => ipcRenderer.invoke('reload'),
|
reload: () => ipcRenderer.invoke('reload'),
|
||||||
saveFile: (path: string, content: string, options?: { compress: boolean }) => {
|
saveFile: (path: string, content: string, options?: { compress: boolean }) => {
|
||||||
ipcRenderer.invoke('save-file', path, content, options)
|
return ipcRenderer.invoke('save-file', path, content, options)
|
||||||
},
|
},
|
||||||
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||||
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text),
|
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text),
|
||||||
file: {
|
file: {
|
||||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||||
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
|
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
|
||||||
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId)
|
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId),
|
||||||
|
clear: () => ipcRenderer.invoke('file:clear')
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
base64: (filePath: string) => ipcRenderer.invoke('image:base64', filePath)
|
base64: (filePath: string) => ipcRenderer.invoke('image:base64', filePath)
|
||||||
|
|||||||
@ -4,6 +4,11 @@ import localforage from 'localforage'
|
|||||||
|
|
||||||
export async function populateTopics(trans: Transaction) {
|
export async function populateTopics(trans: Transaction) {
|
||||||
const indexedKeys = await localforage.keys()
|
const indexedKeys = await localforage.keys()
|
||||||
|
|
||||||
|
if (indexedKeys.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for (const key of indexedKeys) {
|
for (const key of indexedKeys) {
|
||||||
const value: any = await localforage.getItem(key)
|
const value: any = await localforage.getItem(key)
|
||||||
if (key.startsWith('topic:')) {
|
if (key.startsWith('topic:')) {
|
||||||
|
|||||||
@ -55,6 +55,7 @@ const resources = {
|
|||||||
'chat.completion.paused': 'Chat completion paused',
|
'chat.completion.paused': 'Chat completion paused',
|
||||||
'switch.disabled': 'Switching is disabled while the assistant is generating',
|
'switch.disabled': 'Switching is disabled while the assistant is generating',
|
||||||
'restore.success': 'Restored successfully',
|
'restore.success': 'Restored successfully',
|
||||||
|
'backup.success': 'Backup successful',
|
||||||
'reset.confirm.content': 'Are you sure you want to clear all data?',
|
'reset.confirm.content': 'Are you sure you want to clear all data?',
|
||||||
'reset.double.confirm.title': 'DATA LOST !!!',
|
'reset.double.confirm.title': 'DATA LOST !!!',
|
||||||
'reset.double.confirm.content': 'All data will be lost, do you want to continue?',
|
'reset.double.confirm.content': 'All data will be lost, do you want to continue?',
|
||||||
@ -321,6 +322,7 @@ const resources = {
|
|||||||
'chat.completion.paused': '会话已停止',
|
'chat.completion.paused': '会话已停止',
|
||||||
'switch.disabled': '模型回复完成后才能切换',
|
'switch.disabled': '模型回复完成后才能切换',
|
||||||
'restore.success': '恢复成功',
|
'restore.success': '恢复成功',
|
||||||
|
'backup.success': '备份成功',
|
||||||
'reset.confirm.content': '确定要重置所有数据吗?',
|
'reset.confirm.content': '确定要重置所有数据吗?',
|
||||||
'reset.double.confirm.title': '数据丢失!!!',
|
'reset.double.confirm.title': '数据丢失!!!',
|
||||||
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?',
|
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?',
|
||||||
|
|||||||
@ -1,31 +1,26 @@
|
|||||||
|
import db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
|
|
||||||
export async function backup() {
|
export async function backup() {
|
||||||
const indexedKeys = await localforage.keys()
|
const version = 2
|
||||||
const version = 1
|
|
||||||
const time = new Date().getTime()
|
const time = new Date().getTime()
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
time,
|
time,
|
||||||
version,
|
version,
|
||||||
localStorage,
|
localStorage,
|
||||||
indexedDB: [] as { key: string; value: any }[]
|
indexedDB: await backupDatabase()
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of indexedKeys) {
|
|
||||||
data.indexedDB.push({
|
|
||||||
key,
|
|
||||||
value: await localforage.getItem(key)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filename = `cherry-studio.${dayjs().format('YYYYMMDD')}.bak`
|
const filename = `cherry-studio.${dayjs().format('YYYYMMDD')}.bak`
|
||||||
const fileContnet = JSON.stringify(data)
|
const fileContnet = JSON.stringify(data)
|
||||||
const file = await window.api.compress(fileContnet)
|
const file = await window.api.compress(fileContnet)
|
||||||
|
|
||||||
window.api.saveFile(filename, file)
|
await window.api.saveFile(filename, file)
|
||||||
|
|
||||||
|
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function restore() {
|
export async function restore() {
|
||||||
@ -37,17 +32,19 @@ export async function restore() {
|
|||||||
const data = JSON.parse(content)
|
const data = JSON.parse(content)
|
||||||
|
|
||||||
if (data.version === 1) {
|
if (data.version === 1) {
|
||||||
localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
|
window.modal.confirm({ content: 'Please use a version less than v0.7.0 for recovery.' })
|
||||||
|
return
|
||||||
for (const { key, value } of data.indexedDB) {
|
|
||||||
await localforage.setItem(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' })
|
|
||||||
setTimeout(() => window.api.reload(), 1500)
|
|
||||||
} else {
|
|
||||||
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.version === 2) {
|
||||||
|
localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
|
||||||
|
await restoreDatabase(data.indexedDB)
|
||||||
|
window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' })
|
||||||
|
setTimeout(() => window.api.reload(), 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
|
||||||
@ -59,6 +56,7 @@ export async function reset() {
|
|||||||
window.modal.confirm({
|
window.modal.confirm({
|
||||||
title: i18n.t('common.warning'),
|
title: i18n.t('common.warning'),
|
||||||
content: i18n.t('message.reset.confirm.content'),
|
content: i18n.t('message.reset.confirm.content'),
|
||||||
|
centered: true,
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
window.modal.confirm({
|
window.modal.confirm({
|
||||||
title: i18n.t('message.reset.double.confirm.title'),
|
title: i18n.t('message.reset.double.confirm.title'),
|
||||||
@ -67,9 +65,43 @@ export async function reset() {
|
|||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
await localStorage.clear()
|
await localStorage.clear()
|
||||||
await localforage.clear()
|
await localforage.clear()
|
||||||
|
await clearDatabase()
|
||||||
|
await window.api.file.clear()
|
||||||
window.api.reload()
|
window.api.reload()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************************************* Backup Utils ************************************** */
|
||||||
|
|
||||||
|
async function backupDatabase() {
|
||||||
|
const tables = db.tables
|
||||||
|
const backup = {}
|
||||||
|
|
||||||
|
for (const table of tables) {
|
||||||
|
backup[table.name] = await table.toArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
return backup
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreDatabase(backup: Record<string, any>) {
|
||||||
|
await db.transaction('rw', db.tables, async () => {
|
||||||
|
for (const tableName in backup) {
|
||||||
|
await db.table(tableName).clear()
|
||||||
|
await db.table(tableName).bulkAdd(backup[tableName])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearDatabase() {
|
||||||
|
const storeNames = await db.tables.map((table) => table.name)
|
||||||
|
|
||||||
|
await db.transaction('rw', db.tables, async () => {
|
||||||
|
for (const storeName of storeNames) {
|
||||||
|
await db[storeName].clear()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user