feat: added file management functionality and API operations

- Improved functionality for file management has been added.
- Added file system management functionality through IPC.
- Added functionality to interact with files including selection, upload, deletion, and batch operations.
- Added new file operations to the custom API, including file select, upload, delete, batch upload, and batch delete functions.
- Implemented feature to select and upload files via API.
This commit is contained in:
kangfenmao 2024-09-10 17:02:07 +08:00
parent 2bad5a1184
commit 76d1f0bb1e
5 changed files with 134 additions and 22 deletions

View File

@ -1,4 +1,5 @@
import { app, dialog } from 'electron' import * as crypto from 'crypto'
import { app, dialog, OpenDialogOptions } from 'electron'
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@ -6,9 +7,11 @@ import { v4 as uuidv4 } from 'uuid'
interface FileMetadata { interface FileMetadata {
id: string id: string
name: string name: string
fileName: string
path: string path: string
createdAt: Date
size: number size: number
ext: string
createdAt: Date
} }
export class File { export class File {
@ -25,41 +28,103 @@ export class File {
} }
} }
async selectFile(): Promise<FileMetadata | null> { private async getFileHash(filePath: string): Promise<string> {
const result = await dialog.showOpenDialog({ return new Promise((resolve, reject) => {
properties: ['openFile'] const hash = crypto.createHash('md5')
const stream = fs.createReadStream(filePath)
stream.on('data', (data) => hash.update(data))
stream.on('end', () => resolve(hash.digest('hex')))
stream.on('error', reject)
}) })
}
async findDuplicateFile(filePath: string): Promise<FileMetadata | null> {
const stats = fs.statSync(filePath)
const fileSize = stats.size
const files = await fs.promises.readdir(this.storageDir)
for (const file of files) {
const storedFilePath = path.join(this.storageDir, file)
const storedStats = fs.statSync(storedFilePath)
if (storedStats.size === fileSize) {
const [originalHash, storedHash] = await Promise.all([
this.getFileHash(filePath),
this.getFileHash(storedFilePath)
])
if (originalHash === storedHash) {
const ext = path.extname(file)
return {
id: path.basename(file, ext),
name: path.basename(filePath),
fileName: file,
path: storedFilePath,
createdAt: storedStats.birthtime,
size: storedStats.size,
ext: ext
}
}
}
}
return null
}
async selectFile(options?: OpenDialogOptions): Promise<FileMetadata[] | null> {
const defaultOptions: OpenDialogOptions = {
properties: ['openFile']
}
const dialogOptions = { ...defaultOptions, ...options }
const result = await dialog.showOpenDialog(dialogOptions)
if (result.canceled || result.filePaths.length === 0) { if (result.canceled || result.filePaths.length === 0) {
return null return null
} }
const filePath = result.filePaths[0] const fileMetadataPromises = result.filePaths.map(async (filePath) => {
const stats = fs.statSync(filePath) const stats = fs.statSync(filePath)
const ext = path.extname(filePath)
return { return {
id: uuidv4(), id: uuidv4(),
name: path.basename(filePath), name: path.basename(filePath),
fileName: path.basename(filePath),
path: filePath, path: filePath,
createdAt: stats.birthtime, createdAt: stats.birthtime,
size: stats.size size: stats.size,
ext: ext
} }
})
return Promise.all(fileMetadataPromises)
} }
async uploadFile(filePath: string): Promise<FileMetadata> { async uploadFile(filePath: string): Promise<FileMetadata> {
const id = uuidv4() const duplicateFile = await this.findDuplicateFile(filePath)
if (duplicateFile) {
return duplicateFile
}
const uuid = uuidv4()
const name = path.basename(filePath) const name = path.basename(filePath)
const destPath = path.join(this.storageDir, id) const ext = path.extname(name)
const destPath = path.join(this.storageDir, uuid + ext)
await fs.promises.copyFile(filePath, destPath) await fs.promises.copyFile(filePath, destPath)
const stats = await fs.promises.stat(destPath) const stats = await fs.promises.stat(destPath)
return { return {
id, id: uuid,
name, name,
fileName: uuid + ext,
path: destPath, path: destPath,
createdAt: stats.birthtime, createdAt: stats.birthtime,
size: stats.size size: stats.size,
ext: ext
} }
} }

