// Package fileutil enthält gemeinsame Datei-Hilfsfunktionen für Upload-Handler (V1, N6). package fileutil import ( "fmt" "io" "os" "path/filepath" "strings" "time" ) // SaveUploadedFile speichert einen Datei-Stream in uploadDir/{tenantSlug}/ und // gibt den relativen HTTP-Pfad (/uploads/{tenantSlug}/filename) sowie die // Anzahl geschriebener Bytes zurück. // // V1: Gemeinsame Upload-Logik — ersetzt 3× duplizierte Implementierung. // N6: Tenant-spezifisches Verzeichnis statt gemeinsamer Ablage. func SaveUploadedFile(file io.Reader, originalFilename, title, uploadDir, tenantSlug string) (storagePath string, size int64, err error) { safeSlug := sanitize(tenantSlug) if safeSlug == "" { safeSlug = "default" } tenantDir := filepath.Join(uploadDir, safeSlug) if mkErr := os.MkdirAll(tenantDir, 0755); mkErr != nil { return "", 0, fmt.Errorf("fileutil: mkdir %s: %w", tenantDir, mkErr) } ext := filepath.Ext(originalFilename) safeTitle := sanitize(title) if safeTitle == "" { safeTitle = "file" } filename := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), safeTitle, ext) destPath := filepath.Join(tenantDir, filename) dest, createErr := os.Create(destPath) if createErr != nil { return "", 0, fmt.Errorf("fileutil: create %s: %w", destPath, createErr) } defer dest.Close() n, copyErr := io.Copy(dest, file) if copyErr != nil { os.Remove(destPath) //nolint:errcheck return "", 0, fmt.Errorf("fileutil: write %s: %w", destPath, copyErr) } return "/uploads/" + safeSlug + "/" + filename, n, nil } // sanitize konvertiert einen String in einen sicheren Dateinamen-Bestandteil // (nur a-z, A-Z, 0-9, -, _; maximal 40 Zeichen). func sanitize(s string) string { var b strings.Builder for _, r := range s { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' { b.WriteRune(r) } else { b.WriteRune('_') } } out := b.String() if len(out) > 40 { out = out[:40] } return out }