mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
feat: implement folder watching functionality with IPC integration
This commit is contained in:
parent
775dea7894
commit
28f556224a
5 changed files with 280 additions and 0 deletions
|
|
@ -6,4 +6,14 @@ export const IPC_CHANNELS = {
|
|||
SET_QUICK_ASK_MODE: 'set-quick-ask-mode',
|
||||
GET_QUICK_ASK_MODE: 'get-quick-ask-mode',
|
||||
REPLACE_TEXT: 'replace-text',
|
||||
// Folder sync channels
|
||||
FOLDER_SYNC_SELECT_FOLDER: 'folder-sync:select-folder',
|
||||
FOLDER_SYNC_ADD_FOLDER: 'folder-sync:add-folder',
|
||||
FOLDER_SYNC_REMOVE_FOLDER: 'folder-sync:remove-folder',
|
||||
FOLDER_SYNC_GET_FOLDERS: 'folder-sync:get-folders',
|
||||
FOLDER_SYNC_GET_STATUS: 'folder-sync:get-status',
|
||||
FOLDER_SYNC_FILE_CHANGED: 'folder-sync:file-changed',
|
||||
FOLDER_SYNC_WATCHER_READY: 'folder-sync:watcher-ready',
|
||||
FOLDER_SYNC_PAUSE: 'folder-sync:pause',
|
||||
FOLDER_SYNC_RESUME: 'folder-sync:resume',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
import { app, ipcMain, shell } from 'electron';
|
||||
import { IPC_CHANNELS } from './channels';
|
||||
import {
|
||||
selectFolder,
|
||||
addWatchedFolder,
|
||||
removeWatchedFolder,
|
||||
getWatchedFolders,
|
||||
getWatcherStatus,
|
||||
pauseWatcher,
|
||||
resumeWatcher,
|
||||
} from '../modules/folder-watcher';
|
||||
|
||||
export function registerIpcHandlers(): void {
|
||||
ipcMain.on(IPC_CHANNELS.OPEN_EXTERNAL, (_event, url: string) => {
|
||||
|
|
@ -16,4 +25,23 @@ export function registerIpcHandlers(): void {
|
|||
ipcMain.handle(IPC_CHANNELS.GET_APP_VERSION, () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
// Folder sync handlers
|
||||
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_SELECT_FOLDER, () => selectFolder());
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_ADD_FOLDER, (_event, config) =>
|
||||
addWatchedFolder(config)
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_REMOVE_FOLDER, (_event, folderPath: string) =>
|
||||
removeWatchedFolder(folderPath)
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_GET_FOLDERS, () => getWatchedFolders());
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_GET_STATUS, () => getWatcherStatus());
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_PAUSE, () => pauseWatcher());
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.FOLDER_SYNC_RESUME, () => resumeWatcher());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { setupDeepLinks, handlePendingDeepLink } from './modules/deep-links';
|
|||
import { setupAutoUpdater } from './modules/auto-updater';
|
||||
import { setupMenu } from './modules/menu';
|
||||
import { registerQuickAsk, unregisterQuickAsk } from './modules/quick-ask';
|
||||
import { registerFolderWatcher, unregisterFolderWatcher } from './modules/folder-watcher';
|
||||
import { registerIpcHandlers } from './ipc/handlers';
|
||||
|
||||
registerGlobalErrorHandlers();
|
||||
|
|
@ -28,6 +29,7 @@ app.whenReady().then(async () => {
|
|||
}
|
||||
createMainWindow();
|
||||
registerQuickAsk();
|
||||
registerFolderWatcher();
|
||||
setupAutoUpdater();
|
||||
|
||||
handlePendingDeepLink();
|
||||
|
|
@ -47,4 +49,5 @@ app.on('window-all-closed', () => {
|
|||
|
||||
app.on('will-quit', () => {
|
||||
unregisterQuickAsk();
|
||||
unregisterFolderWatcher();
|
||||
});
|
||||
|
|
|
|||
216
surfsense_desktop/src/modules/folder-watcher.ts
Normal file
216
surfsense_desktop/src/modules/folder-watcher.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import { BrowserWindow, dialog } from 'electron';
|
||||
import chokidar from 'chokidar';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { IPC_CHANNELS } from '../ipc/channels';
|
||||
|
||||
export interface WatchedFolderConfig {
|
||||
path: string;
|
||||
name: string;
|
||||
excludePatterns: string[];
|
||||
fileExtensions: string[] | null;
|
||||
connectorId: number;
|
||||
searchSpaceId: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface WatcherEntry {
|
||||
config: WatchedFolderConfig;
|
||||
watcher: chokidar.FSWatcher | null;
|
||||
}
|
||||
|
||||
const STORE_KEY = 'watchedFolders';
|
||||
let store: any = null;
|
||||
let watchers: Map<string, WatcherEntry> = new Map();
|
||||
|
||||
async function getStore() {
|
||||
if (!store) {
|
||||
const { default: Store } = await import('electron-store');
|
||||
store = new Store({
|
||||
name: 'folder-watcher',
|
||||
defaults: {
|
||||
[STORE_KEY]: [] as WatchedFolderConfig[],
|
||||
},
|
||||
});
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
function getMainWindow(): BrowserWindow | null {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
return windows.length > 0 ? windows[0] : null;
|
||||
}
|
||||
|
||||
function sendToRenderer(channel: string, data: any) {
|
||||
const win = getMainWindow();
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send(channel, data);
|
||||
}
|
||||
}
|
||||
|
||||
function startWatcher(config: WatchedFolderConfig) {
|
||||
if (watchers.has(config.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ignored = [
|
||||
/(^|[/\\])\../, // dotfiles by default
|
||||
...config.excludePatterns.map((p) => `**/${p}/**`),
|
||||
];
|
||||
|
||||
const watcher = chokidar.watch(config.path, {
|
||||
persistent: true,
|
||||
ignoreInitial: false,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 500,
|
||||
pollInterval: 100,
|
||||
},
|
||||
ignored,
|
||||
});
|
||||
|
||||
let ready = false;
|
||||
|
||||
watcher.on('ready', () => {
|
||||
ready = true;
|
||||
sendToRenderer(IPC_CHANNELS.FOLDER_SYNC_WATCHER_READY, {
|
||||
connectorId: config.connectorId,
|
||||
folderPath: config.path,
|
||||
});
|
||||
});
|
||||
|
||||
const handleFileEvent = (filePath: string, action: string) => {
|
||||
if (!ready) return;
|
||||
|
||||
const relativePath = path.relative(config.path, filePath);
|
||||
|
||||
if (
|
||||
config.fileExtensions &&
|
||||
config.fileExtensions.length > 0
|
||||
) {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
if (!config.fileExtensions.includes(ext)) return;
|
||||
}
|
||||
|
||||
sendToRenderer(IPC_CHANNELS.FOLDER_SYNC_FILE_CHANGED, {
|
||||
connectorId: config.connectorId,
|
||||
searchSpaceId: config.searchSpaceId,
|
||||
folderPath: config.path,
|
||||
relativePath,
|
||||
fullPath: filePath,
|
||||
action,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
};
|
||||
|
||||
watcher.on('add', (fp) => handleFileEvent(fp, 'add'));
|
||||
watcher.on('change', (fp) => handleFileEvent(fp, 'change'));
|
||||
watcher.on('unlink', (fp) => handleFileEvent(fp, 'unlink'));
|
||||
|
||||
watchers.set(config.path, { config, watcher });
|
||||
}
|
||||
|
||||
function stopWatcher(folderPath: string) {
|
||||
const entry = watchers.get(folderPath);
|
||||
if (entry?.watcher) {
|
||||
entry.watcher.close();
|
||||
}
|
||||
watchers.delete(folderPath);
|
||||
}
|
||||
|
||||
export async function selectFolder(): Promise<string | null> {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory'],
|
||||
title: 'Select a folder to watch',
|
||||
});
|
||||
if (result.canceled || result.filePaths.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return result.filePaths[0];
|
||||
}
|
||||
|
||||
export async function addWatchedFolder(
|
||||
config: WatchedFolderConfig
|
||||
): Promise<WatchedFolderConfig[]> {
|
||||
const s = await getStore();
|
||||
const folders: WatchedFolderConfig[] = s.get(STORE_KEY, []);
|
||||
|
||||
const existing = folders.findIndex((f: WatchedFolderConfig) => f.path === config.path);
|
||||
if (existing >= 0) {
|
||||
folders[existing] = config;
|
||||
} else {
|
||||
folders.push(config);
|
||||
}
|
||||
|
||||
s.set(STORE_KEY, folders);
|
||||
|
||||
if (config.active) {
|
||||
startWatcher(config);
|
||||
}
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
export async function removeWatchedFolder(
|
||||
folderPath: string
|
||||
): Promise<WatchedFolderConfig[]> {
|
||||
const s = await getStore();
|
||||
const folders: WatchedFolderConfig[] = s.get(STORE_KEY, []);
|
||||
const updated = folders.filter((f: WatchedFolderConfig) => f.path !== folderPath);
|
||||
s.set(STORE_KEY, updated);
|
||||
|
||||
stopWatcher(folderPath);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function getWatchedFolders(): Promise<WatchedFolderConfig[]> {
|
||||
const s = await getStore();
|
||||
return s.get(STORE_KEY, []);
|
||||
}
|
||||
|
||||
export async function getWatcherStatus(): Promise<
|
||||
{ path: string; active: boolean; watching: boolean }[]
|
||||
> {
|
||||
const s = await getStore();
|
||||
const folders: WatchedFolderConfig[] = s.get(STORE_KEY, []);
|
||||
return folders.map((f: WatchedFolderConfig) => ({
|
||||
path: f.path,
|
||||
active: f.active,
|
||||
watching: watchers.has(f.path),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function pauseWatcher(): Promise<void> {
|
||||
for (const [, entry] of watchers) {
|
||||
if (entry.watcher) {
|
||||
await entry.watcher.close();
|
||||
entry.watcher = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function resumeWatcher(): Promise<void> {
|
||||
for (const [folderPath, entry] of watchers) {
|
||||
if (!entry.watcher && entry.config.active) {
|
||||
startWatcher(entry.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerFolderWatcher(): Promise<void> {
|
||||
const s = await getStore();
|
||||
const folders: WatchedFolderConfig[] = s.get(STORE_KEY, []);
|
||||
|
||||
for (const config of folders) {
|
||||
if (config.active && fs.existsSync(config.path)) {
|
||||
startWatcher(config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function unregisterFolderWatcher(): Promise<void> {
|
||||
for (const [folderPath] of watchers) {
|
||||
stopWatcher(folderPath);
|
||||
}
|
||||
watchers.clear();
|
||||
}
|
||||
|
|
@ -21,4 +21,27 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
setQuickAskMode: (mode: string) => ipcRenderer.invoke(IPC_CHANNELS.SET_QUICK_ASK_MODE, mode),
|
||||
getQuickAskMode: () => ipcRenderer.invoke(IPC_CHANNELS.GET_QUICK_ASK_MODE),
|
||||
replaceText: (text: string) => ipcRenderer.invoke(IPC_CHANNELS.REPLACE_TEXT, text),
|
||||
|
||||
// Folder sync
|
||||
selectFolder: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_SELECT_FOLDER),
|
||||
addWatchedFolder: (config: any) => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_ADD_FOLDER, config),
|
||||
removeWatchedFolder: (folderPath: string) => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_REMOVE_FOLDER, folderPath),
|
||||
getWatchedFolders: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_GET_FOLDERS),
|
||||
getWatcherStatus: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_GET_STATUS),
|
||||
onFileChanged: (callback: (data: any) => void) => {
|
||||
const listener = (_event: unknown, data: any) => callback(data);
|
||||
ipcRenderer.on(IPC_CHANNELS.FOLDER_SYNC_FILE_CHANGED, listener);
|
||||
return () => {
|
||||
ipcRenderer.removeListener(IPC_CHANNELS.FOLDER_SYNC_FILE_CHANGED, listener);
|
||||
};
|
||||
},
|
||||
onWatcherReady: (callback: (data: any) => void) => {
|
||||
const listener = (_event: unknown, data: any) => callback(data);
|
||||
ipcRenderer.on(IPC_CHANNELS.FOLDER_SYNC_WATCHER_READY, listener);
|
||||
return () => {
|
||||
ipcRenderer.removeListener(IPC_CHANNELS.FOLDER_SYNC_WATCHER_READY, listener);
|
||||
};
|
||||
},
|
||||
pauseWatcher: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_PAUSE),
|
||||
resumeWatcher: () => ipcRenderer.invoke(IPC_CHANNELS.FOLDER_SYNC_RESUME),
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue