Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
<div class="window">
<div class="titlebar">
<div class="title">FolderVault</div>
<span style="margin-left:12px;color:var(--muted)">AES-256-GCM · scrypt</span>
<span class="titlebar-subtitle">AES-256-GCM · scrypt</span>
</div>

<div class="left panel">
<div style="margin-bottom:8px">
<div class="button-group">
<button id="pick" class="btn">Pick Folder…</button>
<button id="openDist" class="btn ghost">Open App Folder</button>
</div>

<div class="folder-area" id="folderArea">Drop a folder here or click "Pick Folder"</div>
<div style="margin-top:8px">
<div class="password-section">
<label>Password</label>
<input id="password" type="password" placeholder="Enter encryption password" />
</div>
Expand All @@ -33,25 +33,29 @@
<label><input type="checkbox" id="secureDelete" checked /> Secure-delete originals (best-effort)</label>
</div>

<div style="margin-top:10px">
<div class="action-buttons">
<button id="encrypt" class="btn primary">Encrypt Folder</button>
<button id="decrypt" class="btn">Decrypt Folder</button>
<button id="cancel" class="btn">Cancel</button>
</div>

<div style="margin-top:12px; font-size:12px; color:var(--muted)">Selected: <span id="folder">(none)</span>
<div class="selected-folder">Selected: <span id="folder">(none)</span>
</div>
</div>

<div class="right panel">
<h3 style="margin-top:0; margin-bottom:8px">Files & Progress</h3>
<div class="progress-row" style="margin-bottom:8px">
<div style="width:120px; color:var(--muted); font-size:13px">Overall</div>
<h3>Files & Progress</h3>
<div class="progress-row">
<div class="progress-label">Overall</div>
<div class="progress"><i id="overallBar"></i></div>
<div id="overallText" style="width:90px; text-align:right; color:var(--muted); font-size:13px">0 / 0
</div>
<div id="overallText" class="progress-text">0 / 0</div>
</div>

<div class="file-list-header">
<div class="file-header-name">File Name</div>
<div class="file-header-progress">Progress</div>
<div class="file-header-status">Status</div>
</div>
<ul id="fileList" class="file-list"></ul>
</div>

Expand All @@ -62,4 +66,4 @@ <h3 style="margin-top:0; margin-bottom:8px">Files & Progress</h3>
<script src="renderer.js"></script>
</body>

</html>
</html>
63 changes: 49 additions & 14 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,31 @@ process.on('unhandledRejection', (reason) => {

function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
width: 1200,
height: 800,
minWidth: 1000,
minHeight: 700,
center: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
enableRemoteModule: false
enableRemoteModule: false,
zoomFactor: 1.0
}
});

mainWindow.loadFile(path.join(__dirname, 'index.html'));

// Ensure zoom level is set to 100% (1.0) after window is ready
mainWindow.webContents.once('did-finish-load', () => {
mainWindow.webContents.setZoomFactor(1.0);
});

// Also set zoom when window is shown (in case it was restored)
mainWindow.once('show', () => {
mainWindow.webContents.setZoomFactor(1.0);
});
}

app.whenReady().then(() => {
Expand Down Expand Up @@ -261,27 +275,48 @@ async function decryptFile(encPath, password, options = {}) {
const tmpPath = outPath + '.tmp-' + crypto.randomBytes(6).toString('hex');

const start = headerLen;
const end = fileSize - AUTH_TAG_LEN - 1; // inclusive
const end = fileSize - AUTH_TAG_LEN - 1; // inclusive end position (Node.js end is inclusive)

// Stream decrypted output to a temp file, then fsync & rename atomically
const readOpts = Object.assign({ start, end }, options.signal ? { signal: options.signal } : {});
const readStream = fs.createReadStream(encPath, readOpts);
// Handle empty files (0 bytes of encrypted data) - end will be start - 1
// This is valid - it means the original file was empty
const hasEncryptedData = end >= start;
const totalBytes = hasEncryptedData ? (end - start + 1) : 0; // +1 because end is inclusive

// counting transform for per-file byte progress during decrypt
const totalBytes = end - start + 1;
const counter = new CountingTransform(totalBytes, (seen) => {
sendProgress({ type: 'file-progress', file: encPath, seen, total: totalBytes });
});
// Stream decrypted output to a temp file, then fsync & rename atomically
let readStream;
let counter;

if (hasEncryptedData) {
// Normal case: file has encrypted data
const readOpts = Object.assign({ start, end }, options.signal ? { signal: options.signal } : {});
readStream = fs.createReadStream(encPath, readOpts);
counter = new CountingTransform(totalBytes, (seen) => {
sendProgress({ type: 'file-progress', file: encPath, seen, total: totalBytes });
});
} else {
// Empty file case: create an empty stream (no data to decrypt)
// Use stream.Readable which is already available via the stream module
const { Readable } = stream;
readStream = new Readable({
read() {
this.push(null); // End of stream immediately
}
});
counter = new CountingTransform(0, (seen) => {
sendProgress({ type: 'file-progress', file: encPath, seen, total: 0 });
});
}

// wire abort to destroy streams quickly
const abortHandler = () => {
try { readStream.destroy(new Error('aborted')); } catch (e) { }
try { if (readStream) readStream.destroy(new Error('aborted')); } catch (e) { }
try { decipher.destroy(new Error('aborted')); } catch (e) { }
};
if (options.signal) options.signal.addEventListener('abort', abortHandler, { once: true });

try {
await pipeline(readStream, counter, decipher, fs.createWriteStream(tmpPath));
const writeStream = fs.createWriteStream(tmpPath);
await pipeline(readStream, counter, decipher, writeStream);

// Ensure data is flushed to disk (best-effort)
try {
Expand Down
Loading