Upload via GUI (10 Dateien)
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
# ===== Zugangsdaten / Secrets (NIE hochladen!) =====
|
||||
/data/
|
||||
credentials.json
|
||||
*.env
|
||||
.env*
|
||||
*.pem
|
||||
*.key
|
||||
*.pfx
|
||||
*.p12
|
||||
|
||||
# ===== Abhängigkeiten =====
|
||||
node_modules/
|
||||
.npm/
|
||||
.cache/
|
||||
|
||||
# ===== Build-Ausgaben =====
|
||||
dist/
|
||||
out/
|
||||
release/
|
||||
*.exe
|
||||
*.blockmap
|
||||
*.sha256
|
||||
*.sha256sum
|
||||
|
||||
# ===== .NET Installer (Visual Studio / dotnet) =====
|
||||
installer/bin/
|
||||
installer/obj/
|
||||
**/bin/
|
||||
**/obj/
|
||||
.vs/
|
||||
*.user
|
||||
*.suo
|
||||
|
||||
# ===== Große gebündelte Binaries (MinGit, ca. 93 MB) =====
|
||||
vendor/mingit/
|
||||
|
||||
# ===== Laufzeitdaten / Backups / Repos =====
|
||||
/repos/
|
||||
/backup/
|
||||
*.zip
|
||||
*.rar
|
||||
*.7z
|
||||
|
||||
# ===== Logs =====
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# ===== Editor / OS =====
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# ===== Claude Code (lokale Einstellungen) =====
|
||||
.claude/settings.local.json
|
||||
@@ -77,6 +77,9 @@ const {
|
||||
getGiteaFileContent,
|
||||
uploadGiteaFile,
|
||||
uploadItemsViaGiteaApi,
|
||||
createUploadCancelledError,
|
||||
isUploadCancelledError,
|
||||
throwIfUploadCancelled,
|
||||
listGiteaRepoFiles,
|
||||
deleteGiteaPathsBatched,
|
||||
getGiteaCurrentUser,
|
||||
@@ -290,9 +293,10 @@ const GIT_CLONE_STRATEGIES = [
|
||||
*
|
||||
* @returns {Promise<{ repoGit: object, isEmptyRepo: boolean, strategy: string }>}
|
||||
*/
|
||||
async function cloneForUpload({ authUrl, tmpDir, branch, gitConfig, label = 'Clone' }) {
|
||||
async function cloneForUpload({ authUrl, tmpDir, branch, gitConfig, label = 'Clone', signal }) {
|
||||
const simpleGit = require('simple-git');
|
||||
const config = [...gitConfig, ...GIT_TRANSFER_CONFIG];
|
||||
throwIfUploadCancelled(signal);
|
||||
const safeBranch = sanitizeGitRef(branch || 'HEAD', 'HEAD');
|
||||
|
||||
// Bekannt defekter Server: gar nicht erst versuchen.
|
||||
@@ -313,8 +317,8 @@ async function cloneForUpload({ authUrl, tmpDir, branch, gitConfig, label = 'Clo
|
||||
try {
|
||||
if (attempt > 1) clearDirContents(tmpDir);
|
||||
console.log(`[${label}] Clone-Versuch ${attempt}/${GIT_CLONE_MAX_ATTEMPTS} (${strategy.name}):`, { cloneArgs, tmpDir });
|
||||
await simpleGit({ config }).clone(authUrl, tmpDir, cloneArgs);
|
||||
const repoGit = simpleGit({ baseDir: tmpDir, config });
|
||||
await simpleGit({ config, abort: signal }).clone(authUrl, tmpDir, cloneArgs);
|
||||
const repoGit = simpleGit({ baseDir: tmpDir, config, abort: signal });
|
||||
|
||||
// Ein Clone mit --no-checkout laesst den Index leer. Ohne read-tree wuerde der
|
||||
// folgende Commit den kompletten Bestand des Branches als geloescht markieren.
|
||||
@@ -325,6 +329,11 @@ async function cloneForUpload({ authUrl, tmpDir, branch, gitConfig, label = 'Clo
|
||||
markGitTransportWorking(authUrl);
|
||||
return { repoGit, isEmptyRepo: false, strategy: strategy.name };
|
||||
} catch (cloneErr) {
|
||||
// Abbruch durch den Nutzer: weder wiederholen noch den Server als defekt markieren.
|
||||
if (signal && signal.aborted) {
|
||||
clearDirContents(tmpDir);
|
||||
throw createUploadCancelledError();
|
||||
}
|
||||
const cloneMsg = String(cloneErr);
|
||||
|
||||
// Leeres Repo ohne Commits: lokal initialisieren statt klonen.
|
||||
@@ -332,7 +341,7 @@ async function cloneForUpload({ authUrl, tmpDir, branch, gitConfig, label = 'Clo
|
||||
cloneMsg.includes('did not match') || cloneMsg.includes('Remote branch')) {
|
||||
console.log(`[${label}] Leeres Repo erkannt — initialisiere lokal als "${safeBranch}"`);
|
||||
clearDirContents(tmpDir);
|
||||
const repoGit = simpleGit({ baseDir: tmpDir, config });
|
||||
const repoGit = simpleGit({ baseDir: tmpDir, config, abort: signal });
|
||||
await repoGit.init();
|
||||
|
||||
// git init nutzt init.defaultBranch (haeufig "master"). Ohne diese Korrektur landet
|
||||
@@ -401,11 +410,11 @@ async function stageUploadedFiles(repoGit) {
|
||||
*
|
||||
* @returns {Promise<{ok: boolean, uploaded: string[], failed: Array<{path: string, error: string}>, commits: number, branch: string}>}
|
||||
*/
|
||||
async function uploadItemsViaApiFallback({ token, url, owner, repo, branch, items, message, onProgress, label = 'Upload' }) {
|
||||
async function uploadItemsViaApiFallback({ token, url, owner, repo, branch, items, message, onProgress, label = 'Upload', signal }) {
|
||||
console.warn(`[${label}] Git-Transport nicht nutzbar — weiche auf die Contents-API aus (${items.length} Dateien)`);
|
||||
|
||||
const result = await uploadItemsViaGiteaApi({
|
||||
token, url, owner, repo, branch, items, message, onProgress
|
||||
token, url, owner, repo, branch, items, message, onProgress, signal
|
||||
});
|
||||
|
||||
console.log(`[${label}] API-Upload fertig:`, {
|
||||
@@ -1190,11 +1199,11 @@ function sanitizeGitRef(ref, fallback = 'main') {
|
||||
|
||||
// Startet Git ohne den Hauptprozess zu blockieren. Synchrone Aufrufe (spawnSync) froren
|
||||
// die Oberfläche ein, bis Windows "Anwendung reagiert nicht" meldete.
|
||||
function execGit(args, { cwd, input, env } = {}) {
|
||||
function execGit(args, { cwd, input, env, signal } = {}) {
|
||||
return new Promise((resolve) => {
|
||||
let child;
|
||||
try {
|
||||
child = spawn('git', args, { cwd, env: env || process.env, windowsHide: true });
|
||||
child = spawn('git', args, { cwd, env: env || process.env, windowsHide: true, signal });
|
||||
} catch (e) {
|
||||
resolve({ status: -1, stdout: '', stderr: String(e && e.message || e) });
|
||||
return;
|
||||
@@ -1204,6 +1213,25 @@ function execGit(args, { cwd, input, env } = {}) {
|
||||
child.stdout.on('data', c => out.push(c));
|
||||
child.stderr.on('data', c => err.push(c));
|
||||
child.on('error', e => err.push(Buffer.from(String(e && e.message || e))));
|
||||
|
||||
// Abbruch: unter Windows beendet spawn(signal) nur git.exe, der Netzwerk-Kindprozess
|
||||
// (git-remote-https) läuft weiter und hält die Pipes offen. Deshalb den ganzen
|
||||
// Prozessbaum beenden und sofort zurückkehren, statt auf 'close' zu warten.
|
||||
if (signal) {
|
||||
const onAbort = () => {
|
||||
try {
|
||||
if (process.platform === 'win32' && child.pid) {
|
||||
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
||||
} else {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
} catch (_) {}
|
||||
resolve({ status: -1, stdout: '', stderr: 'aborted' });
|
||||
};
|
||||
if (signal.aborted) onAbort();
|
||||
else signal.addEventListener('abort', onAbort, { once: true });
|
||||
}
|
||||
|
||||
child.on('close', (code) => resolve({
|
||||
status: code,
|
||||
stdout: Buffer.concat(out).toString('utf8'),
|
||||
@@ -1215,7 +1243,9 @@ function execGit(args, { cwd, input, env } = {}) {
|
||||
}
|
||||
|
||||
async function runGitAsync(args, cwd, options = {}) {
|
||||
const res = await execGit(args, { cwd, env: options.env });
|
||||
throwIfUploadCancelled(options.signal);
|
||||
const res = await execGit(args, { cwd, env: options.env, signal: options.signal });
|
||||
throwIfUploadCancelled(options.signal);
|
||||
if (res.status !== 0) {
|
||||
throw new Error(String(res.stderr || res.stdout || 'Git-Fehler').trim());
|
||||
}
|
||||
@@ -1225,7 +1255,7 @@ async function runGitAsync(args, cwd, options = {}) {
|
||||
/* -----------------------------
|
||||
Generic concurrency runner (worker pool)
|
||||
----------------------------- */
|
||||
async function runLimited(tasks, concurrency = DEFAULT_CONCURRENCY, onProgress = null) {
|
||||
async function runLimited(tasks, concurrency = DEFAULT_CONCURRENCY, onProgress = null, signal = null) {
|
||||
const results = new Array(tasks.length);
|
||||
let index = 0;
|
||||
let processed = 0;
|
||||
@@ -1233,6 +1263,7 @@ async function runLimited(tasks, concurrency = DEFAULT_CONCURRENCY, onProgress =
|
||||
|
||||
async function worker() {
|
||||
while (true) {
|
||||
if (signal && signal.aborted) return;
|
||||
const i = index++;
|
||||
if (i >= tasks.length) return;
|
||||
try {
|
||||
@@ -1251,10 +1282,91 @@ async function runLimited(tasks, concurrency = DEFAULT_CONCURRENCY, onProgress =
|
||||
|
||||
const workers = Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(() => worker());
|
||||
await Promise.all(workers);
|
||||
throwIfUploadCancelled(signal);
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
/* -----------------------------
|
||||
Upload-Verlauf
|
||||
Jeder Upload-Handler wird beim Registrieren umhüllt und schreibt Datum, Repo,
|
||||
Dateizahl und Ergebnis in upload-history.json (die letzten 100 Einträge).
|
||||
----------------------------- */
|
||||
const UPLOAD_HISTORY_FILE = ppath.join(app.getPath('userData'), 'upload-history.json');
|
||||
const UPLOAD_HISTORY_MAX = 100;
|
||||
const UPLOAD_HISTORY_CHANNELS = {
|
||||
'upload-paths-to-gitea': 'Drag & Drop',
|
||||
'upload-local-folder-to-gitea': 'Ordner',
|
||||
'upload-gitea-file': 'Dateien',
|
||||
'upload-and-push': 'Ordner/Datei'
|
||||
};
|
||||
|
||||
function readUploadHistory() {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(UPLOAD_HISTORY_FILE, 'utf8'));
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function recordUploadHistory(entry) {
|
||||
try {
|
||||
const history = [entry, ...readUploadHistory()].slice(0, UPLOAD_HISTORY_MAX);
|
||||
fs.writeFileSync(UPLOAD_HISTORY_FILE, JSON.stringify(history, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
console.warn('[UploadHistory] konnte nicht gespeichert werden:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function buildUploadHistoryEntry(kind, data, res, startedAt, thrown) {
|
||||
const results = res && Array.isArray(res.results) ? res.results : null;
|
||||
let status = 'ok';
|
||||
if (thrown || !res || !res.ok) status = res && res.cancelled ? 'abgebrochen' : 'fehler';
|
||||
const error = thrown ? String(thrown.message || thrown) : (res && !res.ok && !res.cancelled ? String(res.error || '') : '');
|
||||
return {
|
||||
at: new Date().toISOString(),
|
||||
kind,
|
||||
repo: data && data.owner && data.repo ? `${data.owner}/${data.repo}` : '',
|
||||
files: results ? results.length : (res && res.singleFile ? 1 : null),
|
||||
failed: res && typeof res.failedCount === 'number' ? res.failedCount : 0,
|
||||
status,
|
||||
error: error.slice(0, 300),
|
||||
durationMs: Date.now() - startedAt
|
||||
};
|
||||
}
|
||||
|
||||
const rawIpcHandle = ipcMain.handle.bind(ipcMain);
|
||||
ipcMain.handle = (channel, handler) => {
|
||||
const kind = UPLOAD_HISTORY_CHANNELS[channel];
|
||||
if (!kind) return rawIpcHandle(channel, handler);
|
||||
return rawIpcHandle(channel, async (event, data) => {
|
||||
const startedAt = Date.now();
|
||||
let res;
|
||||
try {
|
||||
res = await handler(event, data);
|
||||
} catch (e) {
|
||||
recordUploadHistory(buildUploadHistoryEntry(kind, data, null, startedAt, e));
|
||||
throw e;
|
||||
}
|
||||
recordUploadHistory(buildUploadHistoryEntry(kind, data, res, startedAt, null));
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
ipcMain.handle('get-upload-history', () => ({ ok: true, history: readUploadHistory() }));
|
||||
ipcMain.handle('clear-upload-history', () => {
|
||||
try { fs.writeFileSync(UPLOAD_HISTORY_FILE, '[]', 'utf8'); return { ok: true }; }
|
||||
catch (e) { return { ok: false, error: String(e && e.message || e) }; }
|
||||
});
|
||||
|
||||
const gitignoreTemplates = require('./src/features/gitignoreTemplates.js');
|
||||
ipcMain.handle('get-gitignore-templates', () => ({ ok: true, templates: gitignoreTemplates.listTemplates() }));
|
||||
ipcMain.handle('build-gitignore', (_event, data) => ({
|
||||
ok: true,
|
||||
content: gitignoreTemplates.buildGitignore(Array.isArray(data && data.keys) ? data.keys : [])
|
||||
}));
|
||||
|
||||
/* -----------------------------
|
||||
Basic IPC handlers
|
||||
----------------------------- */
|
||||
@@ -2458,6 +2570,8 @@ ipcMain.handle('read-gitea-file', async (event, data) => {
|
||||
});
|
||||
|
||||
ipcMain.handle('upload-gitea-file', async (event, data) => {
|
||||
const uploadAbort = beginCancellableUpload();
|
||||
const signal = uploadAbort.signal;
|
||||
const uploadDebugId = `f-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
@@ -2566,11 +2680,13 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
||||
tmpDir,
|
||||
branch,
|
||||
gitConfig,
|
||||
label: 'FileUpload'
|
||||
label: 'FileUpload',
|
||||
signal
|
||||
});
|
||||
|
||||
// Dateien in den Zielordner kopieren
|
||||
for (const localFile of validFiles) {
|
||||
throwIfUploadCancelled(signal);
|
||||
const fileName = ppath.basename(localFile);
|
||||
const targetPath = destPath ? `${destPath}/${fileName}` : fileName;
|
||||
const destFile = ppath.join(tmpDir, ...targetPath.split('/'));
|
||||
@@ -2579,6 +2695,7 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
||||
results.push({ file: localFile, ok: true, targetPath });
|
||||
}
|
||||
|
||||
throwIfUploadCancelled(signal);
|
||||
await stageUploadedFiles(repoGit);
|
||||
|
||||
let hasChanges = true;
|
||||
@@ -2612,6 +2729,9 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
||||
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
||||
logger.error('upload-gitea-file', `Git upload failed`, { error: String(gitErr), uploadDebugId });
|
||||
|
||||
// Abbruch durch den Nutzer: kein API-Ausweichweg.
|
||||
if (isUploadCancelledError(gitErr) || signal.aborted) throw createUploadCancelledError();
|
||||
|
||||
// Ausweichweg über die REST-API, falls der Git-Transport nicht nutzbar ist.
|
||||
try {
|
||||
const apiItems = validFiles.map(localFile => {
|
||||
@@ -2623,7 +2743,8 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
||||
token, url, owner, repo, branch,
|
||||
items: apiItems,
|
||||
message,
|
||||
label: 'FileUpload'
|
||||
label: 'FileUpload',
|
||||
signal
|
||||
});
|
||||
|
||||
if (apiResult.ok) {
|
||||
@@ -2647,6 +2768,7 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
||||
|
||||
return { ok: false, error: describeApiFallbackFailure(gitErr, apiResult), debugId: uploadDebugId };
|
||||
} catch (apiErr) {
|
||||
if (isUploadCancelledError(apiErr)) throw apiErr;
|
||||
logger.error('upload-gitea-file', 'API-Fallback fehlgeschlagen', { error: String(apiErr), uploadDebugId });
|
||||
return { ok: false, error: describeApiFallbackFailure(gitErr, null) + ` — API-Fallback: ${apiErr.message || String(apiErr)}`, debugId: uploadDebugId };
|
||||
}
|
||||
@@ -2656,8 +2778,11 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
||||
logger.info('upload-gitea-file', 'Gitea upload done', { failedCount, total: results.length, uploadDebugId });
|
||||
return { ok: failedCount === 0, results, failedCount, debugId: uploadDebugId };
|
||||
} catch (e) {
|
||||
if (isUploadCancelledError(e)) return { ok: false, cancelled: true, error: 'Upload abgebrochen', debugId: uploadDebugId };
|
||||
console.error('[UPLOAD_DEBUG][main] upload-gitea-file:fatal', { uploadDebugId, error: String(e) });
|
||||
return { ok: false, error: `${String(e)} (${uploadDebugId})`, debugId: uploadDebugId };
|
||||
} finally {
|
||||
endCancellableUpload(uploadAbort);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2748,7 +2873,38 @@ ipcMain.handle('remove-retry-queue-item', async (event, data) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Gesamtgröße der Quelldateien in Bytes (für die Fortschrittsanzeige).
|
||||
function sumFileSizes(items) {
|
||||
let bytes = 0;
|
||||
for (const it of items) {
|
||||
try { bytes += fs.statSync(it.localFile).size; } catch (_) {}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Abbruch laufender Uploads und Downloads: der Renderer bricht per IPC alle laufenden ab
|
||||
// (es gibt nur einen Fortschrittsbalken mit einem Abbrechen-Knopf).
|
||||
const activeTransferAborts = new Set();
|
||||
|
||||
function beginCancellableUpload() {
|
||||
const controller = new AbortController();
|
||||
activeTransferAborts.add(controller);
|
||||
return controller;
|
||||
}
|
||||
|
||||
function endCancellableUpload(controller) {
|
||||
activeTransferAborts.delete(controller);
|
||||
}
|
||||
|
||||
ipcMain.handle('cancel-upload', () => {
|
||||
if (activeTransferAborts.size === 0) return { ok: false, error: 'no-active-upload' };
|
||||
for (const controller of activeTransferAborts) controller.abort();
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||
const uploadAbort = beginCancellableUpload();
|
||||
const signal = uploadAbort.signal;
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||
@@ -2796,6 +2952,7 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||
|
||||
const total = items.length;
|
||||
if (total === 0) return { ok: true, results: [] };
|
||||
const totalBytes = sumFileSizes(items);
|
||||
|
||||
// Git-basierter Upload: alle Dateien in einem einzigen Commit.
|
||||
// Vermeidet Giteas API-Index-Timing-Probleme (422 SHA) komplett.
|
||||
@@ -2814,7 +2971,7 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||
const gitConfig = gitIdentityConfig();
|
||||
|
||||
// Fortschritt: Start
|
||||
try { event.sender.send('folder-upload-progress', { processed: 0, total, percent: 0 }); } catch (_) {}
|
||||
try { event.sender.send('folder-upload-progress', { processed: 0, total, percent: 0, totalBytes }); } catch (_) {}
|
||||
|
||||
console.log('[FolderUpload] Starte Git-Upload:', { owner, repo, branch, total, destPath, tmpDir });
|
||||
|
||||
@@ -2824,23 +2981,26 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||
tmpDir,
|
||||
branch,
|
||||
gitConfig,
|
||||
label: 'FolderUpload'
|
||||
label: 'FolderUpload',
|
||||
signal
|
||||
});
|
||||
|
||||
// Fortschritt: 30% (nach Clone)
|
||||
try { event.sender.send('folder-upload-progress', { processed: Math.floor(total * 0.3), total, percent: 30 }); } catch (_) {}
|
||||
try { event.sender.send('folder-upload-progress', { processed: Math.floor(total * 0.3), total, percent: 30, totalBytes }); } catch (_) {}
|
||||
|
||||
// Dateien in den Zielordner im geklonten Repo kopieren
|
||||
for (const item of items) {
|
||||
throwIfUploadCancelled(signal);
|
||||
const destFile = ppath.join(tmpDir, ...item.targetPath.split('/'));
|
||||
fs.mkdirSync(ppath.dirname(destFile), { recursive: true });
|
||||
fs.copyFileSync(item.localFile, destFile);
|
||||
}
|
||||
|
||||
// Fortschritt: 60% (nach Kopieren)
|
||||
try { event.sender.send('folder-upload-progress', { processed: Math.floor(total * 0.6), total, percent: 60 }); } catch (_) {}
|
||||
try { event.sender.send('folder-upload-progress', { processed: Math.floor(total * 0.6), total, percent: 60, totalBytes }); } catch (_) {}
|
||||
|
||||
// git add + commit + push
|
||||
throwIfUploadCancelled(signal);
|
||||
await stageUploadedFiles(repoGit);
|
||||
|
||||
let hasChanges = true;
|
||||
@@ -2870,7 +3030,7 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||
}
|
||||
|
||||
// Fortschritt: 100%
|
||||
try { event.sender.send('folder-upload-progress', { processed: total, total, percent: 100 }); } catch (_) {}
|
||||
try { event.sender.send('folder-upload-progress', { processed: total, total, percent: 100, totalBytes }); } catch (_) {}
|
||||
|
||||
// Temp-Ordner nach kurzer Verzögerung bereinigen
|
||||
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5000);
|
||||
@@ -2882,17 +3042,22 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||
console.error('[FolderUpload] Git-Fehler:', String(gitErr));
|
||||
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
||||
|
||||
// Abbruch durch den Nutzer: kein API-Ausweichweg.
|
||||
if (isUploadCancelledError(gitErr) || signal.aborted) throw createUploadCancelledError();
|
||||
|
||||
// Ausweichweg über die REST-API, falls der Git-Transport nicht nutzbar ist.
|
||||
const apiResult = await uploadItemsViaApiFallback({
|
||||
token, url, owner, repo, branch, items,
|
||||
message: `${messagePrefix} - ${folderName}`,
|
||||
label: 'FolderUpload',
|
||||
signal,
|
||||
onProgress: percent => {
|
||||
try {
|
||||
event.sender.send('folder-upload-progress', {
|
||||
processed: Math.floor(total * (percent / 100)),
|
||||
total,
|
||||
percent
|
||||
percent,
|
||||
totalBytes
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -2906,8 +3071,11 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||
return { ok: true, results: successResults, failedCount: 0, viaApi: true };
|
||||
}
|
||||
} catch (e) {
|
||||
if (isUploadCancelledError(e)) return { ok: false, cancelled: true, error: 'Upload abgebrochen' };
|
||||
console.error('upload-local-folder-to-gitea error', e);
|
||||
return { ok: false, error: String(e) };
|
||||
} finally {
|
||||
endCancellableUpload(uploadAbort);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3019,7 +3187,7 @@ async function buildItemsFromPaths(paths, destPath) {
|
||||
}
|
||||
|
||||
// Klont das Repo (shallow), kopiert alle items hinein, commit + push.
|
||||
async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, message, onProgress }) {
|
||||
async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, message, onProgress, signal }) {
|
||||
const rawUrl = url.startsWith('http') ? url : `https://${url}`;
|
||||
const urlObj = new URL(rawUrl.replace(/\/$/, ''));
|
||||
const authUrl = `${urlObj.protocol}//${encodeURIComponent(token)}@${urlObj.host}/${owner}/${repo}.git`;
|
||||
@@ -3034,12 +3202,14 @@ async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, mes
|
||||
tmpDir,
|
||||
branch: safeBranch,
|
||||
gitConfig,
|
||||
label: 'UploadPaths'
|
||||
label: 'UploadPaths',
|
||||
signal
|
||||
});
|
||||
|
||||
if (onProgress) onProgress(30);
|
||||
|
||||
for (const item of items) {
|
||||
throwIfUploadCancelled(signal);
|
||||
const destFile = ppath.join(tmpDir, ...item.targetPath.split('/'));
|
||||
fs.mkdirSync(ppath.dirname(destFile), { recursive: true });
|
||||
fs.copyFileSync(item.localFile, destFile);
|
||||
@@ -3047,6 +3217,7 @@ async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, mes
|
||||
|
||||
if (onProgress) onProgress(60);
|
||||
|
||||
throwIfUploadCancelled(signal);
|
||||
await stageUploadedFiles(repoGit);
|
||||
|
||||
let hasChanges = true;
|
||||
@@ -3072,14 +3243,23 @@ async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, mes
|
||||
} catch (gitErr) {
|
||||
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
||||
|
||||
// Abbruch durch den Nutzer: kein API-Ausweichweg.
|
||||
if (isUploadCancelledError(gitErr) || (signal && signal.aborted)) throw createUploadCancelledError();
|
||||
|
||||
// Ausweichweg über die REST-API, falls der Git-Transport nicht nutzbar ist.
|
||||
const apiResult = await uploadItemsViaApiFallback({
|
||||
token, url, owner, repo, branch: safeBranch, items, message, onProgress,
|
||||
label: 'UploadPaths'
|
||||
label: 'UploadPaths',
|
||||
signal
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
throw new Error(describeApiFallbackFailure(gitErr, apiResult));
|
||||
// Die fehlgeschlagenen Dateien mitgeben, damit der Renderer nur diese wiederholen kann.
|
||||
const failedPaths = new Set((apiResult.failed || []).map(f => f.path));
|
||||
const err = new Error(describeApiFallbackFailure(gitErr, apiResult));
|
||||
err.failedItems = items.filter(it => failedPaths.has(it.targetPath));
|
||||
err.uploadedCount = (apiResult.uploaded || []).length;
|
||||
throw err;
|
||||
}
|
||||
|
||||
return { hasChanges: apiResult.uploaded.length > 0, viaApi: true };
|
||||
@@ -3087,6 +3267,8 @@ async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, mes
|
||||
}
|
||||
|
||||
ipcMain.handle('upload-paths-to-gitea', async (event, data) => {
|
||||
const uploadAbort = beginCancellableUpload();
|
||||
const signal = uploadAbort.signal;
|
||||
const uploadDebugId = `p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
@@ -3102,12 +3284,20 @@ ipcMain.handle('upload-paths-to-gitea', async (event, data) => {
|
||||
const branch = sanitizeGitRef(data.branch || 'HEAD', 'HEAD');
|
||||
const messagePrefix = data.messagePrefix || 'Upload via GUI';
|
||||
const paths = Array.isArray(data.paths) ? data.paths.filter(Boolean) : [];
|
||||
if (paths.length === 0) return { ok: false, error: 'Keine gültigen Pfade übergeben', debugId: uploadDebugId };
|
||||
// Wiederholung: bereits aufgelöste Einträge (Quelldatei + Zielpfad) statt neuer Pfade.
|
||||
const retryItems = Array.isArray(data.items)
|
||||
? data.items.filter(it => it && it.localFile && it.targetPath && fs.existsSync(it.localFile))
|
||||
: null;
|
||||
if (paths.length === 0 && !(retryItems && retryItems.length)) {
|
||||
return { ok: false, error: 'Keine gültigen Pfade übergeben', debugId: uploadDebugId };
|
||||
}
|
||||
|
||||
try { invalidateRepoContentsCache(owner, repo); } catch (_) {}
|
||||
try { caches.repos.invalidate(`${owner}/${repo}`); } catch (_) {}
|
||||
|
||||
const built = await buildItemsFromPaths(paths, destPath);
|
||||
const built = retryItems && retryItems.length
|
||||
? { items: retryItems, skipped: [], ignored: [] }
|
||||
: await buildItemsFromPaths(paths, destPath);
|
||||
const skipped = built.skipped;
|
||||
const excluded = new Set(Array.isArray(data.excludeFiles) ? data.excludeFiles : []);
|
||||
const items = excluded.size ? built.items.filter(it => !excluded.has(it.localFile)) : built.items;
|
||||
@@ -3116,8 +3306,9 @@ ipcMain.handle('upload-paths-to-gitea', async (event, data) => {
|
||||
}
|
||||
|
||||
const total = items.length;
|
||||
const totalBytes = sumFileSizes(items);
|
||||
const onProgress = (percent) => {
|
||||
try { event.sender.send('folder-upload-progress', { processed: Math.floor(total * percent / 100), total, percent }); } catch (_) {}
|
||||
try { event.sender.send('folder-upload-progress', { processed: Math.floor(total * percent / 100), total, percent, totalBytes }); } catch (_) {}
|
||||
};
|
||||
onProgress(0);
|
||||
|
||||
@@ -3125,15 +3316,25 @@ ipcMain.handle('upload-paths-to-gitea', async (event, data) => {
|
||||
await gitPushItemsToGitea({
|
||||
token, url, owner, repo, branch, items,
|
||||
message: `${messagePrefix} (${total} Datei${total === 1 ? '' : 'en'})`,
|
||||
onProgress
|
||||
onProgress,
|
||||
signal
|
||||
});
|
||||
console.log('[UploadPaths] done', { uploadDebugId, total });
|
||||
|
||||
const results = items.map(it => ({ ok: true, localFile: it.localFile, targetPath: it.targetPath }));
|
||||
return { ok: true, results, failedCount: 0, skipped, debugId: uploadDebugId };
|
||||
} catch (e) {
|
||||
if (isUploadCancelledError(e)) return { ok: false, cancelled: true, error: 'Upload abgebrochen', debugId: uploadDebugId };
|
||||
console.error('[UploadPaths] fatal', { uploadDebugId, error: String(e) });
|
||||
return { ok: false, error: String(e && e.message ? e.message : e), debugId: uploadDebugId };
|
||||
return {
|
||||
ok: false,
|
||||
error: String(e && e.message ? e.message : e),
|
||||
retryItems: e && Array.isArray(e.failedItems) && e.failedItems.length ? e.failedItems : undefined,
|
||||
uploadedCount: e && typeof e.uploadedCount === 'number' ? e.uploadedCount : undefined,
|
||||
debugId: uploadDebugId
|
||||
};
|
||||
} finally {
|
||||
endCancellableUpload(uploadAbort);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3159,6 +3360,8 @@ ipcMain.handle('download-gitea-file', async (event, data) => {
|
||||
});
|
||||
|
||||
ipcMain.handle('download-gitea-folder', async (event, data) => {
|
||||
const downloadAbort = beginCancellableUpload();
|
||||
const signal = downloadAbort.signal;
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||
@@ -3175,6 +3378,7 @@ ipcMain.handle('download-gitea-folder', async (event, data) => {
|
||||
|
||||
const allFiles = [];
|
||||
async function gather(pathInRepo) {
|
||||
throwIfUploadCancelled(signal);
|
||||
const _r = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'HEAD' });
|
||||
const items = _r.items || _r;
|
||||
for (const item of items) {
|
||||
@@ -3199,12 +3403,15 @@ ipcMain.handle('download-gitea-folder', async (event, data) => {
|
||||
try { event.sender.send('folder-download-progress', { processed, total: t, percent: Math.round((processed / t) * 100) }); } catch (_) {}
|
||||
};
|
||||
|
||||
const results = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total));
|
||||
const results = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total), signal);
|
||||
const savedFiles = results.filter(r => r.ok).map(r => r.result);
|
||||
return { ok: true, savedTo: destBase, files: savedFiles };
|
||||
} catch (e) {
|
||||
if (isUploadCancelledError(e)) return { ok: false, cancelled: true, error: 'Download abgebrochen' };
|
||||
console.error('download-gitea-folder error', e);
|
||||
return { ok: false, error: String(e) };
|
||||
} finally {
|
||||
endCancellableUpload(downloadAbort);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3386,6 +3593,8 @@ ipcMain.handle('delete-gitea-repo', async (event, data) => {
|
||||
});
|
||||
|
||||
ipcMain.handle('upload-and-push', async (event, data) => {
|
||||
const uploadAbort = beginCancellableUpload();
|
||||
const signal = uploadAbort.signal;
|
||||
const uploadDebugId = `u-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
try {
|
||||
if (!data || !data.localFolder || !fs.existsSync(data.localFolder)) return { ok: false, error: `local-folder-not-found (${uploadDebugId})` };
|
||||
@@ -3422,7 +3631,8 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
||||
GIT_PAGER: 'cat',
|
||||
PAGER: 'cat'
|
||||
},
|
||||
maxBuffer: 10 * 1024 * 1024
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
signal
|
||||
};
|
||||
|
||||
// Prüfen ob es eine Datei oder ein Ordner ist
|
||||
@@ -3460,6 +3670,7 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
||||
});
|
||||
return { ok: true, usedGit: false, singleFile: true, uploaded: result, debugId: uploadDebugId };
|
||||
} catch (e) {
|
||||
throwIfUploadCancelled(signal);
|
||||
console.error('[UPLOAD_DEBUG][main] single-file:api-error', { uploadDebugId, error: String(e && e.message ? e.message : e) });
|
||||
|
||||
// FALLBACK: Wenn API fehlschlägt (z.B. 422 SHA Fehler), nutze Git
|
||||
@@ -3529,6 +3740,7 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
||||
} catch (gitErr) {
|
||||
console.error('[UPLOAD_DEBUG][main] single-file:git-fallback-error', { uploadDebugId, error: String(gitErr) });
|
||||
if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
if (isUploadCancelledError(gitErr)) throw gitErr;
|
||||
return { ok: false, error: `API failed: ${e.message}. Git fallback failed: ${String(gitErr)} (${uploadDebugId})` };
|
||||
}
|
||||
}
|
||||
@@ -3622,6 +3834,7 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
||||
throw gitErr;
|
||||
}
|
||||
} catch (gitErr) {
|
||||
if (isUploadCancelledError(gitErr) || signal.aborted) throw createUploadCancelledError();
|
||||
console.warn('[UPLOAD_DEBUG][main] directory-upload:git-failed-fallback-to-api', { uploadDebugId, error: String(gitErr && gitErr.message ? gitErr.message : gitErr) });
|
||||
// -> weiter zum API-Fallback unten
|
||||
}
|
||||
@@ -3686,13 +3899,16 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
||||
try { event.sender.send('folder-upload-progress', { processed, total: t, percent: Math.round((processed / t) * 100) }); } catch (_) {}
|
||||
};
|
||||
|
||||
const uploadResults = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total));
|
||||
const uploadResults = await runLimited(tasks, concurrency, (proc, t) => onProgress(proc, total), signal);
|
||||
const failedCount = uploadResults.filter(r => !r.ok).length;
|
||||
console.log('[UPLOAD_DEBUG][main] api-fallback:done', { uploadDebugId, failedCount, total: uploadResults.length });
|
||||
return { ok: failedCount === 0, usedGit: false, results: uploadResults, debugId: uploadDebugId, failedCount };
|
||||
} catch (e) {
|
||||
if (isUploadCancelledError(e)) return { ok: false, cancelled: true, error: 'Upload abgebrochen', debugId: uploadDebugId };
|
||||
console.error('[UPLOAD_DEBUG][main] upload-and-push:fatal', { uploadDebugId, error: String(e) });
|
||||
return { ok: false, error: `${String(e)} (${uploadDebugId})`, debugId: uploadDebugId };
|
||||
} finally {
|
||||
endCancellableUpload(uploadAbort);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3714,6 +3930,8 @@ ipcMain.handle('get-path-type', async (_event, filePath) => {
|
||||
});
|
||||
|
||||
ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
||||
const batchAbort = beginCancellableUpload();
|
||||
const signal = batchAbort.signal;
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
if (!credentials || !credentials.giteaToken || !credentials.giteaURL) {
|
||||
@@ -3745,7 +3963,9 @@ ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
||||
};
|
||||
|
||||
const results = [];
|
||||
let cancelled = false;
|
||||
for (let i = 0; i < repos.length; i++) {
|
||||
if (signal.aborted) { cancelled = true; break; }
|
||||
const item = repos[i];
|
||||
const progressBase = { index: i + 1, total: repos.length, repo: item.id, action };
|
||||
sendProgress({ ...progressBase, status: 'running' });
|
||||
@@ -3777,7 +3997,15 @@ ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
await runGitAsync(['clone', '--depth', '1', authCloneUrl, repoDir], process.cwd(), { stdio: 'ignore' });
|
||||
try {
|
||||
await runGitAsync(['clone', '--depth', '1', authCloneUrl, repoDir], process.cwd(), { stdio: 'ignore', signal });
|
||||
} catch (cloneErr) {
|
||||
// Abgebrochener Clone: den angelegten Teilordner entfernen (existierte vorher nicht).
|
||||
if (isUploadCancelledError(cloneErr)) {
|
||||
try { fs.rmSync(repoDir, { recursive: true, force: true }); } catch (_) {}
|
||||
}
|
||||
throw cloneErr;
|
||||
}
|
||||
results.push({ repo: item.id, ok: true, message: `Geklont nach ${repoDir}` });
|
||||
} else if (action === 'create-tag') {
|
||||
const tag = String(options.tag || '').trim();
|
||||
@@ -3822,6 +4050,7 @@ ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
||||
|
||||
sendProgress({ ...progressBase, status: 'ok' });
|
||||
} catch (e) {
|
||||
if (isUploadCancelledError(e)) { cancelled = true; break; }
|
||||
const msg = mapIpcError(e);
|
||||
results.push({ repo: item.id, ok: false, error: msg });
|
||||
sendProgress({ ...progressBase, status: 'error', error: msg });
|
||||
@@ -3833,12 +4062,15 @@ ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
||||
return {
|
||||
ok: true,
|
||||
action,
|
||||
summary: { total: results.length, success, failed },
|
||||
cancelled,
|
||||
summary: { total: repos.length, done: results.length, success, failed },
|
||||
results
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('run-batch-repo-action error', e);
|
||||
return { ok: false, error: String(e) };
|
||||
} finally {
|
||||
endCancellableUpload(batchAbort);
|
||||
}
|
||||
});
|
||||
/* ================================
|
||||
|
||||
@@ -39,6 +39,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
||||
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
||||
uploadPathsToGitea: (data) => ipcRenderer.invoke('upload-paths-to-gitea', data),
|
||||
cancelUpload: () => ipcRenderer.invoke('cancel-upload'),
|
||||
getUploadHistory: () => ipcRenderer.invoke('get-upload-history'),
|
||||
clearUploadHistory: () => ipcRenderer.invoke('clear-upload-history'),
|
||||
getGitignoreTemplates: () => ipcRenderer.invoke('get-gitignore-templates'),
|
||||
buildGitignore: (data) => ipcRenderer.invoke('build-gitignore', data),
|
||||
|
||||
// Repository & Git Management
|
||||
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
||||
|
||||
Reference in New Issue
Block a user