import { Filesystem, Directory, Encoding } from '@capacitor/filesystem'; import { db } from '../db/database.js'; class FileSystemService { constructor() { this.directory = Directory.Documents; this.baseFolder = 'SchoolMind'; } async init() { try { await Filesystem.mkdir({ path: this.baseFolder, directory: this.directory, recursive: true }); } catch (e) {} try { await Filesystem.mkdir({ path: `${this.baseFolder}/.trash`, directory: this.directory, recursive: true }); } catch (e) {} // Run trash cleanup on init this.cleanupTrash(); } /** * Liest alle Dateien in einem Ordner. */ async readDir(path = '') { try { const fullPath = path ? `${this.baseFolder}/${path}` : this.baseFolder; const result = await Filesystem.readdir({ path: fullPath, directory: this.directory }); // Filter out internal .trash folder return result.files.filter(f => { const name = typeof f === 'string' ? f : f.name; return name && !name.includes('.trash'); }); } catch (e) { console.error('Error reading directory:', e); return []; } } /** * Liest den Inhalt einer Datei als String. */ async readFile(filePath) { try { const fullPath = `${this.baseFolder}/${filePath}`; const result = await Filesystem.readFile({ path: fullPath, directory: this.directory, encoding: Encoding.UTF8 }); return result.data; } catch (e) { console.error('Error reading file:', e); return null; } } /** * Liest eine Datei als Base64-String. */ async readBinaryFile(filePath) { try { const fullPath = `${this.baseFolder}/${filePath}`; const result = await Filesystem.readFile({ path: fullPath, directory: this.directory }); return result.data; // returns base64 } catch (e) { console.error('Error reading binary file:', e); return null; } } /** * Speichert einen Text in einer Datei (überschreibt bestehende). */ async writeFile(filePath, data) { try { const fullPath = `${this.baseFolder}/${filePath}`; // Ensure parent directory exists first by getting the dir part const parts = fullPath.split('/'); parts.pop(); if (parts.length > 1) { const parentDir = parts.join('/'); try { await Filesystem.mkdir({ path: parentDir, directory: this.directory, recursive: true }); } catch (e) { // ignore "Directory exists" error } } await Filesystem.writeFile({ path: fullPath, data: data, directory: this.directory, encoding: Encoding.UTF8 }); return true; } catch (e) { console.error('Error writing file:', e); return false; } } /** * Speichert eine binäre Datei (z.B. PDF) aus einem Base64 String. */ async writeBinaryFile(filePath, base64Data) { try { const fullPath = `${this.baseFolder}/${filePath}`; const parts = fullPath.split('/'); parts.pop(); if (parts.length > 1) { const parentDir = parts.join('/'); try { await Filesystem.mkdir({ path: parentDir, directory: this.directory, recursive: true }); } catch (e) {} } await Filesystem.writeFile({ path: fullPath, data: base64Data, // without encoding it writes binary directory: this.directory }); return true; } catch (e) { console.error('Error writing binary file:', e); return false; } } /** * Erstellt einen neuen Ordner. */ async createFolder(path) { try { const fullPath = `${this.baseFolder}/${path}`; await Filesystem.mkdir({ path: fullPath, directory: this.directory, recursive: true }); return true; } catch (e) { console.error('Error creating folder:', e); return false; } } /** * Löscht eine Datei oder einen Ordner. */ async deletePath(path, isDir = false) { try { const fullPath = `${this.baseFolder}/${path}`; if (isDir) { await Filesystem.rmdir({ path: fullPath, directory: this.directory, recursive: true }); } else { await Filesystem.deleteFile({ path: fullPath, directory: this.directory }); } return true; } catch (e) { console.error('Error deleting path:', e); return false; } } /** * Verschiebt eine Datei oder einen Ordner in den Papierkorb (.trash). */ async moveToTrash(path, isDir = false) { try { const id = 'trash_' + Date.now(); const fromPath = `${this.baseFolder}/${path}`; const toPath = `${this.baseFolder}/.trash/${id}`; await Filesystem.rename({ from: fromPath, to: toPath, directory: this.directory }); db.addTrashItem(id, path, isDir); return true; } catch (e) { console.error('Error moving to trash:', e); alert('Konnte Element nicht in den Papierkorb verschieben: ' + e.message); return false; } } /** * Stellt ein Element aus dem Papierkorb wieder her. */ async restoreFromTrash(id) { try { const items = db.getTrashItems(); const item = items.find(i => i.id === id); if (!item) return false; const fromPath = `${this.baseFolder}/.trash/${id}`; let toPath = `${this.baseFolder}/${item.original_path}`; // Verify the source file actually exists try { await Filesystem.stat({ path: fromPath, directory: this.directory }); } catch (e) { // If it doesn't exist, remove from DB and clean up the UI db.removeTrashItem(id); alert('Dieses Element konnte nicht wiederhergestellt werden, da die eigentliche Datei nicht mehr im Papierkorb-Ordner gefunden wurde (vermutlich wurde sie bereits gelöscht). Der fehlerhafte Eintrag wird jetzt entfernt.'); return false; } // Ensure parent directory exists for the restored item const parts = toPath.split('/'); parts.pop(); if (parts.length > 1) { const parentDir = parts.join('/'); try { await Filesystem.mkdir({ path: parentDir, directory: this.directory, recursive: true }); } catch (e) {} } // Conflict resolution: Check if destination already exists let finalToPath = toPath; let suffix = 1; let nameWithoutExt = item.original_path; let ext = ''; const isDir = item.is_dir === 1; if (!isDir && item.original_path.includes('.')) { const lastDot = item.original_path.lastIndexOf('.'); nameWithoutExt = item.original_path.substring(0, lastDot); ext = item.original_path.substring(lastDot); } while (true) { try { await Filesystem.stat({ path: finalToPath, directory: this.directory }); // If stat succeeds, file exists! We must try a new name. finalToPath = `${this.baseFolder}/${nameWithoutExt} (${suffix})${ext}`; suffix++; } catch (e) { // Stat failed, meaning file does NOT exist. We found a valid path! break; } } await Filesystem.rename({ from: fromPath, to: finalToPath, directory: this.directory }); // If the name changed and it was a folder, we might want to update metadata if needed, but it's okay, it just loses its custom icon if renamed. db.removeTrashItem(id); return true; } catch (e) { console.error('Error restoring from trash:', e); alert('Restore Error: ' + e.message); return false; } } /** * Löscht endgültig alle Elemente, die älter als 30 Tage sind. */ async cleanupTrash() { try { const items = db.getTrashItems(); const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; const now = Date.now(); for (const item of items) { const deletedAt = new Date(item.deleted_at).getTime(); if (now - deletedAt > THIRTY_DAYS_MS) { const isDir = item.is_dir === 1; const success = await this.deletePath(`.trash/${item.id}`, isDir); if (success) { db.removeTrashItem(item.id); } } } } catch (e) { console.error('Error during trash cleanup:', e); } } } export const fsService = new FileSystemService();