Skip to content

Commit 4170a31

Browse files
author
FileShot
committed
Fix blank UI + add sidebar File Explorer + real secure shred engine
1 parent e34eca9 commit 4170a31

5 files changed

Lines changed: 924 additions & 151 deletions

File tree

main.js

Lines changed: 332 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { app, BrowserWindow, Tray, Menu, ipcMain, dialog, shell, nativeImage, clipboard } = require('electron');
22
const path = require('path');
33
const fs = require('fs');
4+
const os = require('os');
45
const crypto = require('crypto');
56
const axios = require('axios');
67
const FormData = require('form-data');
@@ -93,6 +94,181 @@ function vaultTotalBytes(items) {
9394
return (items || []).reduce((sum, it) => sum + Number(it.size || 0), 0);
9495
}
9596

97+
// ============================================================================
98+
// SECURE SHRED (BEST-EFFORT)
99+
// ============================================================================
100+
101+
function shredPassesForMethod(method) {
102+
const m = String(method || '').toLowerCase();
103+
if (m === 'gutmann') return 35;
104+
if (m === 'dod') return 7;
105+
if (m === 'simple' || m === 'single') return 1;
106+
return 1;
107+
}
108+
109+
function isProbablyDir(p) {
110+
try {
111+
const st = fs.statSync(p);
112+
return st.isDirectory();
113+
} catch (_) {
114+
return false;
115+
}
116+
}
117+
118+
async function listFilesRecursive(rootPath, fileList = []) {
119+
const p = String(rootPath || '');
120+
if (!p) return fileList;
121+
let ents;
122+
try {
123+
ents = await fs.promises.readdir(p, { withFileTypes: true });
124+
} catch (_) {
125+
return fileList;
126+
}
127+
128+
for (const ent of ents) {
129+
const full = path.join(p, ent.name);
130+
try {
131+
if (ent.isDirectory()) {
132+
await listFilesRecursive(full, fileList);
133+
} else if (ent.isFile()) {
134+
fileList.push(full);
135+
}
136+
// Skip symlinks and others
137+
} catch (_) {}
138+
}
139+
return fileList;
140+
}
141+
142+
async function tryRemoveEmptyDirs(rootPath) {
143+
const p = String(rootPath || '');
144+
if (!p) return;
145+
let ents;
146+
try {
147+
ents = await fs.promises.readdir(p, { withFileTypes: true });
148+
} catch (_) {
149+
return;
150+
}
151+
152+
for (const ent of ents) {
153+
if (!ent.isDirectory()) continue;
154+
const full = path.join(p, ent.name);
155+
await tryRemoveEmptyDirs(full);
156+
}
157+
158+
// Now try removing this dir if empty
159+
try {
160+
const left = await fs.promises.readdir(p);
161+
if (left.length === 0) {
162+
await fs.promises.rmdir(p);
163+
}
164+
} catch (_) {}
165+
}
166+
167+
async function overwriteAndDeleteFile(filePath, passes, onProgress) {
168+
const p = String(filePath || '');
169+
if (!p) return;
170+
171+
let st;
172+
try {
173+
st = await fs.promises.stat(p);
174+
} catch (e) {
175+
throw new Error(`Missing file: ${p}`);
176+
}
177+
if (!st.isFile()) return;
178+
179+
// Rename before wipe (best-effort)
180+
let workPath = p;
181+
try {
182+
const dir = path.dirname(p);
183+
const rnd = crypto.randomBytes(12).toString('hex');
184+
const renamed = path.join(dir, rnd);
185+
await fs.promises.rename(p, renamed);
186+
workPath = renamed;
187+
} catch (_) {}
188+
189+
if (passes <= 0) {
190+
await fs.promises.unlink(workPath);
191+
return;
192+
}
193+
194+
const size = Number(st.size || 0);
195+
const chunkSize = 1024 * 1024; // 1MB
196+
const buf = Buffer.allocUnsafe(chunkSize);
197+
198+
const fh = await fs.promises.open(workPath, 'r+');
199+
try {
200+
for (let pass = 1; pass <= passes; pass++) {
201+
let offset = 0;
202+
while (offset < size) {
203+
const len = Math.min(chunkSize, size - offset);
204+
crypto.randomFillSync(buf, 0, len);
205+
await fh.write(buf, 0, len, offset);
206+
offset += len;
207+
}
208+
try { await fh.sync(); } catch (_) {}
209+
if (typeof onProgress === 'function') {
210+
onProgress({ pass, passes });
211+
}
212+
}
213+
} finally {
214+
try { await fh.close(); } catch (_) {}
215+
}
216+
217+
try { await fs.promises.truncate(workPath, 0); } catch (_) {}
218+
await fs.promises.unlink(workPath);
219+
}
220+
221+
function resolveSpecialTargetPaths(targets) {
222+
const t = Array.isArray(targets) ? targets.map(String) : [];
223+
const out = [];
224+
const platform = process.platform;
225+
226+
if (t.includes('downloads')) {
227+
try { out.push(app.getPath('downloads')); } catch (_) {}
228+
}
229+
if (t.includes('temp')) {
230+
try { out.push(app.getPath('temp')); } catch (_) { out.push(os.tmpdir()); }
231+
}
232+
if (t.includes('recycle')) {
233+
if (platform === 'win32') {
234+
// Best-effort: common recycle bin folder (permissions may block)
235+
out.push('C:\\$Recycle.Bin');
236+
} else if (platform === 'darwin') {
237+
out.push(path.join(os.homedir(), '.Trash'));
238+
} else {
239+
out.push(path.join(os.homedir(), '.local', 'share', 'Trash', 'files'));
240+
}
241+
}
242+
if (t.includes('browser')) {
243+
// Best-effort cache locations. Often locked; failures are reported.
244+
if (platform === 'win32') {
245+
const local = process.env.LOCALAPPDATA || '';
246+
if (local) {
247+
out.push(path.join(local, 'Google', 'Chrome', 'User Data', 'Default', 'Cache'));
248+
out.push(path.join(local, 'Microsoft', 'Edge', 'User Data', 'Default', 'Cache'));
249+
}
250+
} else if (platform === 'darwin') {
251+
out.push(path.join(os.homedir(), 'Library', 'Caches', 'Google', 'Chrome'));
252+
out.push(path.join(os.homedir(), 'Library', 'Caches', 'Microsoft Edge'));
253+
} else {
254+
out.push(path.join(os.homedir(), '.cache', 'google-chrome'));
255+
out.push(path.join(os.homedir(), '.cache', 'chromium'));
256+
}
257+
}
258+
259+
// Deduplicate + keep existing
260+
const seen = new Set();
261+
const cleaned = [];
262+
for (const p of out) {
263+
const s = String(p || '').trim();
264+
if (!s) continue;
265+
if (seen.has(s)) continue;
266+
seen.add(s);
267+
cleaned.push(s);
268+
}
269+
return cleaned;
270+
}
271+
96272
function hasBundledFrontend() {
97273
try {
98274
return fs.existsSync(LOCAL_FRONTEND_INDEX);
@@ -211,7 +387,7 @@ function createWindow() {
211387
const u = new URL(url);
212388
const isFileShot = u.hostname === 'fileshot.io' || u.hostname === 'www.fileshot.io';
213389
if (isFileShot && mainWindow) {
214-
loadFrontend({ preferredPath: u.pathname + u.search + u.hash, reason: 'same-window navigation' }).catch(() => {});
390+
loadFrontendFallback({ preferredPath: u.pathname + u.search + u.hash, reason: 'same-window navigation' }).catch(() => {});
215391
return { action: 'deny' };
216392
}
217393
} catch (_) {}
@@ -570,14 +746,168 @@ ipcMain.handle('select-folder', async () => {
570746
return result.filePaths;
571747
});
572748

749+
ipcMain.handle('save-file', async (_event, options) => {
750+
const opts = options && typeof options === 'object' ? options : {};
751+
const result = await dialog.showSaveDialog({
752+
title: opts.title || 'Save File',
753+
defaultPath: opts.defaultPath,
754+
filters: Array.isArray(opts.filters) ? opts.filters : undefined
755+
});
756+
return { canceled: result.canceled, filePath: result.filePath };
757+
});
758+
759+
ipcMain.handle('fs-list-drives', async () => {
760+
// Cross-platform “roots” list.
761+
// Windows: return common drive letters when possible.
762+
// macOS/Linux: return '/'
763+
const platform = process.platform;
764+
765+
if (platform !== 'win32') {
766+
return [{ name: '/', path: '/' }];
767+
}
768+
769+
// Windows: probe common drive letters quickly without shelling out
770+
const drives = [];
771+
for (let c = 67; c <= 90; c++) { // C..Z
772+
const letter = String.fromCharCode(c);
773+
const p = `${letter}:\\`;
774+
try {
775+
if (fs.existsSync(p)) drives.push({ name: p, path: p });
776+
} catch (_) {}
777+
}
778+
return drives.length ? drives : [{ name: 'C:\\', path: 'C:\\' }];
779+
});
780+
781+
ipcMain.handle('fs-list-dir', async (_event, dirPath) => {
782+
const p = String(dirPath || '');
783+
if (!p) return { path: p, entries: [] };
784+
785+
try {
786+
const entries = await fs.promises.readdir(p, { withFileTypes: true });
787+
const mapped = [];
788+
for (const ent of entries) {
789+
const full = path.join(p, ent.name);
790+
mapped.push({
791+
name: ent.name,
792+
path: full,
793+
isDir: ent.isDirectory(),
794+
isFile: ent.isFile()
795+
});
796+
}
797+
798+
// Sort directories first, then files, then alpha
799+
mapped.sort((a, b) => {
800+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
801+
return a.name.localeCompare(b.name);
802+
});
803+
804+
return { path: p, entries: mapped };
805+
} catch (e) {
806+
return { path: p, entries: [], error: e && e.message ? e.message : String(e) };
807+
}
808+
});
809+
573810
ipcMain.handle('copy-to-clipboard', async (_event, text) => {
574811
clipboard.writeText(String(text || ''));
575812
return { success: true };
576813
});
577814

815+
function sendShredProgress(event, requestId, msg) {
816+
if (!requestId) return;
817+
try {
818+
event.sender.send(`shred-progress:${requestId}`, msg);
819+
} catch (_) {}
820+
}
821+
822+
ipcMain.handle('shred-start', async (event, payload) => {
823+
const req = payload && typeof payload === 'object' ? payload : {};
824+
const requestId = String(req.requestId || '');
825+
const method = String(req.method || 'simple');
826+
const passes = shredPassesForMethod(method);
827+
828+
const targets = Array.isArray(req.targets) ? req.targets.map(String) : [];
829+
const customPaths = Array.isArray(req.paths) ? req.paths.map(String) : [];
830+
831+
const targetPaths = resolveSpecialTargetPaths(targets);
832+
const allRoots = [...customPaths, ...targetPaths]
833+
.map((p) => String(p || '').trim())
834+
.filter(Boolean);
835+
836+
if (!allRoots.length) {
837+
return { success: false, error: 'No targets specified' };
838+
}
839+
840+
// Expand directories to files, keep file targets as-is.
841+
const files = [];
842+
const dirs = [];
843+
for (const p of allRoots) {
844+
try {
845+
const st = await fs.promises.stat(p);
846+
if (st.isDirectory()) dirs.push(p);
847+
else if (st.isFile()) files.push(p);
848+
} catch (_) {
849+
// Missing targets are ignored but reported
850+
}
851+
}
852+
853+
for (const d of dirs) {
854+
await listFilesRecursive(d, files);
855+
}
856+
857+
// Deduplicate files
858+
const uniq = [];
859+
const seen = new Set();
860+
for (const f of files) {
861+
if (!f) continue;
862+
const key = String(f);
863+
if (seen.has(key)) continue;
864+
seen.add(key);
865+
uniq.push(key);
866+
}
867+
868+
const total = uniq.length;
869+
const errors = [];
870+
871+
sendShredProgress(event, requestId, { percent: 0, message: `Preparing… (${total} file(s))` });
872+
873+
let done = 0;
874+
for (const f of uniq) {
875+
try {
876+
sendShredProgress(event, requestId, {
877+
percent: total ? Math.round((done / total) * 100) : 0,
878+
message: `Shredding: ${f}`
879+
});
880+
881+
await overwriteAndDeleteFile(f, passes, ({ pass, passes: pcount }) => {
882+
sendShredProgress(event, requestId, {
883+
percent: total ? Math.round((done / total) * 100) : 0,
884+
message: `Wipe pass ${pass}/${pcount}: ${path.basename(f)}`
885+
});
886+
});
887+
} catch (e) {
888+
errors.push({ path: f, error: e && e.message ? e.message : String(e) });
889+
}
890+
done++;
891+
sendShredProgress(event, requestId, {
892+
percent: total ? Math.round((done / total) * 100) : 100,
893+
message: `Progress: ${done}/${total}`
894+
});
895+
}
896+
897+
// Best-effort cleanup of emptied directories
898+
for (const d of dirs) {
899+
try { await tryRemoveEmptyDirs(d); } catch (_) {}
900+
}
901+
902+
const success = errors.length === 0;
903+
sendShredProgress(event, requestId, { percent: 100, message: success ? 'Complete' : `Complete with ${errors.length} error(s)` });
904+
905+
return { success, totalFiles: total, errors };
906+
});
907+
578908
ipcMain.handle('go-online', async () => {
579909
if (!mainWindow) return { success: false };
580-
await loadFrontend({ preferredPath: '/', reason: 'renderer:go-online' });
910+
await loadFrontendFallback({ preferredPath: '/', reason: 'renderer:go-online' });
581911
mainWindow.show();
582912
mainWindow.focus();
583913
return { success: true };

preload.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
2222
// File operations
2323
selectFile: () => ipcRenderer.invoke('select-file'),
2424
selectFolder: () => ipcRenderer.invoke('select-folder'),
25+
saveFile: (options) => ipcRenderer.invoke('save-file', options),
26+
27+
// Local file explorer (read-only listings)
28+
fsListDrives: () => ipcRenderer.invoke('fs-list-drives'),
29+
fsListDir: (dirPath) => ipcRenderer.invoke('fs-list-dir', dirPath),
2530

2631
// Local vault
2732
vaultList: () => ipcRenderer.invoke('vault-list'),
@@ -51,6 +56,17 @@ contextBridge.exposeInMainWorld('electronAPI', {
5156
// App controls
5257
goOnline: () => ipcRenderer.invoke('go-online'),
5358
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', String(text || '')),
59+
60+
// Secure shred (local destructive operation)
61+
shredStart: (payload, progressCb) => {
62+
const requestId = `${Date.now()}_${Math.random().toString(16).slice(2)}`;
63+
if (typeof progressCb === 'function') {
64+
ipcRenderer.on(`shred-progress:${requestId}`, (_event, msg) => {
65+
try { progressCb(msg); } catch (_) {}
66+
});
67+
}
68+
return ipcRenderer.invoke('shred-start', { ...(payload || {}), requestId });
69+
},
5470

5571
// Recent uploads
5672
addRecentUpload: (upload) => ipcRenderer.invoke('add-recent-upload', upload),

0 commit comments

Comments
 (0)