Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c93a139d5 | |||
| 7bd8668e79 | |||
| 19be614284 | |||
| 0272756be7 |
134
main.js
134
main.js
@@ -69,19 +69,24 @@ function ensureDir(dirPath) {
|
||||
*/
|
||||
function getSafeTmpDir(baseName) {
|
||||
const safeBase = (baseName || 'tmp').replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').substring(0, 64);
|
||||
const dir = ppath.join(os.tmpdir(), `${safeBase}-${Date.now()}`);
|
||||
try {
|
||||
if (fs.existsSync(dir)) {
|
||||
if (!fs.statSync(dir).isDirectory()) fs.unlinkSync(dir);
|
||||
else fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
} catch (e) {
|
||||
throw new Error(`getSafeTmpDir failed for ${dir}: ${e && e.message ? e.message : e}`);
|
||||
}
|
||||
|
||||
// Basis-Temp-Ordner für interne Verwaltung
|
||||
const internalBase = ppath.join(os.tmpdir(), 'gitea-drag');
|
||||
if (!fs.existsSync(internalBase)) fs.mkdirSync(internalBase, { recursive: true });
|
||||
|
||||
// Eindeutiger Unterordner, um Kollisionen zu vermeiden
|
||||
const uniqueSub = crypto.randomBytes(8).toString('hex');
|
||||
const internalDir = ppath.join(internalBase, uniqueSub);
|
||||
fs.mkdirSync(internalDir, { recursive: true });
|
||||
|
||||
// Sichtbarer Ordnername = safeBase
|
||||
const finalDir = ppath.join(internalDir, safeBase);
|
||||
fs.mkdirSync(finalDir, { recursive: true });
|
||||
|
||||
return finalDir;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------
|
||||
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) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
@@ -671,37 +700,104 @@ ipcMain.handle('prepare-download-drag', async (event, data) => {
|
||||
const repo = data.repo;
|
||||
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');
|
||||
|
||||
// Gather list of files (recursive)
|
||||
const allFiles = [];
|
||||
async function gather(pathInRepo) {
|
||||
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: 'main' });
|
||||
for (const item of items) {
|
||||
const items = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'main' });
|
||||
for (const item of items || []) {
|
||||
if (item.type === 'dir') await gather(item.path);
|
||||
else if (item.type === 'file') allFiles.push(item.path);
|
||||
}
|
||||
}
|
||||
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 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);
|
||||
fs.mkdirSync(ppath.dirname(localPath), { recursive: true });
|
||||
fs.writeFileSync(localPath, content, 'utf8');
|
||||
ensureDir(ppath.dirname(localPath));
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
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);
|
||||
return { ok: true, tempPath: tmpBase };
|
||||
|
||||
return { ok: true, tempPath: tmpBase, files: successFiles };
|
||||
} catch (e) {
|
||||
console.error('prepare-download-drag error', 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) => {
|
||||
try {
|
||||
if (!filePath || !fs.existsSync(filePath)) return;
|
||||
|
||||
Reference in New Issue
Block a user