diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 5bc984b..41a043a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,54 @@ TASKMATE - CHANGELOG ==================== +================================================================================ +30.12.2025 - Browser-Upload: Lokale Verzeichnisse ins Gitea pushen +================================================================================ + +FEATURE: VERZEICHNIS-UPLOAD VOM BROWSER +-------------------------------------------------------------------------------- +- Lokale Verzeichnisse direkt vom Computer ins Gitea pushen +- Verwendet File System Access API (Chrome/Edge/Opera) +- Drag & Drop als Fallback für andere Browser +- Automatische Filterung von .git, node_modules, etc. + +ABLAUF +-------------------------------------------------------------------------------- +1. Ziel-Repository aus Gitea-Liste auswählen +2. Ziel-Branch eingeben (Standard: main) +3. "Verzeichnis auswählen" klicken oder Ordner per Drag & Drop +4. Datei-Vorschau prüfen +5. Commit-Nachricht eingeben +6. "Commit & Push" ausführen + +TECHNISCHE ÄNDERUNGEN +-------------------------------------------------------------------------------- +- backend/routes/git.js: + * POST /api/git/browser-upload - Empfängt Dateien und pusht ins Gitea + * POST /api/git/browser-upload-prepare - Bereitet Upload-Session vor + * DELETE /api/git/browser-upload/:sessionId - Bricht Upload ab + * Multer-Konfiguration für Git-Uploads (50MB/Datei, 500 Dateien max) +- frontend/js/api.js: + * prepareBrowserUpload() - Session vorbereiten + * browserUploadAndPush() - Dateien hochladen und pushen + * cancelBrowserUpload() - Session abbrechen +- frontend/js/gitea.js: + * Browser-Upload Properties und Ignore-Patterns + * bindBrowserUploadEvents() - Event-Handler + * handleSelectDirectory() - File System Access API + * readDirectoryRecursive() - Verzeichnis rekursiv lesen + * handleDroppedFiles() - Drag & Drop Handler + * renderUploadPreview() - Datei-Vorschau + * executeBrowserUpload() - Upload durchführen +- frontend/index.html: Neues Browser-Upload UI mit Schritten +- frontend/css/gitea.css: Styles für Upload-Schritte, Drop-Zone, Progress +- frontend/sw.js: Cache-Version auf 132 erhöht + +IGNORIERTE DATEIEN/ORDNER +-------------------------------------------------------------------------------- +.git, node_modules, __pycache__, .env, .env.local, .env.production, +.DS_Store, Thumbs.db, .idea, .vscode, dist, build, .cache, coverage + ================================================================================ 30.12.2025 - Gitea-Integration: Server-Modus ================================================================================ diff --git a/backend/routes/git.js b/backend/routes/git.js index 3a5a232..ce29203 100644 --- a/backend/routes/git.js +++ b/backend/routes/git.js @@ -6,14 +6,49 @@ const express = require('express'); const router = express.Router(); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); const { getDb } = require('../database'); const logger = require('../utils/logger'); const gitService = require('../services/gitService'); const giteaService = require('../services/giteaService'); +const multer = require('multer'); // Fester Pfad für Server-Modus (TaskMate Source-Code) const SERVER_SOURCE_PATH = '/app/taskmate-source'; +// Temporäres Verzeichnis für Browser-Uploads +const TEMP_UPLOAD_DIR = path.join(os.tmpdir(), 'taskmate-git-uploads'); + +// Multer-Konfiguration für Git-Uploads (beliebige Dateitypen) +const gitUploadStorage = multer.diskStorage({ + destination: (req, file, cb) => { + // Erstelle eindeutiges temporäres Verzeichnis pro Upload-Session + const sessionId = req.body.sessionId || Date.now().toString(); + const sessionDir = path.join(TEMP_UPLOAD_DIR, sessionId); + + // Relativen Pfad aus dem Dateinamen extrahieren (wird vom Frontend gesendet) + const relativePath = file.originalname; + const fileDir = path.join(sessionDir, path.dirname(relativePath)); + + fs.mkdirSync(fileDir, { recursive: true }); + cb(null, fileDir); + }, + filename: (req, file, cb) => { + // Nur den Dateinamen ohne Pfad + cb(null, path.basename(file.originalname)); + } +}); + +const gitUpload = multer({ + storage: gitUploadStorage, + limits: { + fileSize: 50 * 1024 * 1024, // 50MB pro Datei + files: 500 // Maximal 500 Dateien + } +}); + /** * Hilfsfunktion: Anwendung für Projekt abrufen */ @@ -757,4 +792,178 @@ router.get('/server/info', (req, res) => { } }); +// ============================================ +// BROWSER-UPLOAD ENDPOINTS +// Für lokale Verzeichnis-Uploads vom Browser +// ============================================ + +/** + * Hilfsfunktion: Verzeichnis rekursiv löschen + */ +function deleteFolderRecursive(dirPath) { + if (fs.existsSync(dirPath)) { + fs.readdirSync(dirPath).forEach((file) => { + const curPath = path.join(dirPath, file); + if (fs.lstatSync(curPath).isDirectory()) { + deleteFolderRecursive(curPath); + } else { + fs.unlinkSync(curPath); + } + }); + fs.rmdirSync(dirPath); + } +} + +/** + * POST /api/git/browser-upload + * Empfängt Dateien vom Browser und pusht sie ins Gitea + * + * Body (multipart/form-data): + * - files: Die hochgeladenen Dateien (originalname enthält relativen Pfad) + * - repoUrl: Die Gitea-Repository-URL + * - branch: Der Ziel-Branch (default: main) + * - commitMessage: Die Commit-Nachricht + * - sessionId: Eindeutige Session-ID für den Upload + */ +router.post('/browser-upload', gitUpload.array('files', 500), async (req, res) => { + const sessionId = req.body.sessionId || Date.now().toString(); + const sessionDir = path.join(TEMP_UPLOAD_DIR, sessionId); + + try { + const { repoUrl, branch = 'main', commitMessage } = req.body; + const files = req.files; + + // Validierung + if (!repoUrl) { + return res.status(400).json({ error: 'Repository-URL ist erforderlich' }); + } + + if (!commitMessage) { + return res.status(400).json({ error: 'Commit-Nachricht ist erforderlich' }); + } + + if (!files || files.length === 0) { + return res.status(400).json({ error: 'Keine Dateien hochgeladen' }); + } + + logger.info(`[Browser-Upload] ${files.length} Dateien empfangen für ${repoUrl}`); + + // Git-Repository initialisieren + const initResult = gitService.initRepository(sessionDir); + if (!initResult.success) { + throw new Error('Git-Initialisierung fehlgeschlagen: ' + initResult.error); + } + + // Remote hinzufügen + const remoteResult = gitService.addRemote(sessionDir, repoUrl, 'origin'); + if (!remoteResult.success) { + throw new Error('Remote hinzufügen fehlgeschlagen: ' + remoteResult.error); + } + + // Autor aus eingeloggtem Benutzer + const author = req.user ? { + name: req.user.display_name || req.user.username, + email: req.user.email || `${req.user.username.toLowerCase()}@taskmate.local` + } : null; + + // Alle Dateien stagen + const stageResult = gitService.stageAll(sessionDir); + if (!stageResult.success) { + throw new Error('Staging fehlgeschlagen: ' + stageResult.error); + } + + // Commit erstellen + const commitResult = gitService.commit(sessionDir, commitMessage, author); + if (!commitResult.success) { + throw new Error('Commit fehlgeschlagen: ' + commitResult.error); + } + + // Push mit Upstream + const pushResult = gitService.pushWithUpstream(sessionDir, branch, 'origin', false); + if (!pushResult.success) { + // Bei Fehler: Versuche Force-Push falls Branch existiert + if (pushResult.error && pushResult.error.includes('rejected')) { + logger.warn('[Browser-Upload] Normaler Push abgelehnt, versuche mit Force...'); + const forcePushResult = gitService.pushWithUpstream(sessionDir, branch, 'origin', true); + if (!forcePushResult.success) { + throw new Error('Push fehlgeschlagen: ' + forcePushResult.error); + } + } else { + throw new Error('Push fehlgeschlagen: ' + pushResult.error); + } + } + + logger.info(`[Browser-Upload] Erfolgreich nach ${repoUrl}/${branch} gepusht`); + + // Erfolgreich - Aufräumen + deleteFolderRecursive(sessionDir); + + res.json({ + success: true, + message: `${files.length} Dateien erfolgreich nach ${branch} gepusht`, + filesCount: files.length, + branch: branch, + commit: commitResult.hash || null + }); + + } catch (error) { + logger.error('[Browser-Upload] Fehler:', error); + + // Bei Fehler aufräumen + try { + deleteFolderRecursive(sessionDir); + } catch (cleanupError) { + logger.warn('[Browser-Upload] Aufräumen fehlgeschlagen:', cleanupError); + } + + res.status(500).json({ + success: false, + error: error.message || 'Upload fehlgeschlagen' + }); + } +}); + +/** + * POST /api/git/browser-upload-prepare + * Bereitet einen Upload vor und gibt eine Session-ID zurück + */ +router.post('/browser-upload-prepare', (req, res) => { + const sessionId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const sessionDir = path.join(TEMP_UPLOAD_DIR, sessionId); + + try { + fs.mkdirSync(sessionDir, { recursive: true }); + + res.json({ + success: true, + sessionId: sessionId, + maxFileSize: 50 * 1024 * 1024, // 50MB + maxFiles: 500 + }); + } catch (error) { + logger.error('[Browser-Upload] Prepare fehlgeschlagen:', error); + res.status(500).json({ error: 'Vorbereitung fehlgeschlagen' }); + } +}); + +/** + * DELETE /api/git/browser-upload/:sessionId + * Löscht eine Upload-Session (für Abbruch) + */ +router.delete('/browser-upload/:sessionId', (req, res) => { + const { sessionId } = req.params; + const sessionDir = path.join(TEMP_UPLOAD_DIR, sessionId); + + try { + if (fs.existsSync(sessionDir)) { + deleteFolderRecursive(sessionDir); + } + + res.json({ success: true }); + } catch (error) { + logger.error('[Browser-Upload] Löschen fehlgeschlagen:', error); + res.status(500).json({ error: 'Löschen fehlgeschlagen' }); + } +}); + module.exports = router; diff --git a/backups/backup_2025-12-28T16-17-00-340Z.db b/backups/backup_2025-12-28T16-17-00-340Z.db deleted file mode 100644 index c09da33..0000000 Binary files a/backups/backup_2025-12-28T16-17-00-340Z.db and /dev/null differ diff --git a/backups/backup_2025-12-28T16-17-00-340Z.db-wal b/backups/backup_2025-12-28T16-17-00-340Z.db-wal deleted file mode 100644 index b37a0ee..0000000 Binary files a/backups/backup_2025-12-28T16-17-00-340Z.db-wal and /dev/null differ diff --git a/backups/backup_2025-12-28T16-19-37-475Z.db b/backups/backup_2025-12-28T16-19-37-475Z.db deleted file mode 100644 index c09da33..0000000 Binary files a/backups/backup_2025-12-28T16-19-37-475Z.db and /dev/null differ diff --git a/backups/backup_2025-12-28T16-19-37-475Z.db-wal b/backups/backup_2025-12-28T16-19-37-475Z.db-wal deleted file mode 100644 index 4ea6f02..0000000 Binary files a/backups/backup_2025-12-28T16-19-37-475Z.db-wal and /dev/null differ diff --git a/data/taskmate.db-shm b/data/taskmate.db-shm index 74142a2..b0d807c 100644 Binary files a/data/taskmate.db-shm and b/data/taskmate.db-shm differ diff --git a/data/taskmate.db-wal b/data/taskmate.db-wal index 8c53d73..3af8f89 100644 Binary files a/data/taskmate.db-wal and b/data/taskmate.db-wal differ diff --git a/frontend/css/gitea.css b/frontend/css/gitea.css index 3795fd6..3d69c9e 100644 --- a/frontend/css/gitea.css +++ b/frontend/css/gitea.css @@ -695,3 +695,206 @@ justify-content: center; } } + +/* ============================================================================= + BROWSER UPLOAD + ============================================================================= */ + +.browser-compat-notice { + display: flex; + align-items: flex-start; + gap: var(--spacing-3); + padding: var(--spacing-3) var(--spacing-4); + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.3); + border-radius: var(--radius-md); + margin-bottom: var(--spacing-4); + color: var(--warning); + font-size: var(--text-sm); +} + +.browser-compat-notice svg { + flex-shrink: 0; + margin-top: 2px; +} + +/* Upload Steps */ +.upload-step { + background: var(--bg-tertiary); + border-radius: var(--radius-md); + padding: var(--spacing-4); + margin-bottom: var(--spacing-4); +} + +.step-header { + display: flex; + align-items: center; + gap: var(--spacing-3); + margin-bottom: var(--spacing-4); +} + +.step-number { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: var(--primary); + color: white; + border-radius: 50%; + font-weight: 600; + font-size: var(--text-sm); + flex-shrink: 0; +} + +.step-title { + font-weight: 500; + color: var(--text-primary); + flex: 1; +} + +.file-count { + font-size: var(--text-sm); + color: var(--text-secondary); + background: var(--bg-primary); + padding: var(--spacing-1) var(--spacing-2); + border-radius: var(--radius-sm); +} + +/* Directory Picker */ +.directory-picker { + display: flex; + flex-direction: column; + gap: var(--spacing-3); + align-items: center; +} + +.btn-lg { + padding: var(--spacing-3) var(--spacing-6); + font-size: var(--text-base); +} + +.drop-zone { + width: 100%; + padding: var(--spacing-8); + border: 2px dashed var(--border-default); + border-radius: var(--radius-lg); + text-align: center; + color: var(--text-tertiary); + transition: all 0.2s; +} + +.drop-zone svg { + margin-bottom: var(--spacing-2); + color: var(--text-tertiary); +} + +.drop-zone p { + margin: 0; + font-size: var(--text-sm); +} + +.drop-zone.drag-over { + border-color: var(--primary); + background: rgba(59, 130, 246, 0.05); + color: var(--primary); +} + +.drop-zone.drag-over svg { + color: var(--primary); +} + +/* Files List */ +.upload-files-list { + max-height: 250px; + overflow-y: auto; + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + background: var(--bg-primary); +} + +.upload-file-item { + display: flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-2) var(--spacing-3); + border-bottom: 1px solid var(--border-default); + font-size: var(--text-sm); + font-family: monospace; +} + +.upload-file-item:last-child { + border-bottom: none; +} + +.upload-file-item .file-icon { + color: var(--text-tertiary); + flex-shrink: 0; +} + +.upload-file-item .file-path { + flex: 1; + color: var(--text-primary); + word-break: break-all; +} + +.upload-file-item .file-size { + color: var(--text-tertiary); + font-size: var(--text-xs); + flex-shrink: 0; +} + +.excluded-info { + margin-top: var(--spacing-2); + color: var(--text-tertiary); +} + +/* Upload Actions */ +.upload-actions { + display: flex; + gap: var(--spacing-3); + justify-content: flex-end; + margin-top: var(--spacing-4); +} + +/* Progress Bar */ +.upload-progress { + margin-top: var(--spacing-4); + display: flex; + align-items: center; + gap: var(--spacing-3); +} + +.progress-bar { + flex: 1; + height: 8px; + background: var(--bg-primary); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: var(--primary); + border-radius: var(--radius-full); + transition: width 0.3s ease; + width: 0%; +} + +#upload-progress-text { + font-size: var(--text-sm); + color: var(--text-secondary); + min-width: 40px; + text-align: right; +} + +/* Responsive */ +@media (max-width: 480px) { + .upload-actions { + flex-direction: column; + } + + .upload-actions .btn { + width: 100%; + } +} diff --git a/frontend/index.html b/frontend/index.html index eeea9d9..0d08213 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -610,7 +610,110 @@ - + + + +