refactor: remove sqlite3 use dexie
This commit is contained in:
parent
0ddef31ed8
commit
9ae9fdf392
@ -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",
|
||||
|
||||
@ -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
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -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
|
||||
@ -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')
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@ -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 }>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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'
|
||||
|
||||
13
src/renderer/src/databases/index.ts
Normal file
13
src/renderer/src/databases/index.ts
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
57
src/renderer/src/services/file.ts
Normal file
57
src/renderer/src/services/file.ts
Normal 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
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user