Gitea-Fix
Dieser Commit ist enthalten in:
@ -884,6 +884,99 @@ class ApiClient {
|
||||
async serverGitCheckout(branch) {
|
||||
return this.post('/git/server/checkout', { branch });
|
||||
}
|
||||
|
||||
// =====================
|
||||
// BROWSER-UPLOAD ENDPOINTS
|
||||
// =====================
|
||||
|
||||
async prepareBrowserUpload() {
|
||||
return this.post('/git/browser-upload-prepare', {});
|
||||
}
|
||||
|
||||
async cancelBrowserUpload(sessionId) {
|
||||
return this.delete(`/git/browser-upload/${sessionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Dateien vom Browser hoch und pusht sie ins Gitea
|
||||
* @param {Object} options - Upload-Optionen
|
||||
* @param {File[]} options.files - Array von File-Objekten mit relativePath Property
|
||||
* @param {string} options.repoUrl - Gitea Repository URL
|
||||
* @param {string} options.branch - Ziel-Branch
|
||||
* @param {string} options.commitMessage - Commit-Nachricht
|
||||
* @param {string} options.sessionId - Session-ID vom prepare-Aufruf
|
||||
* @param {Function} options.onProgress - Progress-Callback (optional)
|
||||
*/
|
||||
async browserUploadAndPush(options) {
|
||||
const { files, repoUrl, branch, commitMessage, sessionId, onProgress } = options;
|
||||
|
||||
const url = `${this.baseUrl}/git/browser-upload`;
|
||||
const formData = new FormData();
|
||||
|
||||
// Metadaten hinzufügen
|
||||
formData.append('repoUrl', repoUrl);
|
||||
formData.append('branch', branch || 'main');
|
||||
formData.append('commitMessage', commitMessage);
|
||||
formData.append('sessionId', sessionId);
|
||||
|
||||
// Dateien hinzufügen (mit relativem Pfad als Dateiname)
|
||||
files.forEach(fileInfo => {
|
||||
// Erstelle neues File-Objekt mit relativem Pfad als Namen
|
||||
const file = new File([fileInfo.file], fileInfo.relativePath, {
|
||||
type: fileInfo.file.type
|
||||
});
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
const token = this.getToken();
|
||||
const csrfToken = this.getCsrfToken();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.open('POST', url);
|
||||
|
||||
if (token) {
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
if (csrfToken) {
|
||||
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
|
||||
}
|
||||
|
||||
if (onProgress) {
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentage = Math.round((e.loaded / e.total) * 100);
|
||||
onProgress(percentage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
resolve(JSON.parse(xhr.responseText));
|
||||
} catch {
|
||||
resolve({ success: true });
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const error = JSON.parse(xhr.responseText);
|
||||
reject(new Error(error.error || 'Upload fehlgeschlagen'));
|
||||
} catch {
|
||||
reject(new Error('Upload fehlgeschlagen'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
reject(new Error('Netzwerkfehler beim Upload'));
|
||||
});
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Custom API Error Class
|
||||
|
||||
@ -31,6 +31,29 @@ class GiteaManager {
|
||||
this.serverBranches = [];
|
||||
this.serverCommits = [];
|
||||
this.hiddenServerCommits = new Set();
|
||||
|
||||
// Browser-Upload Eigenschaften
|
||||
this.browserUploadFiles = [];
|
||||
this.browserUploadSessionId = null;
|
||||
this.supportsDirectoryPicker = 'showDirectoryPicker' in window;
|
||||
|
||||
// Zu ignorierende Ordner/Dateien
|
||||
this.ignorePatterns = [
|
||||
'.git',
|
||||
'node_modules',
|
||||
'__pycache__',
|
||||
'.env',
|
||||
'.env.local',
|
||||
'.env.production',
|
||||
'.DS_Store',
|
||||
'Thumbs.db',
|
||||
'.idea',
|
||||
'.vscode',
|
||||
'dist',
|
||||
'build',
|
||||
'.cache',
|
||||
'coverage'
|
||||
];
|
||||
}
|
||||
|
||||
async init() {
|
||||
@ -51,8 +74,12 @@ class GiteaManager {
|
||||
this.mainSection = $('#gitea-main-section');
|
||||
this.connectionStatus = $('#gitea-connection-status');
|
||||
|
||||
// DOM Elements - Browser-Upload
|
||||
this.browserUploadSection = $('#gitea-browser-upload');
|
||||
|
||||
this.bindEvents();
|
||||
this.bindServerEvents();
|
||||
this.bindBrowserUploadEvents();
|
||||
this.subscribeToStore();
|
||||
this.initialized = true;
|
||||
}
|
||||
@ -143,14 +170,19 @@ class GiteaManager {
|
||||
if (this.currentMode === 'server') {
|
||||
// Server-Modus
|
||||
this.serverModeSection?.classList.remove('hidden');
|
||||
this.browserUploadSection?.classList.add('hidden');
|
||||
this.noProjectSection?.classList.add('hidden');
|
||||
this.configSection?.classList.add('hidden');
|
||||
this.mainSection?.classList.add('hidden');
|
||||
this.loadServerData();
|
||||
} else {
|
||||
// Projekt-Modus
|
||||
// Projekt-Modus (Browser-Upload)
|
||||
this.serverModeSection?.classList.add('hidden');
|
||||
this.loadApplication();
|
||||
this.browserUploadSection?.classList.remove('hidden');
|
||||
this.noProjectSection?.classList.add('hidden');
|
||||
this.configSection?.classList.add('hidden');
|
||||
this.mainSection?.classList.add('hidden');
|
||||
this.initBrowserUpload();
|
||||
}
|
||||
}
|
||||
|
||||
@ -524,6 +556,402 @@ class GiteaManager {
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// BROWSER-UPLOAD METHODEN
|
||||
// =====================
|
||||
|
||||
bindBrowserUploadEvents() {
|
||||
// Verzeichnis auswählen
|
||||
$('#btn-select-directory')?.addEventListener('click', () => this.handleSelectDirectory());
|
||||
|
||||
// Repository für Upload aktualisieren
|
||||
$('#btn-refresh-upload-repos')?.addEventListener('click', () => this.loadBrowserUploadRepos());
|
||||
|
||||
// Upload abbrechen
|
||||
$('#btn-cancel-upload')?.addEventListener('click', () => this.cancelBrowserUpload());
|
||||
|
||||
// Upload ausführen
|
||||
$('#btn-execute-upload')?.addEventListener('click', () => this.executeBrowserUpload());
|
||||
|
||||
// Drop Zone Events
|
||||
const dropZone = $('#drop-zone');
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('drag-over');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('drag-over');
|
||||
this.handleDroppedFiles(e.dataTransfer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async initBrowserUpload() {
|
||||
// Browser-Kompatibilität prüfen
|
||||
const compatNotice = $('#browser-upload-compat');
|
||||
if (compatNotice) {
|
||||
if (!this.supportsDirectoryPicker) {
|
||||
compatNotice.classList.remove('hidden');
|
||||
// Drop-Zone anzeigen als Alternative
|
||||
$('#drop-zone')?.classList.remove('hidden');
|
||||
} else {
|
||||
compatNotice.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Upload-Zustand zurücksetzen
|
||||
this.browserUploadFiles = [];
|
||||
this.browserUploadSessionId = null;
|
||||
this.resetBrowserUploadUI();
|
||||
|
||||
// Repositories laden
|
||||
await this.loadBrowserUploadRepos();
|
||||
}
|
||||
|
||||
async loadBrowserUploadRepos() {
|
||||
const select = $('#browser-upload-repo-select');
|
||||
if (!select) return;
|
||||
|
||||
try {
|
||||
const result = await api.getGiteaRepositories();
|
||||
// API gibt { success, repositories } zurück
|
||||
this.giteaRepos = result.repositories || [];
|
||||
|
||||
select.innerHTML = '<option value="">-- Repository wählen --</option>';
|
||||
|
||||
this.giteaRepos.forEach(repo => {
|
||||
const option = document.createElement('option');
|
||||
// API-Felder: cloneUrl, fullName, owner, name
|
||||
option.value = repo.cloneUrl;
|
||||
option.textContent = repo.fullName;
|
||||
option.dataset.owner = repo.owner || '';
|
||||
option.dataset.name = repo.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
if (this.giteaRepos.length === 0) {
|
||||
this.showToast('Keine Repositories gefunden', 'info');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Browser-Upload] Repositories laden fehlgeschlagen:', error);
|
||||
this.showToast('Repositories konnten nicht geladen werden', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async handleSelectDirectory() {
|
||||
if (!this.supportsDirectoryPicker) {
|
||||
this.showToast('Verzeichnis-Auswahl wird in diesem Browser nicht unterstützt', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// File System Access API verwenden
|
||||
const dirHandle = await window.showDirectoryPicker({
|
||||
mode: 'read'
|
||||
});
|
||||
|
||||
this.showToast('Lese Verzeichnis...', 'info');
|
||||
|
||||
// Dateien rekursiv lesen
|
||||
const files = await this.readDirectoryRecursive(dirHandle, '');
|
||||
|
||||
if (files.length === 0) {
|
||||
this.showToast('Keine Dateien gefunden oder alle wurden ignoriert', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.browserUploadFiles = files;
|
||||
this.renderUploadPreview();
|
||||
|
||||
this.showToast(`${files.length} Dateien ausgewählt`, 'success');
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
// Benutzer hat abgebrochen
|
||||
return;
|
||||
}
|
||||
console.error('[Browser-Upload] Verzeichnis lesen fehlgeschlagen:', error);
|
||||
this.showToast('Verzeichnis konnte nicht gelesen werden', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async readDirectoryRecursive(dirHandle, basePath) {
|
||||
const files = [];
|
||||
|
||||
for await (const entry of dirHandle.values()) {
|
||||
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
||||
|
||||
// Ignorierte Muster prüfen
|
||||
if (this.shouldIgnore(entry.name, relativePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.kind === 'file') {
|
||||
try {
|
||||
const file = await entry.getFile();
|
||||
files.push({
|
||||
file: file,
|
||||
relativePath: relativePath,
|
||||
size: file.size
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn(`Datei konnte nicht gelesen werden: ${relativePath}`, err);
|
||||
}
|
||||
} else if (entry.kind === 'directory') {
|
||||
const subFiles = await this.readDirectoryRecursive(entry, relativePath);
|
||||
files.push(...subFiles);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
shouldIgnore(name, path) {
|
||||
// Einfache Muster prüfen
|
||||
for (const pattern of this.ignorePatterns) {
|
||||
if (pattern.startsWith('*.')) {
|
||||
// Wildcard-Muster (z.B. *.log)
|
||||
const ext = pattern.substring(1);
|
||||
if (name.endsWith(ext)) {
|
||||
return true;
|
||||
}
|
||||
} else if (name === pattern || path.includes(`/${pattern}/`) || path.startsWith(`${pattern}/`)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async handleDroppedFiles(dataTransfer) {
|
||||
const items = dataTransfer.items;
|
||||
if (!items || items.length === 0) return;
|
||||
|
||||
const files = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.kind === 'file') {
|
||||
// webkitGetAsEntry für Verzeichnis-Support
|
||||
const entry = item.webkitGetAsEntry?.();
|
||||
if (entry) {
|
||||
const entryFiles = await this.readEntry(entry, '');
|
||||
files.push(...entryFiles);
|
||||
} else {
|
||||
// Fallback: Einzelne Datei
|
||||
const file = item.getAsFile();
|
||||
if (file && !this.shouldIgnore(file.name, file.name)) {
|
||||
files.push({
|
||||
file: file,
|
||||
relativePath: file.name,
|
||||
size: file.size
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
this.showToast('Keine Dateien gefunden oder alle wurden ignoriert', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
this.browserUploadFiles = files;
|
||||
this.renderUploadPreview();
|
||||
this.showToast(`${files.length} Dateien ausgewählt`, 'success');
|
||||
}
|
||||
|
||||
async readEntry(entry, basePath) {
|
||||
const files = [];
|
||||
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
||||
|
||||
if (this.shouldIgnore(entry.name, relativePath)) {
|
||||
return files;
|
||||
}
|
||||
|
||||
if (entry.isFile) {
|
||||
return new Promise((resolve) => {
|
||||
entry.file((file) => {
|
||||
resolve([{
|
||||
file: file,
|
||||
relativePath: relativePath,
|
||||
size: file.size
|
||||
}]);
|
||||
}, () => resolve([]));
|
||||
});
|
||||
} else if (entry.isDirectory) {
|
||||
const reader = entry.createReader();
|
||||
return new Promise((resolve) => {
|
||||
reader.readEntries(async (entries) => {
|
||||
for (const subEntry of entries) {
|
||||
const subFiles = await this.readEntry(subEntry, relativePath);
|
||||
files.push(...subFiles);
|
||||
}
|
||||
resolve(files);
|
||||
}, () => resolve([]));
|
||||
});
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
renderUploadPreview() {
|
||||
const previewSection = $('#upload-preview-section');
|
||||
const commitSection = $('#upload-commit-section');
|
||||
const filesList = $('#upload-files-list');
|
||||
const fileCount = $('#upload-file-count');
|
||||
|
||||
if (!previewSection || !filesList) return;
|
||||
|
||||
// Sektionen anzeigen
|
||||
previewSection.classList.remove('hidden');
|
||||
commitSection?.classList.remove('hidden');
|
||||
|
||||
// Dateianzahl
|
||||
if (fileCount) {
|
||||
fileCount.textContent = `${this.browserUploadFiles.length} Dateien`;
|
||||
}
|
||||
|
||||
// Dateiliste rendern (max 100 anzeigen)
|
||||
const displayFiles = this.browserUploadFiles.slice(0, 100);
|
||||
filesList.innerHTML = displayFiles.map(f => `
|
||||
<div class="upload-file-item">
|
||||
<span class="file-icon">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="2" fill="none"/><polyline points="14 2 14 8 20 8" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
||||
</span>
|
||||
<span class="file-path">${escapeHtml(f.relativePath)}</span>
|
||||
<span class="file-size">${this.formatFileSizeUpload(f.size)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
if (this.browserUploadFiles.length > 100) {
|
||||
filesList.innerHTML += `
|
||||
<div class="upload-file-item" style="justify-content: center; color: var(--text-tertiary);">
|
||||
... und ${this.browserUploadFiles.length - 100} weitere Dateien
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
formatFileSizeUpload(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
resetBrowserUploadUI() {
|
||||
// Vorschau und Commit-Sektion verstecken
|
||||
$('#upload-preview-section')?.classList.add('hidden');
|
||||
$('#upload-commit-section')?.classList.add('hidden');
|
||||
$('#upload-progress-container')?.classList.add('hidden');
|
||||
|
||||
// Felder zurücksetzen
|
||||
const commitMessage = $('#browser-upload-commit-message');
|
||||
if (commitMessage) commitMessage.value = '';
|
||||
|
||||
const filesList = $('#upload-files-list');
|
||||
if (filesList) filesList.innerHTML = '';
|
||||
|
||||
// Progress zurücksetzen
|
||||
const progressBar = $('#upload-progress-bar');
|
||||
if (progressBar) progressBar.style.width = '0%';
|
||||
|
||||
const progressText = $('#upload-progress-text');
|
||||
if (progressText) progressText.textContent = '0%';
|
||||
}
|
||||
|
||||
cancelBrowserUpload() {
|
||||
this.browserUploadFiles = [];
|
||||
this.resetBrowserUploadUI();
|
||||
this.showToast('Upload abgebrochen', 'info');
|
||||
}
|
||||
|
||||
async executeBrowserUpload() {
|
||||
// Validierung
|
||||
const repoSelect = $('#browser-upload-repo-select');
|
||||
const repoUrl = repoSelect?.value;
|
||||
|
||||
if (!repoUrl) {
|
||||
this.showToast('Bitte wählen Sie ein Repository aus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.browserUploadFiles.length === 0) {
|
||||
this.showToast('Keine Dateien ausgewählt', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const commitMessage = $('#browser-upload-commit-message')?.value.trim();
|
||||
if (!commitMessage) {
|
||||
this.showToast('Bitte geben Sie eine Commit-Nachricht ein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const branch = $('#browser-upload-branch')?.value || 'main';
|
||||
|
||||
// UI aktualisieren
|
||||
const executeBtn = $('#btn-execute-upload');
|
||||
const cancelBtn = $('#btn-cancel-upload');
|
||||
const progressContainer = $('#upload-progress-container');
|
||||
|
||||
if (executeBtn) {
|
||||
executeBtn.disabled = true;
|
||||
executeBtn.innerHTML = '<span class="spinner"></span> Lädt hoch...';
|
||||
}
|
||||
if (cancelBtn) cancelBtn.disabled = true;
|
||||
progressContainer?.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
// Session vorbereiten
|
||||
const prepareResult = await api.prepareBrowserUpload();
|
||||
this.browserUploadSessionId = prepareResult.sessionId;
|
||||
|
||||
// Upload durchführen
|
||||
const result = await api.browserUploadAndPush({
|
||||
files: this.browserUploadFiles,
|
||||
repoUrl: repoUrl,
|
||||
branch: branch,
|
||||
commitMessage: commitMessage,
|
||||
sessionId: this.browserUploadSessionId,
|
||||
onProgress: (percent) => {
|
||||
const progressBar = $('#upload-progress-bar');
|
||||
const progressText = $('#upload-progress-text');
|
||||
if (progressBar) progressBar.style.width = `${percent}%`;
|
||||
if (progressText) progressText.textContent = `${percent}%`;
|
||||
}
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.showToast(`Erfolgreich gepusht: ${result.filesCount} Dateien`, 'success');
|
||||
this.cancelBrowserUpload(); // UI zurücksetzen
|
||||
} else {
|
||||
throw new Error(result.error || 'Upload fehlgeschlagen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Browser-Upload] Fehler:', error);
|
||||
this.showToast(error.message || 'Upload fehlgeschlagen', 'error');
|
||||
} finally {
|
||||
// UI wiederherstellen
|
||||
if (executeBtn) {
|
||||
executeBtn.disabled = false;
|
||||
executeBtn.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
Commit & Push
|
||||
`;
|
||||
}
|
||||
if (cancelBtn) cancelBtn.disabled = false;
|
||||
progressContainer?.classList.add('hidden');
|
||||
|
||||
this.browserUploadSessionId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// PROJEKT-MODUS METHODEN
|
||||
// =====================
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren