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,
|
getGiteaFileContent,
|
||||||
uploadGiteaFile,
|
uploadGiteaFile,
|
||||||
uploadItemsViaGiteaApi,
|
uploadItemsViaGiteaApi,
|
||||||
|
createUploadCancelledError,
|
||||||
|
isUploadCancelledError,
|
||||||
|
throwIfUploadCancelled,
|
||||||
listGiteaRepoFiles,
|
listGiteaRepoFiles,
|
||||||
deleteGiteaPathsBatched,
|
deleteGiteaPathsBatched,
|
||||||
getGiteaCurrentUser,
|
getGiteaCurrentUser,
|
||||||
@@ -290,9 +293,10 @@ const GIT_CLONE_STRATEGIES = [
|
|||||||
*
|
*
|
||||||
* @returns {Promise<{ repoGit: object, isEmptyRepo: boolean, strategy: string }>}
|
* @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 simpleGit = require('simple-git');
|
||||||
const config = [...gitConfig, ...GIT_TRANSFER_CONFIG];
|
const config = [...gitConfig, ...GIT_TRANSFER_CONFIG];
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
const safeBranch = sanitizeGitRef(branch || 'HEAD', 'HEAD');
|
const safeBranch = sanitizeGitRef(branch || 'HEAD', 'HEAD');
|
||||||
|
|
||||||
// Bekannt defekter Server: gar nicht erst versuchen.
|
// Bekannt defekter Server: gar nicht erst versuchen.
|
||||||
@@ -313,8 +317,8 @@ async function cloneForUpload({ authUrl, tmpDir, branch, gitConfig, label = 'Clo
|
|||||||
try {
|
try {
|
||||||
if (attempt > 1) clearDirContents(tmpDir);
|
if (attempt > 1) clearDirContents(tmpDir);
|
||||||
console.log(`[${label}] Clone-Versuch ${attempt}/${GIT_CLONE_MAX_ATTEMPTS} (${strategy.name}):`, { cloneArgs, tmpDir });
|
console.log(`[${label}] Clone-Versuch ${attempt}/${GIT_CLONE_MAX_ATTEMPTS} (${strategy.name}):`, { cloneArgs, tmpDir });
|
||||||
await simpleGit({ config }).clone(authUrl, tmpDir, cloneArgs);
|
await simpleGit({ config, abort: signal }).clone(authUrl, tmpDir, cloneArgs);
|
||||||
const repoGit = simpleGit({ baseDir: tmpDir, config });
|
const repoGit = simpleGit({ baseDir: tmpDir, config, abort: signal });
|
||||||
|
|
||||||
// Ein Clone mit --no-checkout laesst den Index leer. Ohne read-tree wuerde der
|
// Ein Clone mit --no-checkout laesst den Index leer. Ohne read-tree wuerde der
|
||||||
// folgende Commit den kompletten Bestand des Branches als geloescht markieren.
|
// 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);
|
markGitTransportWorking(authUrl);
|
||||||
return { repoGit, isEmptyRepo: false, strategy: strategy.name };
|
return { repoGit, isEmptyRepo: false, strategy: strategy.name };
|
||||||
} catch (cloneErr) {
|
} 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);
|
const cloneMsg = String(cloneErr);
|
||||||
|
|
||||||
// Leeres Repo ohne Commits: lokal initialisieren statt klonen.
|
// 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')) {
|
cloneMsg.includes('did not match') || cloneMsg.includes('Remote branch')) {
|
||||||
console.log(`[${label}] Leeres Repo erkannt — initialisiere lokal als "${safeBranch}"`);
|
console.log(`[${label}] Leeres Repo erkannt — initialisiere lokal als "${safeBranch}"`);
|
||||||
clearDirContents(tmpDir);
|
clearDirContents(tmpDir);
|
||||||
const repoGit = simpleGit({ baseDir: tmpDir, config });
|
const repoGit = simpleGit({ baseDir: tmpDir, config, abort: signal });
|
||||||
await repoGit.init();
|
await repoGit.init();
|
||||||
|
|
||||||
// git init nutzt init.defaultBranch (haeufig "master"). Ohne diese Korrektur landet
|
// 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}>}
|
* @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)`);
|
console.warn(`[${label}] Git-Transport nicht nutzbar — weiche auf die Contents-API aus (${items.length} Dateien)`);
|
||||||
|
|
||||||
const result = await uploadItemsViaGiteaApi({
|
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:`, {
|
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
|
// Startet Git ohne den Hauptprozess zu blockieren. Synchrone Aufrufe (spawnSync) froren
|
||||||
// die Oberfläche ein, bis Windows "Anwendung reagiert nicht" meldete.
|
// 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) => {
|
return new Promise((resolve) => {
|
||||||
let child;
|
let child;
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
resolve({ status: -1, stdout: '', stderr: String(e && e.message || e) });
|
resolve({ status: -1, stdout: '', stderr: String(e && e.message || e) });
|
||||||
return;
|
return;
|
||||||
@@ -1204,6 +1213,25 @@ function execGit(args, { cwd, input, env } = {}) {
|
|||||||
child.stdout.on('data', c => out.push(c));
|
child.stdout.on('data', c => out.push(c));
|
||||||
child.stderr.on('data', c => err.push(c));
|
child.stderr.on('data', c => err.push(c));
|
||||||
child.on('error', e => err.push(Buffer.from(String(e && e.message || e))));
|
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({
|
child.on('close', (code) => resolve({
|
||||||
status: code,
|
status: code,
|
||||||
stdout: Buffer.concat(out).toString('utf8'),
|
stdout: Buffer.concat(out).toString('utf8'),
|
||||||
@@ -1215,7 +1243,9 @@ function execGit(args, { cwd, input, env } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runGitAsync(args, cwd, options = {}) {
|
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) {
|
if (res.status !== 0) {
|
||||||
throw new Error(String(res.stderr || res.stdout || 'Git-Fehler').trim());
|
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)
|
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);
|
const results = new Array(tasks.length);
|
||||||
let index = 0;
|
let index = 0;
|
||||||
let processed = 0;
|
let processed = 0;
|
||||||
@@ -1233,6 +1263,7 @@ async function runLimited(tasks, concurrency = DEFAULT_CONCURRENCY, onProgress =
|
|||||||
|
|
||||||
async function worker() {
|
async function worker() {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
if (signal && signal.aborted) return;
|
||||||
const i = index++;
|
const i = index++;
|
||||||
if (i >= tasks.length) return;
|
if (i >= tasks.length) return;
|
||||||
try {
|
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());
|
const workers = Array(Math.max(1, Math.min(concurrency, tasks.length))).fill(0).map(() => worker());
|
||||||
await Promise.all(workers);
|
await Promise.all(workers);
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
return results;
|
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
|
Basic IPC handlers
|
||||||
----------------------------- */
|
----------------------------- */
|
||||||
@@ -2458,6 +2570,8 @@ ipcMain.handle('read-gitea-file', async (event, data) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('upload-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)}`;
|
const uploadDebugId = `f-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
try {
|
try {
|
||||||
const credentials = readCredentials();
|
const credentials = readCredentials();
|
||||||
@@ -2566,11 +2680,13 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
|||||||
tmpDir,
|
tmpDir,
|
||||||
branch,
|
branch,
|
||||||
gitConfig,
|
gitConfig,
|
||||||
label: 'FileUpload'
|
label: 'FileUpload',
|
||||||
|
signal
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dateien in den Zielordner kopieren
|
// Dateien in den Zielordner kopieren
|
||||||
for (const localFile of validFiles) {
|
for (const localFile of validFiles) {
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
const fileName = ppath.basename(localFile);
|
const fileName = ppath.basename(localFile);
|
||||||
const targetPath = destPath ? `${destPath}/${fileName}` : fileName;
|
const targetPath = destPath ? `${destPath}/${fileName}` : fileName;
|
||||||
const destFile = ppath.join(tmpDir, ...targetPath.split('/'));
|
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 });
|
results.push({ file: localFile, ok: true, targetPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
await stageUploadedFiles(repoGit);
|
await stageUploadedFiles(repoGit);
|
||||||
|
|
||||||
let hasChanges = true;
|
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 (_) {}
|
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 });
|
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.
|
// Ausweichweg über die REST-API, falls der Git-Transport nicht nutzbar ist.
|
||||||
try {
|
try {
|
||||||
const apiItems = validFiles.map(localFile => {
|
const apiItems = validFiles.map(localFile => {
|
||||||
@@ -2623,7 +2743,8 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
|||||||
token, url, owner, repo, branch,
|
token, url, owner, repo, branch,
|
||||||
items: apiItems,
|
items: apiItems,
|
||||||
message,
|
message,
|
||||||
label: 'FileUpload'
|
label: 'FileUpload',
|
||||||
|
signal
|
||||||
});
|
});
|
||||||
|
|
||||||
if (apiResult.ok) {
|
if (apiResult.ok) {
|
||||||
@@ -2647,6 +2768,7 @@ ipcMain.handle('upload-gitea-file', async (event, data) => {
|
|||||||
|
|
||||||
return { ok: false, error: describeApiFallbackFailure(gitErr, apiResult), debugId: uploadDebugId };
|
return { ok: false, error: describeApiFallbackFailure(gitErr, apiResult), debugId: uploadDebugId };
|
||||||
} catch (apiErr) {
|
} catch (apiErr) {
|
||||||
|
if (isUploadCancelledError(apiErr)) throw apiErr;
|
||||||
logger.error('upload-gitea-file', 'API-Fallback fehlgeschlagen', { error: String(apiErr), uploadDebugId });
|
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 };
|
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 });
|
logger.info('upload-gitea-file', 'Gitea upload done', { failedCount, total: results.length, uploadDebugId });
|
||||||
return { ok: failedCount === 0, results, failedCount, debugId: uploadDebugId };
|
return { ok: failedCount === 0, results, failedCount, debugId: uploadDebugId };
|
||||||
} catch (e) {
|
} 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) });
|
console.error('[UPLOAD_DEBUG][main] upload-gitea-file:fatal', { uploadDebugId, error: String(e) });
|
||||||
return { ok: false, error: `${String(e)} (${uploadDebugId})`, debugId: uploadDebugId };
|
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) => {
|
ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||||
|
const uploadAbort = beginCancellableUpload();
|
||||||
|
const signal = uploadAbort.signal;
|
||||||
try {
|
try {
|
||||||
const credentials = readCredentials();
|
const credentials = readCredentials();
|
||||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
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;
|
const total = items.length;
|
||||||
if (total === 0) return { ok: true, results: [] };
|
if (total === 0) return { ok: true, results: [] };
|
||||||
|
const totalBytes = sumFileSizes(items);
|
||||||
|
|
||||||
// Git-basierter Upload: alle Dateien in einem einzigen Commit.
|
// Git-basierter Upload: alle Dateien in einem einzigen Commit.
|
||||||
// Vermeidet Giteas API-Index-Timing-Probleme (422 SHA) komplett.
|
// 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();
|
const gitConfig = gitIdentityConfig();
|
||||||
|
|
||||||
// Fortschritt: Start
|
// 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 });
|
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,
|
tmpDir,
|
||||||
branch,
|
branch,
|
||||||
gitConfig,
|
gitConfig,
|
||||||
label: 'FolderUpload'
|
label: 'FolderUpload',
|
||||||
|
signal
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fortschritt: 30% (nach Clone)
|
// 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
|
// Dateien in den Zielordner im geklonten Repo kopieren
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
const destFile = ppath.join(tmpDir, ...item.targetPath.split('/'));
|
const destFile = ppath.join(tmpDir, ...item.targetPath.split('/'));
|
||||||
fs.mkdirSync(ppath.dirname(destFile), { recursive: true });
|
fs.mkdirSync(ppath.dirname(destFile), { recursive: true });
|
||||||
fs.copyFileSync(item.localFile, destFile);
|
fs.copyFileSync(item.localFile, destFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fortschritt: 60% (nach Kopieren)
|
// 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
|
// git add + commit + push
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
await stageUploadedFiles(repoGit);
|
await stageUploadedFiles(repoGit);
|
||||||
|
|
||||||
let hasChanges = true;
|
let hasChanges = true;
|
||||||
@@ -2870,7 +3030,7 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fortschritt: 100%
|
// 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
|
// Temp-Ordner nach kurzer Verzögerung bereinigen
|
||||||
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5000);
|
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));
|
console.error('[FolderUpload] Git-Fehler:', String(gitErr));
|
||||||
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
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.
|
// Ausweichweg über die REST-API, falls der Git-Transport nicht nutzbar ist.
|
||||||
const apiResult = await uploadItemsViaApiFallback({
|
const apiResult = await uploadItemsViaApiFallback({
|
||||||
token, url, owner, repo, branch, items,
|
token, url, owner, repo, branch, items,
|
||||||
message: `${messagePrefix} - ${folderName}`,
|
message: `${messagePrefix} - ${folderName}`,
|
||||||
label: 'FolderUpload',
|
label: 'FolderUpload',
|
||||||
|
signal,
|
||||||
onProgress: percent => {
|
onProgress: percent => {
|
||||||
try {
|
try {
|
||||||
event.sender.send('folder-upload-progress', {
|
event.sender.send('folder-upload-progress', {
|
||||||
processed: Math.floor(total * (percent / 100)),
|
processed: Math.floor(total * (percent / 100)),
|
||||||
total,
|
total,
|
||||||
percent
|
percent,
|
||||||
|
totalBytes
|
||||||
});
|
});
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
@@ -2906,8 +3071,11 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
|||||||
return { ok: true, results: successResults, failedCount: 0, viaApi: true };
|
return { ok: true, results: successResults, failedCount: 0, viaApi: true };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (isUploadCancelledError(e)) return { ok: false, cancelled: true, error: 'Upload abgebrochen' };
|
||||||
console.error('upload-local-folder-to-gitea error', e);
|
console.error('upload-local-folder-to-gitea error', e);
|
||||||
return { ok: false, error: String(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.
|
// 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 rawUrl = url.startsWith('http') ? url : `https://${url}`;
|
||||||
const urlObj = new URL(rawUrl.replace(/\/$/, ''));
|
const urlObj = new URL(rawUrl.replace(/\/$/, ''));
|
||||||
const authUrl = `${urlObj.protocol}//${encodeURIComponent(token)}@${urlObj.host}/${owner}/${repo}.git`;
|
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,
|
tmpDir,
|
||||||
branch: safeBranch,
|
branch: safeBranch,
|
||||||
gitConfig,
|
gitConfig,
|
||||||
label: 'UploadPaths'
|
label: 'UploadPaths',
|
||||||
|
signal
|
||||||
});
|
});
|
||||||
|
|
||||||
if (onProgress) onProgress(30);
|
if (onProgress) onProgress(30);
|
||||||
|
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
const destFile = ppath.join(tmpDir, ...item.targetPath.split('/'));
|
const destFile = ppath.join(tmpDir, ...item.targetPath.split('/'));
|
||||||
fs.mkdirSync(ppath.dirname(destFile), { recursive: true });
|
fs.mkdirSync(ppath.dirname(destFile), { recursive: true });
|
||||||
fs.copyFileSync(item.localFile, destFile);
|
fs.copyFileSync(item.localFile, destFile);
|
||||||
@@ -3047,6 +3217,7 @@ async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, mes
|
|||||||
|
|
||||||
if (onProgress) onProgress(60);
|
if (onProgress) onProgress(60);
|
||||||
|
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
await stageUploadedFiles(repoGit);
|
await stageUploadedFiles(repoGit);
|
||||||
|
|
||||||
let hasChanges = true;
|
let hasChanges = true;
|
||||||
@@ -3072,14 +3243,23 @@ async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, mes
|
|||||||
} catch (gitErr) {
|
} catch (gitErr) {
|
||||||
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
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.
|
// Ausweichweg über die REST-API, falls der Git-Transport nicht nutzbar ist.
|
||||||
const apiResult = await uploadItemsViaApiFallback({
|
const apiResult = await uploadItemsViaApiFallback({
|
||||||
token, url, owner, repo, branch: safeBranch, items, message, onProgress,
|
token, url, owner, repo, branch: safeBranch, items, message, onProgress,
|
||||||
label: 'UploadPaths'
|
label: 'UploadPaths',
|
||||||
|
signal
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!apiResult.ok) {
|
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 };
|
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) => {
|
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)}`;
|
const uploadDebugId = `p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
try {
|
try {
|
||||||
const credentials = readCredentials();
|
const credentials = readCredentials();
|
||||||
@@ -3102,12 +3284,20 @@ ipcMain.handle('upload-paths-to-gitea', async (event, data) => {
|
|||||||
const branch = sanitizeGitRef(data.branch || 'HEAD', 'HEAD');
|
const branch = sanitizeGitRef(data.branch || 'HEAD', 'HEAD');
|
||||||
const messagePrefix = data.messagePrefix || 'Upload via GUI';
|
const messagePrefix = data.messagePrefix || 'Upload via GUI';
|
||||||
const paths = Array.isArray(data.paths) ? data.paths.filter(Boolean) : [];
|
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 { invalidateRepoContentsCache(owner, repo); } catch (_) {}
|
||||||
try { caches.repos.invalidate(`${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 skipped = built.skipped;
|
||||||
const excluded = new Set(Array.isArray(data.excludeFiles) ? data.excludeFiles : []);
|
const excluded = new Set(Array.isArray(data.excludeFiles) ? data.excludeFiles : []);
|
||||||
const items = excluded.size ? built.items.filter(it => !excluded.has(it.localFile)) : built.items;
|
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 total = items.length;
|
||||||
|
const totalBytes = sumFileSizes(items);
|
||||||
const onProgress = (percent) => {
|
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);
|
onProgress(0);
|
||||||
|
|
||||||
@@ -3125,15 +3316,25 @@ ipcMain.handle('upload-paths-to-gitea', async (event, data) => {
|
|||||||
await gitPushItemsToGitea({
|
await gitPushItemsToGitea({
|
||||||
token, url, owner, repo, branch, items,
|
token, url, owner, repo, branch, items,
|
||||||
message: `${messagePrefix} (${total} Datei${total === 1 ? '' : 'en'})`,
|
message: `${messagePrefix} (${total} Datei${total === 1 ? '' : 'en'})`,
|
||||||
onProgress
|
onProgress,
|
||||||
|
signal
|
||||||
});
|
});
|
||||||
console.log('[UploadPaths] done', { uploadDebugId, total });
|
console.log('[UploadPaths] done', { uploadDebugId, total });
|
||||||
|
|
||||||
const results = items.map(it => ({ ok: true, localFile: it.localFile, targetPath: it.targetPath }));
|
const results = items.map(it => ({ ok: true, localFile: it.localFile, targetPath: it.targetPath }));
|
||||||
return { ok: true, results, failedCount: 0, skipped, debugId: uploadDebugId };
|
return { ok: true, results, failedCount: 0, skipped, debugId: uploadDebugId };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (isUploadCancelledError(e)) return { ok: false, cancelled: true, error: 'Upload abgebrochen', debugId: uploadDebugId };
|
||||||
console.error('[UploadPaths] fatal', { uploadDebugId, error: String(e) });
|
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) => {
|
ipcMain.handle('download-gitea-folder', async (event, data) => {
|
||||||
|
const downloadAbort = beginCancellableUpload();
|
||||||
|
const signal = downloadAbort.signal;
|
||||||
try {
|
try {
|
||||||
const credentials = readCredentials();
|
const credentials = readCredentials();
|
||||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||||
@@ -3175,6 +3378,7 @@ ipcMain.handle('download-gitea-folder', async (event, data) => {
|
|||||||
|
|
||||||
const allFiles = [];
|
const allFiles = [];
|
||||||
async function gather(pathInRepo) {
|
async function gather(pathInRepo) {
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
const _r = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'HEAD' });
|
const _r = await getGiteaRepoContents({ token, url, owner, repo, path: pathInRepo, ref: data.ref || 'HEAD' });
|
||||||
const items = _r.items || _r;
|
const items = _r.items || _r;
|
||||||
for (const item of items) {
|
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 (_) {}
|
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);
|
const savedFiles = results.filter(r => r.ok).map(r => r.result);
|
||||||
return { ok: true, savedTo: destBase, files: savedFiles };
|
return { ok: true, savedTo: destBase, files: savedFiles };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (isUploadCancelledError(e)) return { ok: false, cancelled: true, error: 'Download abgebrochen' };
|
||||||
console.error('download-gitea-folder error', e);
|
console.error('download-gitea-folder error', e);
|
||||||
return { ok: false, error: String(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) => {
|
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)}`;
|
const uploadDebugId = `u-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
try {
|
try {
|
||||||
if (!data || !data.localFolder || !fs.existsSync(data.localFolder)) return { ok: false, error: `local-folder-not-found (${uploadDebugId})` };
|
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',
|
GIT_PAGER: 'cat',
|
||||||
PAGER: 'cat'
|
PAGER: 'cat'
|
||||||
},
|
},
|
||||||
maxBuffer: 10 * 1024 * 1024
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
signal
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prüfen ob es eine Datei oder ein Ordner ist
|
// 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 };
|
return { ok: true, usedGit: false, singleFile: true, uploaded: result, debugId: uploadDebugId };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
throwIfUploadCancelled(signal);
|
||||||
console.error('[UPLOAD_DEBUG][main] single-file:api-error', { uploadDebugId, error: String(e && e.message ? e.message : e) });
|
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
|
// 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) {
|
} catch (gitErr) {
|
||||||
console.error('[UPLOAD_DEBUG][main] single-file:git-fallback-error', { uploadDebugId, error: String(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 (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})` };
|
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;
|
throw gitErr;
|
||||||
}
|
}
|
||||||
} catch (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) });
|
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
|
// -> 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 (_) {}
|
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;
|
const failedCount = uploadResults.filter(r => !r.ok).length;
|
||||||
console.log('[UPLOAD_DEBUG][main] api-fallback:done', { uploadDebugId, failedCount, total: uploadResults.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 };
|
return { ok: failedCount === 0, usedGit: false, results: uploadResults, debugId: uploadDebugId, failedCount };
|
||||||
} catch (e) {
|
} 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) });
|
console.error('[UPLOAD_DEBUG][main] upload-and-push:fatal', { uploadDebugId, error: String(e) });
|
||||||
return { ok: false, error: `${String(e)} (${uploadDebugId})`, debugId: uploadDebugId };
|
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) => {
|
ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
||||||
|
const batchAbort = beginCancellableUpload();
|
||||||
|
const signal = batchAbort.signal;
|
||||||
try {
|
try {
|
||||||
const credentials = readCredentials();
|
const credentials = readCredentials();
|
||||||
if (!credentials || !credentials.giteaToken || !credentials.giteaURL) {
|
if (!credentials || !credentials.giteaToken || !credentials.giteaURL) {
|
||||||
@@ -3745,7 +3963,9 @@ ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
let cancelled = false;
|
||||||
for (let i = 0; i < repos.length; i++) {
|
for (let i = 0; i < repos.length; i++) {
|
||||||
|
if (signal.aborted) { cancelled = true; break; }
|
||||||
const item = repos[i];
|
const item = repos[i];
|
||||||
const progressBase = { index: i + 1, total: repos.length, repo: item.id, action };
|
const progressBase = { index: i + 1, total: repos.length, repo: item.id, action };
|
||||||
sendProgress({ ...progressBase, status: 'running' });
|
sendProgress({ ...progressBase, status: 'running' });
|
||||||
@@ -3777,7 +3997,15 @@ ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
|||||||
}
|
}
|
||||||
} catch (_) {}
|
} 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}` });
|
results.push({ repo: item.id, ok: true, message: `Geklont nach ${repoDir}` });
|
||||||
} else if (action === 'create-tag') {
|
} else if (action === 'create-tag') {
|
||||||
const tag = String(options.tag || '').trim();
|
const tag = String(options.tag || '').trim();
|
||||||
@@ -3822,6 +4050,7 @@ ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
|||||||
|
|
||||||
sendProgress({ ...progressBase, status: 'ok' });
|
sendProgress({ ...progressBase, status: 'ok' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (isUploadCancelledError(e)) { cancelled = true; break; }
|
||||||
const msg = mapIpcError(e);
|
const msg = mapIpcError(e);
|
||||||
results.push({ repo: item.id, ok: false, error: msg });
|
results.push({ repo: item.id, ok: false, error: msg });
|
||||||
sendProgress({ ...progressBase, status: 'error', error: msg });
|
sendProgress({ ...progressBase, status: 'error', error: msg });
|
||||||
@@ -3833,12 +4062,15 @@ ipcMain.handle('run-batch-repo-action', async (event, data) => {
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
action,
|
action,
|
||||||
summary: { total: results.length, success, failed },
|
cancelled,
|
||||||
|
summary: { total: repos.length, done: results.length, success, failed },
|
||||||
results
|
results
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('run-batch-repo-action error', e);
|
console.error('run-batch-repo-action error', e);
|
||||||
return { ok: false, error: String(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),
|
downloadGiteaFile: (data) => ipcRenderer.invoke('download-gitea-file', data),
|
||||||
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
uploadLocalFolderToGitea: (data) => ipcRenderer.invoke('upload-local-folder-to-gitea', data),
|
||||||
uploadPathsToGitea: (data) => ipcRenderer.invoke('upload-paths-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
|
// Repository & Git Management
|
||||||
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
||||||
|
|||||||
Reference in New Issue
Block a user