refactor: remove sqlite3 use dexie

This commit is contained in:
kangfenmao 2024-09-14 15:25:56 +08:00
parent 0ddef31ed8
commit 9ae9fdf392
17 changed files with 158 additions and 1150 deletions

View File

@ -34,10 +34,7 @@
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3",
"sequelize": "^6.37.3",
"sqlite3": "^5.1.7",
"umzug": "^3.8.1"
"electron-window-state": "^5.0.3"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.24.3",
@ -57,6 +54,8 @@
"axios": "^1.7.3",
"browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7",
"dotenv-cli": "^7.4.2",
"electron": "^28.3.3",
"electron-builder": "^24.9.1",

View File

@ -1,35 +0,0 @@
import Logger from 'electron-log'
import path from 'path'
import { Sequelize } from 'sequelize'
import { SequelizeStorage, Umzug } from 'umzug'
import { DATA_PATH } from '../config'
import migrations from './migrations'
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: path.join(DATA_PATH, 'data.db'),
logging: false
})
const umzug = new Umzug({
migrations,
context: sequelize.getQueryInterface(),
storage: new SequelizeStorage({ sequelize, modelName: 'Migration', tableName: 'migrations' }),
logger: Logger
})
export async function initDatabase() {
try {
await sequelize.authenticate()
Logger.log('Database connection has been established successfully.')
// Run migrations
await umzug.up()
Logger.log('Migrations have been executed successfully.')
} catch (error) {
Logger.error('Migrations failed to execute:', error)
}
}
export default sequelize

View File

@ -1,50 +0,0 @@
import { DataTypes } from 'sequelize'
export default [
{
name: '20240912072241-create-files-table',
async up({ context }) {
await context.createTable('files', {
id: {
type: DataTypes.TEXT,
primaryKey: true
},
name: {
type: DataTypes.TEXT,
allowNull: false
},
file_name: {
type: DataTypes.TEXT,
allowNull: false
},
path: {
type: DataTypes.TEXT,
allowNull: false
},
size: {
type: DataTypes.INTEGER,
allowNull: false
},
ext: {
type: DataTypes.TEXT,
allowNull: false
},
type: {
type: DataTypes.TEXT,
allowNull: false
},
created_at: {
type: DataTypes.TEXT,
allowNull: false
},
count: {
type: DataTypes.INTEGER,
defaultValue: 1
}
})
},
async down({ context }) {
await context.dropTable('files')
}
}
]

View File

@ -1,41 +0,0 @@
import { FileMetadata, FileType } from '@types'
import { DataTypes, Model } from 'sequelize'
import sequelize from '..'
class FileModel extends Model<FileMetadata> implements FileMetadata {
public id!: string
public name!: string
public file_name!: string
public path!: string
public size!: number
public ext!: string
public type!: FileType
public created_at!: Date
public count!: number
}
FileModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true
},
name: DataTypes.STRING,
file_name: DataTypes.STRING,
path: DataTypes.STRING,
size: DataTypes.INTEGER,
ext: DataTypes.STRING,
type: DataTypes.STRING,
created_at: DataTypes.DATE,
count: DataTypes.INTEGER
},
{
sequelize,
modelName: 'File',
tableName: 'files',
timestamps: false
}
)
export default FileModel

View File

@ -1,7 +1,6 @@
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow } from 'electron'
import { initDatabase } from './database'
import { registerIpc } from './ipc'
import { updateUserDataPath } from './utils/upgrade'
import { createMainWindow } from './window'
@ -11,7 +10,6 @@ import { createMainWindow } from './window'
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
await updateUserDataPath()
await initDatabase()
// Set app user model id for windows
electronApp.setAppUserModelId('com.kangfenmao.CherryStudio')

View File

