Upload via GUI (31 Dateien)
This commit is contained in:
@@ -2444,6 +2444,174 @@ ipcMain.handle('upload-local-folder-to-gitea', async (event, data) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* -----------------------------------------------------------------
|
||||
Gemeinsamer Git-Upload für einen kompletten Drop (Dateien + Ordner).
|
||||
Erzeugt EINEN Clone + EINEN Commit + EINEN Push für ALLE Pfade
|
||||
zusammen – egal wie viele Dateien/Ordner gedroppt wurden.
|
||||
----------------------------------------------------------------- */
|
||||
const GIT_UPLOAD_EXCLUDE = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
||||
|
||||
// Wandelt gedroppte Pfade (Dateien und/oder Ordner) in eine flache Liste
|
||||
// { localFile, targetPath } um. Dateien -> destPath/<name>, Ordner ->
|
||||
// destPath/<ordnername>/<rel> (identisch zum bisherigen Verhalten).
|
||||
function buildItemsFromPaths(paths, destPath) {
|
||||
const items = [];
|
||||
const skipped = [];
|
||||
for (const p of paths) {
|
||||
let stat;
|
||||
try { stat = fs.statSync(p); } catch (e) { skipped.push({ path: p, error: 'not-found' }); continue; }
|
||||
const baseName = ppath.basename(p);
|
||||
if (stat.isFile()) {
|
||||
const targetPath = destPath ? `${destPath}/${baseName}` : baseName;
|
||||
items.push({ localFile: p, targetPath });
|
||||
} else if (stat.isDirectory()) {
|
||||
const folderName = baseName;
|
||||
(function walk(dir) {
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(dir); } catch (e) { return; }
|
||||
for (const entry of entries) {
|
||||
if (GIT_UPLOAD_EXCLUDE.includes(entry)) continue;
|
||||
const full = ppath.join(dir, entry);
|
||||
let st;
|
||||
try { st = fs.statSync(full); } catch (e) { continue; }
|
||||
if (st.isDirectory()) {
|
||||
walk(full);
|
||||
} else if (st.isFile()) {
|
||||
const rel = ppath.relative(p, full).split(ppath.sep).join('/');
|
||||
const targetPath = destPath ? `${destPath}/${folderName}/${rel}` : `${folderName}/${rel}`;
|
||||
items.push({ localFile: full, targetPath });
|
||||
}
|
||||
}
|
||||
})(p);
|
||||
} else {
|
||||
skipped.push({ path: p, error: 'unsupported' });
|
||||
}
|
||||
}
|
||||
return { items, skipped };
|
||||
}
|
||||
|
||||
// Klont das Repo (shallow), kopiert alle items hinein, commit + push.
|
||||
async function gitPushItemsToGitea({ token, url, owner, repo, branch, items, message, onProgress }) {
|
||||
const simpleGit = require('simple-git');
|
||||
|
||||
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`;
|
||||
|
||||
const tmpDir = getSafeTmpDir(`git-paths-${owner}-${repo}`);
|
||||
const gitConfig = ['user.email=gui@gitmanager.local', 'user.name=Git Manager GUI'];
|
||||
const safeBranch = sanitizeGitRef(branch || 'HEAD', 'HEAD');
|
||||
|
||||
try {
|
||||
const git = simpleGit({ config: gitConfig });
|
||||
let repoGit;
|
||||
let isEmptyRepo = false;
|
||||
|
||||
try {
|
||||
const cloneArgs = ['--depth', '1', '--no-single-branch'];
|
||||
if (safeBranch !== 'HEAD') cloneArgs.push('--branch', safeBranch);
|
||||
console.log('[UploadPaths] Clone-Versuch:', { cloneArgs, tmpDir });
|
||||
await git.clone(authUrl, tmpDir, cloneArgs);
|
||||
repoGit = simpleGit({ baseDir: tmpDir, config: gitConfig });
|
||||
} catch (cloneErr) {
|
||||
const cloneMsg = String(cloneErr);
|
||||
if (cloneMsg.includes('empty') || cloneMsg.includes('nothing to clone') ||
|
||||
cloneMsg.includes('did not match') || cloneMsg.includes('Remote branch')) {
|
||||
isEmptyRepo = true;
|
||||
repoGit = simpleGit({ baseDir: tmpDir, config: gitConfig });
|
||||
await repoGit.init();
|
||||
await repoGit.addRemote('origin', authUrl);
|
||||
} else {
|
||||
throw cloneErr;
|
||||
}
|
||||
}
|
||||
|
||||
if (onProgress) onProgress(30);
|
||||
|
||||
for (const item of items) {
|
||||
const destFile = ppath.join(tmpDir, ...item.targetPath.split('/'));
|
||||
fs.mkdirSync(ppath.dirname(destFile), { recursive: true });
|
||||
fs.copyFileSync(item.localFile, destFile);
|
||||
}
|
||||
|
||||
if (onProgress) onProgress(60);
|
||||
|
||||
await repoGit.add('.');
|
||||
|
||||
let hasChanges = true;
|
||||
try {
|
||||
await repoGit.commit(message);
|
||||
} catch (commitErr) {
|
||||
if (String(commitErr).includes('nothing to commit')) hasChanges = false;
|
||||
else throw commitErr;
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
let pushBranch = safeBranch;
|
||||
if (pushBranch === 'HEAD' || isEmptyRepo) {
|
||||
try { const bs = await repoGit.branch(); pushBranch = bs.current || 'main'; } catch (_) { pushBranch = 'main'; }
|
||||
}
|
||||
if (isEmptyRepo) await repoGit.push(['-u', 'origin', pushBranch]);
|
||||
else await repoGit.push('origin', pushBranch);
|
||||
}
|
||||
|
||||
if (onProgress) onProgress(100);
|
||||
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5000);
|
||||
return { hasChanges };
|
||||
} catch (gitErr) {
|
||||
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
||||
throw gitErr;
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle('upload-paths-to-gitea', async (event, data) => {
|
||||
const uploadDebugId = `p-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
const token = (data && data.token) || (credentials && credentials.giteaToken);
|
||||
const url = (data && data.url) || (credentials && credentials.giteaURL);
|
||||
if (!token || !url) return { ok: false, error: 'Zugangsdaten fehlen', debugId: uploadDebugId };
|
||||
|
||||
const owner = data.owner;
|
||||
const repo = data.repo;
|
||||
if (!owner || !repo) return { ok: false, error: 'missing-owner-or-repo', debugId: uploadDebugId };
|
||||
|
||||
const destPath = (data.destPath || '').replace(/^\//, '').replace(/\/$/, '');
|
||||
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 };
|
||||
|
||||
try { invalidateRepoContentsCache(owner, repo); } catch (_) {}
|
||||
try { caches.repos.invalidate(`${owner}/${repo}`); } catch (_) {}
|
||||
|
||||
const { items, skipped } = buildItemsFromPaths(paths, destPath);
|
||||
if (items.length === 0) {
|
||||
return { ok: false, error: 'Keine Dateien zum Hochladen gefunden', skipped, debugId: uploadDebugId };
|
||||
}
|
||||
|
||||
const total = items.length;
|
||||
const onProgress = (percent) => {
|
||||
try { event.sender.send('folder-upload-progress', { processed: Math.floor(total * percent / 100), total, percent }); } catch (_) {}
|
||||
};
|
||||
onProgress(0);
|
||||
|
||||
console.log('[UploadPaths] start', { uploadDebugId, owner, repo, branch, total, destPath, pathCount: paths.length });
|
||||
await gitPushItemsToGitea({
|
||||
token, url, owner, repo, branch, items,
|
||||
message: `${messagePrefix} (${total} Datei${total === 1 ? '' : 'en'})`,
|
||||
onProgress
|
||||
});
|
||||
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) {
|
||||
console.error('[UploadPaths] fatal', { uploadDebugId, error: String(e) });
|
||||
return { ok: false, error: String(e && e.message ? e.message : e), debugId: uploadDebugId };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('download-gitea-file', async (event, data) => {
|
||||
try {
|
||||
const credentials = readCredentials();
|
||||
@@ -2841,19 +3009,97 @@ ipcMain.handle('upload-and-push', async (event, data) => {
|
||||
}
|
||||
}
|
||||
|
||||
// --- FALL 2: Ordner (Normale Git-Logik) ---
|
||||
// --- FALL 2: Ordner ---
|
||||
if (!isDirectory) {
|
||||
return { ok: false, error: `Path is neither file nor directory (${uploadDebugId})` };
|
||||
}
|
||||
|
||||
const folderName = ppath.basename(data.localFolder);
|
||||
// Dateien/Ordner, die niemals hochgeladen werden sollen
|
||||
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
||||
|
||||
// === PRIMÄRWEG: git push =========================================
|
||||
// Ein einziger komprimierter Transfer + genau EIN Commit statt
|
||||
// hunderter API-Requests. Das behebt sowohl die Langsamkeit als auch
|
||||
// die "cannot lock ref"/Timeout-Fehler durch parallele Einzel-Commits.
|
||||
// Bei Fehler (z.B. git nicht installiert) fällt der Code auf den
|
||||
// API-Upload weiter unten zurück.
|
||||
try {
|
||||
let finalCloneUrl = cloneUrl;
|
||||
if (!finalCloneUrl && giteaUrl) {
|
||||
const base = giteaUrl.replace(/\/$/, '');
|
||||
const urlObj = new URL(base);
|
||||
finalCloneUrl = `${urlObj.protocol}//${urlObj.host}/${owner}/${repo}.git`;
|
||||
}
|
||||
if (!finalCloneUrl) throw new Error('no-clone-url');
|
||||
|
||||
// Token in die Clone-URL einbetten (für Authentifizierung beim Push)
|
||||
let authClone = finalCloneUrl;
|
||||
try {
|
||||
const urlObj = new URL(finalCloneUrl);
|
||||
if (token && urlObj.protocol.startsWith('http')) {
|
||||
urlObj.username = encodeURIComponent(token);
|
||||
authClone = urlObj.toString();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
const tmpDir = getSafeTmpDir(`git-push-folder-${owner}-${repo}`);
|
||||
try {
|
||||
console.log('[UPLOAD_DEBUG][main] directory-upload:git-clone', { uploadDebugId, branch, destPath });
|
||||
const cloneArgs = ['clone', '--depth', '1'];
|
||||
if (branch !== 'HEAD') cloneArgs.push('--branch', branch);
|
||||
cloneArgs.push(authClone, tmpDir);
|
||||
runGitSync(cloneArgs, process.cwd(), gitExecOptions);
|
||||
|
||||
// Zielverzeichnis im Repo: <destPath>/<folderName>/ (wie im API-Weg)
|
||||
let destDirInRepo = tmpDir;
|
||||
if (destPath) destDirInRepo = ppath.join(tmpDir, destPath.split('/').join(ppath.sep));
|
||||
destDirInRepo = ppath.join(destDirInRepo, folderName);
|
||||
ensureDir(destDirInRepo);
|
||||
|
||||
// Kompletten Ordner rekursiv kopieren, ausgeschlossene Einträge überspringen
|
||||
fs.cpSync(data.localFolder, destDirInRepo, {
|
||||
recursive: true,
|
||||
filter: (src) => !excludeList.includes(ppath.basename(src))
|
||||
});
|
||||
|
||||
runGitSync(['-C', tmpDir, 'add', '.'], process.cwd(), gitExecOptions);
|
||||
try {
|
||||
runGitSync(['-C', tmpDir, 'commit', '-m', `Upload ${folderName} via GUI`], process.cwd(), gitExecOptions);
|
||||
} catch (commitErr) {
|
||||
// "nothing to commit" ist kein echter Fehler
|
||||
if (!/nothing to commit/i.test(String(commitErr))) throw commitErr;
|
||||
console.log('[UPLOAD_DEBUG][main] directory-upload:git-nothing-to-commit', { uploadDebugId });
|
||||
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
|
||||
return { ok: true, usedGit: true, results: [], debugId: uploadDebugId, msg: 'Keine Änderungen zum Hochladen' };
|
||||
}
|
||||
|
||||
let pushBranch = branch;
|
||||
if (pushBranch === 'HEAD') {
|
||||
try {
|
||||
pushBranch = runGitSync(['-C', tmpDir, 'rev-parse', '--abbrev-ref', 'HEAD'], process.cwd(), gitExecOptions).trim();
|
||||
} catch (_) { pushBranch = 'main'; }
|
||||
if (!pushBranch || pushBranch === 'HEAD') pushBranch = 'main';
|
||||
}
|
||||
runGitSync(['-C', tmpDir, 'push', 'origin', pushBranch], process.cwd(), gitExecOptions);
|
||||
|
||||
console.log('[UPLOAD_DEBUG][main] directory-upload:git-push-done', { uploadDebugId, pushBranch });
|
||||
setTimeout(() => { try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {} }, 5_000);
|
||||
return { ok: true, usedGit: true, results: [], debugId: uploadDebugId };
|
||||
} catch (gitErr) {
|
||||
try { if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
|
||||
throw gitErr;
|
||||
}
|
||||
} catch (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
|
||||
}
|
||||
|
||||
console.log('[UPLOAD_DEBUG][main] directory-upload:using-api-path', { uploadDebugId, branch, destPath });
|
||||
|
||||
// Fallback: API Upload (paralleler Upload)
|
||||
const items = [];
|
||||
const folderName = ppath.basename(data.localFolder);
|
||||
|
||||
// FIXED EXCLUDE LIST: Filter out .git, node_modules etc.
|
||||
const excludeList = ['.git', 'node_modules', '.DS_Store', 'thumbs.db', '.vscode', '.idea'];
|
||||
// excludeList und folderName sind oben (FALL 2) bereits definiert
|
||||
|
||||
(function walk(dir) {
|
||||
const entries = fs.readdirSync(dir);
|
||||
|
||||
Reference in New Issue
Block a user