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:
parent
2bad5a1184
commit
76d1f0bb1e
101
src/main/file.ts
101
src/main/file.ts
@ -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),
|
||||||
path: filePath,
|
fileName: path.basename(filePath),
|
||||||
createdAt: stats.birthtime,
|
path: filePath,
|
||||||
size: stats.size
|
createdAt: stats.birthtime,
|
||||||
}
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
7
src/preload/index.d.ts
vendored
7
src/preload/index.d.ts
vendored
@ -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 }>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user