@ -60,13 +60,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
await fileManager.deleteFile(fileId)
return { success: true }
})
ipcMain.handle('file:batchUpload', async (_, files: FileMetadata[]) => await fileManager.batchUploadFiles(files))
ipcMain.handle('file:batchDelete', async (_, fileIds: string[]) => {
await fileManager.batchDeleteFiles(fileIds)
return { success: true }
})
ipcMain.handle('file:all', () => fileManager.getAllFiles())
ipcMain.handle('minapp', (_, args) => {
createMinappWindow({
url: args.url,

View File

@ -1,5 +1,3 @@
/* eslint-disable react/no-is-mounted */
import FileModel from '@main/database/models/FileModel'
import { getFileType } from '@main/utils/file'
import { FileMetadata } from '@types'
import * as crypto from 'crypto'
@ -50,7 +48,17 @@ class File {
if (originalHash === storedHash) {
const ext = path.extname(file)
const id = path.basename(file, ext)
return this.getFile(id)
return {
id,
origin_name: file,
name: file + ext,
path: storedFilePath,
created_at: storedStats.birthtime,
size: storedStats.size,
ext,
type: getFileType(ext),
count: 2
}
}
}
}
@ -78,8 +86,8 @@ class File {
return {
id: uuidv4(),
origin_name: path.basename(filePath),
name: path.basename(filePath),
file_name: path.basename(filePath),
path: filePath,
created_at: stats.birthtime,
size: stats.size,
@ -96,16 +104,12 @@ class File {
const duplicateFile = await this.findDuplicateFile(file.path)
if (duplicateFile) {
// Increment the count for the duplicate file
await FileModel.increment('count', { where: { id: duplicateFile.id } })
// Fetch the updated file metadata
return (await this.getFile(duplicateFile.id))!
return duplicateFile
}
const uuid = uuidv4()
const name = path.basename(file.path)
const ext = path.extname(name)
const origin_name = path.basename(file.path)
const ext = path.extname(origin_name)
const destPath = path.join(this.storageDir, uuid + ext)
await fs.promises.copyFile(file.path, destPath)
@ -114,8 +118,8 @@ class File {
const fileMetadata: FileMetadata = {
id: uuid,
name,
file_name: uuid + ext,
origin_name,
name: uuid + ext,
path: destPath,
created_at: stats.birthtime,
size: stats.size,
@ -124,43 +128,11 @@ class File {
count: 1
}
await FileModel.create(fileMetadata)
return fileMetadata
}
async deleteFile(fileId: string): Promise<void> {
const fileMetadata = await this.getFile(fileId)
if (fileMetadata) {
if (fileMetadata.count > 1) {
// Decrement the count if there are multiple references
await FileModel.decrement('count', { where: { id: fileId } })
} else {
// Delete the file and database entry if this is the last reference
await fs.promises.unlink(fileMetadata.path)
await FileModel.destroy({ where: { id: fileId } })
}
}
}
async batchUploadFiles(files: FileMetadata[]): Promise<FileMetadata[]> {
const uploadPromises = files.map((file) => this.uploadFile(file))
return Promise.all(uploadPromises)
}
async batchDeleteFiles(fileIds: string[]): Promise<void> {
const deletePromises = fileIds.map((fileId) => this.deleteFile(fileId))
await Promise.all(deletePromises)
}
async getFile(id: string): Promise<FileMetadata | null> {
const file = await FileModel.findByPk(id)
return file ? (file.toJSON() as FileMetadata) : null
}
async getAllFiles(): Promise<FileMetadata[]> {
const files = await FileModel.findAll()
return files.map((file) => file.toJSON() as FileMetadata)
async deleteFile(id: string): Promise<void> {
await fs.promises.unlink(path.join(this.storageDir, id))
}
}

View File

@ -25,9 +25,6 @@ declare global {
select: (options?: OpenDialogOptions) => Promise<FileMetadata[] | null>
upload: (file: FileMetadata) => Promise<FileMetadata>
delete: (fileId: string) => Promise<{ success: boolean }>
batchUpload: (files: FileMetadata[]) => Promise<FileMetadata[]>
batchDelete: (fileIds: string[]) => Promise<{ success: boolean }>
all: () => Promise<FileMetadata[]>
}
image: {
base64: (filePath: string) => Promise<{ mime: string; base64: string; data: string }>

View File

@ -19,10 +19,7 @@ const api = {
file: {
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId),
batchUpload: (filePaths: string[]) => ipcRenderer.invoke('file:batchUpload', filePaths),
batchDelete: (fileIds: string[]) => ipcRenderer.invoke('file:batchDelete', fileIds),
all: () => ipcRenderer.invoke('file:all')
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId)
},
image: {
base64: (filePath: string) => ipcRenderer.invoke('image:base64', filePath)

View File

@ -1,3 +1,5 @@
import '@renderer/databases'
import store, { persistor } from '@renderer/store'
import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom'

View File

@ -0,0 +1,13 @@
import { FileMetadata } from '@renderer/types'
import { Dexie, type EntityTable } from 'dexie'
// Database declaration (move this to its own module also)
export const db = new Dexie('CherryStudio') as Dexie & {
files: EntityTable<FileMetadata, 'id'>
}
db.version(1).stores({
files: 'id, name, origin_name, path, size, ext, type, created_at, count'
})
export default db

View File

@ -1,21 +1,20 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { VStack } from '@renderer/components/Layout'
import db from '@renderer/databases'
import FileManager from '@renderer/services/file'
import { FileMetadata } from '@renderer/types'
import { Image, Table } from 'antd'
import { Button, Image, Table } from 'antd'
import dayjs from 'dayjs'
import { FC, useEffect, useState } from 'react'
import { useLiveQuery } from 'dexie-react-hooks'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const FilesPage: FC = () => {
const { t } = useTranslation()
const [files, setFiles] = useState<FileMetadata[]>([])
const files = useLiveQuery<FileMetadata[]>(() => db.files.toArray())
useEffect(() => {
window.api.file.all().then(setFiles)
}, [])
const dataSource = files.map((file) => ({
const dataSource = files?.map((file) => ({
file: <Image src={'file://' + file.path} preview={false} style={{ maxHeight: '40px' }} />,
name: <a href={'file://' + file.path}>{file.name}</a>,
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
@ -53,6 +52,13 @@ const FilesPage: FC = () => {
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</Navbar>
<ContentContainer>
<Button
onClick={async () => {
const files = await FileManager.selectFiles()
files && FileManager.uploadFiles(files)
}}>
Upload
</Button>
<VStack style={{ flex: 1 }}>
<Table dataSource={dataSource} columns={columns} style={{ width: '100%', height: '100%' }} size="small" />
</VStack>

View File

@ -13,6 +13,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
import { useRuntime, useShowTopics } from '@renderer/hooks/useStore'
import { getDefaultTopic } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import FileManager from '@renderer/services/file'
import { estimateInputTokenCount } from '@renderer/services/messages'
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import { setGenerating, setSearching } from '@renderer/store/runtime'
@ -77,7 +78,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}
if (files.length > 0) {
message.files = await window.api.file.batchUpload(files)
message.files = await FileManager.uploadFiles(files)
}
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)

View File

@ -0,0 +1,57 @@
import db from '@renderer/databases'
import { FileMetadata } from '@renderer/types'
class FileManager {
static async selectFiles(options?: Electron.OpenDialogOptions): Promise<FileMetadata[] | null> {
const files = await window.api.file.select(options)
return files
}
static async uploadFile(file: FileMetadata): Promise<FileMetadata> {
const uploadFile = await window.api.file.upload(file)
const fileRecord = await db.files.get(uploadFile.id)
if (fileRecord) {
await db.files.update(fileRecord.id, { ...fileRecord, count: fileRecord.count + 1 })
return fileRecord
}
await db.files.add(uploadFile)
return uploadFile
}
static async uploadFiles(files: FileMetadata[]): Promise<FileMetadata[]> {
return Promise.all(files.map((file) => this.uploadFile(file)))
}
static async getFile(id: string): Promise<FileMetadata | undefined> {
return db.files.get(id)
}
static async deleteFile(id: string): Promise<void> {
const file = await this.getFile(id)
if (!file) {
return
}
if (file.count > 1) {
await db.files.update(id, { ...file, count: file.count - 1 })
return
}
await window.api.file.delete(id + file.ext)
db.files.delete(id)
}
static async deleteFiles(ids: string[]): Promise<void> {
await Promise.all(ids.map((id) => this.deleteFile(id)))
}
static async allFiles(): Promise<FileMetadata[]> {
return db.files.toArray()
}
}
export default FileManager

View File

@ -4,6 +4,7 @@ import { GPTTokens } from 'gpt-tokens'
import { isEmpty, takeRight } from 'lodash'
import { getAssistantSettings } from './assistant'
import FileManager from './file'
export const filterMessages = (messages: Message[]) => {
return messages
@ -61,5 +62,5 @@ export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[])
}
export function deleteMessageFiles(message: Message) {
message.files && window.api.file.batchDelete(message.files.map((f) => f.id))
message.files && FileManager.deleteFiles(message.files.map((f) => f.id))
}

View File

@ -90,7 +90,7 @@ export type MinAppType = {
export interface FileMetadata {
id: string
name: string
file_name: string
origin_name: string
path: string
size: number
ext: string

990
yarn.lock

File diff suppressed because it is too large Load Diff