View File

@ -1,11 +1,14 @@
import { BrowserWindow, ipcMain, session, shell } from 'electron' import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'electron'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config' import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
import { File } from './file'
import AppUpdater from './updater' import AppUpdater from './updater'
import { openFile, saveFile } from './utils/file' import { openFile, saveFile } from './utils/file'
import { compress, decompress } from './utils/zip' import { compress, decompress } from './utils/zip'
import { createMinappWindow } from './window' import { createMinappWindow } from './window'
const fileManager = new File()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const { autoUpdater } = new AppUpdater(mainWindow) const { autoUpdater } = new AppUpdater(mainWindow)
@ -31,6 +34,28 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('zip:compress', (_, text: string) => compress(text)) ipcMain.handle('zip:compress', (_, text: string) => compress(text))
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text)) ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => {
return await fileManager.selectFile(options)
})
ipcMain.handle('file:upload', async (_, filePath: string) => {
return await fileManager.uploadFile(filePath)
})
ipcMain.handle('file:delete', async (_, fileId: string) => {
await fileManager.deleteFile(fileId)
return { success: true }
})
ipcMain.handle('file:batchUpload', async (_, filePaths: string[]) => {
return await fileManager.batchUploadFiles(filePaths)
})
ipcMain.handle('file:batchDelete', async (_, fileIds: string[]) => {
await fileManager.batchDeleteFiles(fileIds)
return { success: true }
})
ipcMain.handle('minapp', (_, args) => { ipcMain.handle('minapp', (_, args) => {
createMinappWindow({ createMinappWindow({
url: args.url, url: args.url,

View File

@ -1,6 +1,8 @@
import { ElectronAPI } from '@electron-toolkit/preload' import { ElectronAPI } from '@electron-toolkit/preload'
import type { OpenDialogOptions } from 'electron' import type { OpenDialogOptions } from 'electron'
import type FileMetadata from '../main/file'
declare global { declare global {
interface Window { interface Window {
electron: ElectronAPI electron: ElectronAPI
@ -20,6 +22,11 @@ declare global {
reload: () => void reload: () => void
compress: (text: string) => Promise<Buffer> compress: (text: string) => Promise<Buffer>
decompress: (text: Buffer) => Promise<string> decompress: (text: Buffer) => Promise<string>
fileSelect: (options?: OpenDialogOptions) => Promise<FileMetadata[] | null>
fileUpload: (filePath: string) => Promise<FileMetadata>
fileDelete: (fileId: string) => Promise<{ success: boolean }>
fileBatchUpload: (filePaths: string[]) => Promise<FileMetadata[]>
fileBatchDelete: (fileIds: string[]) => Promise<{ success: boolean }>
} }
} }
} }

View File

@ -1,5 +1,5 @@
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge, ipcRenderer } from 'electron' import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
// Custom APIs for renderer // Custom APIs for renderer
const api = { const api = {
@ -15,7 +15,12 @@ const api = {
ipcRenderer.invoke('save-file', path, content, options) 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),
fileSelect: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
fileUpload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
fileDelete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId),
fileBatchUpload: (filePaths: string[]) => ipcRenderer.invoke('file:batchUpload', filePaths),
fileBatchDelete: (fileIds: string[]) => ipcRenderer.invoke('file:batchDelete', fileIds)
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to

View File

@ -7,13 +7,23 @@ import styled from 'styled-components'
const FilesPage: FC = () => { const FilesPage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const handleSelectFile = async () => {
const files = await window.api.fileSelect({
properties: ['openFile', 'multiSelections']
})
for (const file of files || []) {
const result = await window.api.fileUpload(file.path)
console.log('Selected file:', file, result)
}
}
return ( return (
<Container> <Container>
<Navbar> <Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</Navbar> </Navbar>
<ContentContainer> <ContentContainer>
<Button></Button> <Button onClick={handleSelectFile}></Button>
</ContentContainer> </ContentContainer>
</Container> </Container>
) )