Upload main.js via GUI

This commit is contained in:
2025-12-28 21:18:07 +00:00
parent c611aad1f3
commit 0272756be7

134
main.js
View File

@@ -69,19 +69,24 @@ function ensureDir(dirPath) {
*/ */
function getSafeTmpDir(baseName) { function getSafeTmpDir(baseName) {
const safeBase = (baseName || 'tmp').replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').substring(0, 64); const safeBase = (baseName || 'tmp').replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').substring(0, 64);
const dir = ppath.join(os.tmpdir(), `${safeBase}-${Date.now()}`);
try { // Basis-Temp-Ordner für interne Verwaltung
if (fs.existsSync(dir)) { const internalBase = ppath.join(os.tmpdir(), 'gitea-drag');
if (!fs.statSync(dir).isDirectory()) fs.unlinkSync(dir); if (!fs.existsSync(internalBase)) fs.mkdirSync(internalBase, { recursive: true });
else fs.rmSync(dir, { recursive: true, force: true });
} // Eindeutiger Unterordner, um Kollisionen zu vermeiden
fs.mkdirSync(dir, { recursive: true }); const uniqueSub = crypto.randomBytes(8).toString('hex');
return dir; const internalDir = ppath.join(internalBase, uniqueSub);
} catch (e) { fs.mkdirSync(internalDir, { recursive: true });
throw new Error(`getSafeTmpDir failed for ${dir}: ${e && e.message ? e.message : e}`);
} // Sichtbarer Ordnername = safeBase
const finalDir = ppath.join(internalDir, safeBase);
fs.mkdirSync(finalDir, { recursive: true });
return finalDir;
} }
/* ----------------------------- /* -----------------------------
app / window app / window
----------------------------- */ ----------------------------- */
@@ -661,6 +666,30 @@ ipcMain.handle('download-gitea-folder', async (event, data) => {
} }
}); });
/* -----------------------------
prepare-download-drag (robust)
- stellt sicher, dass alle Dateien komplett geschrieben sind
- erkennt Base64 vs UTF-8 und schreibt als Buffer wenn nötig
- nutzt getSafeTmpDir() (siehe oben in deiner main.js)
----------------------------- */
function isBase64Like(str) {
if (typeof str !== 'string') return false;
// Strip whitespace/newlines
const s = str.replace(/\s+/g, '');
if (s.length === 0) return false;
// Base64 valid chars + padding
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) return false;
// length must be multiple of 4 (except maybe line breaks removed)
if (s.length % 4 !== 0) return false;
try {
// Round-trip check (cheap and practical)
const decoded = Buffer.from(s, 'base64');
return decoded.toString('base64') === s;
} catch (e) {
return false;
}
}
ipcMain.handle('prepare-download-drag', async (event, data) => { ipcMain.handle('prepare-download-drag', async (event, data) => {
try { try {
const credentials = readCredentials(); const credentials = readCredentials();
@@ -671,37 +700,104 @@ ipcMain.handle('prepare-download-drag', async (event, data) => {
const repo = data.repo; const repo = data.repo;
const remotePath = (data.path || '').replace(/^\/+/, '').replace(/\/+$/, ''); const remotePath = (data.path || '').replace(/^\/+/, '').replace(/\/+$/, '');
// Use safe tmp dir (unique) // Create a unique temp directory (guarantees clean state)
const tmpBase = getSafeTmpDir(repo || 'gitea-repo'); const tmpBase = getSafeTmpDir(repo || 'gitea-repo');
// Gather list of files (recursive)
const allFiles = []; const allFiles = [];
async function gather(pathInRepo) { async function gather(pathInRepo) {
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: 'main' }); const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'main' });
for (const item of items) { for (const item of items || []) {
if (item.type === 'dir') await gather(item.path); if (item.type === 'dir') await gather(item.path);
else if (item.type === 'file') allFiles.push(item.path); else if (item.type === 'file') allFiles.push(item.path);
} }
} }
await gather(remotePath || ''); await gather(remotePath || '');
// If no files, return early (still provide empty dir)
if (allFiles.length === 0) {
// schedule cleanup
setTimeout(() => { try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {} }, TMP_CLEANUP_MS);
return { ok: true, tempPath: tmpBase, files: [] };
}
// Download files sequentially or with limited concurrency:
const tasks = allFiles.map(remoteFile => async () => { const tasks = allFiles.map(remoteFile => async () => {
const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: 'main' }); const content = await getGiteaFileContent({ token, url, owner, repo, path: remoteFile, ref: data.ref || 'main' });
const localPath = ppath.join(tmpBase, remoteFile); const localPath = ppath.join(tmpBase, remoteFile);
fs.mkdirSync(ppath.dirname(localPath), { recursive: true }); ensureDir(ppath.dirname(localPath));
fs.writeFileSync(localPath, content, 'utf8');
// Decide how to write: if Buffer already, write directly. If string, try base64 detection
if (Buffer.isBuffer(content)) {
fs.writeFileSync(localPath, content);
} else if (typeof content === 'string') {
if (isBase64Like(content)) {
const buf = Buffer.from(content, 'base64');
fs.writeFileSync(localPath, buf);
} else {
// treat as utf8 text
fs.writeFileSync(localPath, content, 'utf8');
}
} else {
// fallback: convert to string
fs.writeFileSync(localPath, String(content), 'utf8');
}
return localPath; return localPath;
}); });
await runLimited(tasks, DEFAULT_CONCURRENCY); // runLimited ensures concurrency and waits for all writes to finish
const results = await runLimited(tasks, data.concurrency || DEFAULT_CONCURRENCY);
// verify at least one successful file
const successFiles = results.filter(r => r.ok).map(r => r.result);
if (successFiles.length === 0) {
// cleanup on complete failure
try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {}
return { ok: false, error: 'no-files-downloaded' };
}
// give renderer the temp dir (renderer should then call 'ondragstart' with the folder path)
// schedule cleanup after delay to keep files available for drag & drop
setTimeout(() => { try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {} }, TMP_CLEANUP_MS); setTimeout(() => { try { if (fs.existsSync(tmpBase)) fs.rmSync(tmpBase, { recursive: true, force: true }); } catch (_) {} }, TMP_CLEANUP_MS);
return { ok: true, tempPath: tmpBase };
return { ok: true, tempPath: tmpBase, files: successFiles };
} catch (e) { } catch (e) {
console.error('prepare-download-drag error', e); console.error('prepare-download-drag error', e);
return { ok: false, error: String(e) }; return { ok: false, error: String(e) };
} }
}); });
/* -----------------------------
ondragstart (no change to API but more defensive)
- expects renderer to call window.electronAPI.ondragStart(tempPath)
----------------------------- */
ipcMain.on('ondragstart', async (event, filePath) => {
try {
if (!filePath || !fs.existsSync(filePath)) {
console.warn('ondragstart: path missing or not exists:', filePath);
return;
}
// Prefer folder icon when dragging a directory
let icon = nativeImage.createEmpty();
try {
icon = await app.getFileIcon(filePath, { size: 'large' });
} catch (e) {
// ignore, keep empty icon
}
// startDrag accepts { file } where file can be a directory
try {
event.sender.startDrag({ file: filePath, icon });
} catch (e) {
// some platforms may require a single file — if folder fails, try to drop a placeholder file
console.error('startDrag failed for', filePath, e);
}
} catch (e) {
console.error('ondragstart error', e);
}
});
ipcMain.on('ondragstart', async (event, filePath) => { ipcMain.on('ondragstart', async (event, filePath) => {
try { try {
if (!filePath || !fs.existsSync(filePath)) return; if (!filePath || !fs.existsSync(filePath)) return;