Refactor file download handlers to implement local storage fallback and enhance organization creation with slug generation

This commit is contained in:
Leon Bösche
2026-01-10 03:47:35 +01:00
parent f3656fdbd0
commit 0797b407a5
3 changed files with 116 additions and 11 deletions

View File

@@ -402,7 +402,7 @@ class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
}
return Column(
children: [
const SizedBox(height: 80),
const SizedBox(height: 16),
_buildOrgRow(context),
Expanded(
child: _buildDrive(

View File

@@ -1383,8 +1383,38 @@ func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database
errors.LogError(r, err, "Failed to download from Nextcloud, trying local storage")
}
// Fallback to local storage (if implemented)
// Fallback to local disk (used when WebDAV is not configured)
baseDir := filepath.Clean(filepath.Join("/tmp/uploads/orgs", orgID.String()))
rel := strings.TrimPrefix(filePath, "/")
localPath := filepath.Join(baseDir, rel)
// Prevent path traversal escaping the baseDir
if !strings.HasPrefix(localPath, baseDir+string(os.PathSeparator)) && localPath != baseDir {
errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
return
}
f, err := os.Open(localPath)
if err != nil {
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
return
}
defer f.Close()
info, _ := f.Stat()
fileName := path.Base(filePath)
contentType := "application/octet-stream"
if strings.HasSuffix(strings.ToLower(fileName), ".pdf") {
contentType = "application/pdf"
} else if strings.HasSuffix(strings.ToLower(fileName), ".png") {
contentType = "image/png"
} else if strings.HasSuffix(strings.ToLower(fileName), ".jpg") || strings.HasSuffix(strings.ToLower(fileName), ".jpeg") {
contentType = "image/jpeg"
}
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName))
w.Header().Set("Content-Type", contentType)
if info != nil {
w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size()))
}
io.Copy(w, f)
}
// downloadUserFileHandler downloads a file from user's personal workspace
@@ -1437,6 +1467,36 @@ func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *databas
errors.LogError(r, err, "Failed to download from Nextcloud, trying local storage")
}
// Fallback to local storage (if implemented)
// Fallback to local disk (used when WebDAV is not configured)
baseDir := filepath.Clean(filepath.Join("/tmp/uploads/users", userID.String()))
rel := strings.TrimPrefix(filePath, "/")
localPath := filepath.Join(baseDir, rel)
// Prevent path traversal escaping the baseDir
if !strings.HasPrefix(localPath, baseDir+string(os.PathSeparator)) && localPath != baseDir {
errors.WriteError(w, errors.CodeInvalidArgument, "Invalid path", http.StatusBadRequest)
return
}
f, err := os.Open(localPath)
if err != nil {
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
return
}
defer f.Close()
info, _ := f.Stat()
fileName := path.Base(filePath)
contentType := "application/octet-stream"
if strings.HasSuffix(strings.ToLower(fileName), ".pdf") {
contentType = "application/pdf"
} else if strings.HasSuffix(strings.ToLower(fileName), ".png") {
contentType = "image/png"
} else if strings.HasSuffix(strings.ToLower(fileName), ".jpg") || strings.HasSuffix(strings.ToLower(fileName), ".jpeg") {
contentType = "image/jpeg"
}
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName))
w.Header().Set("Content-Type", contentType)
if info != nil {
w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size()))
}
io.Copy(w, f)
}

View File

@@ -2,10 +2,14 @@ package org
import (
"context"
"fmt"
"regexp"
"strings"
"go.b0esche.cloud/backend/internal/database"
"github.com/google/uuid"
"github.com/jackc/pgconn"
)
// ResolveUserOrgs returns the organizations a user belongs to
@@ -24,17 +28,58 @@ func CheckMembership(ctx context.Context, db *database.DB, userID, orgID uuid.UU
// CreateOrg creates a new organization and adds the user as owner
func CreateOrg(ctx context.Context, db *database.DB, userID uuid.UUID, name, slug string) (*database.Organization, error) {
if slug == "" {
// Simple slug generation
slug = name // TODO: make URL safe
trimmedName := strings.TrimSpace(name)
if trimmedName == "" {
return nil, fmt.Errorf("organization name cannot be empty")
}
baseSlug := slugify(slug)
if baseSlug == "" {
baseSlug = slugify(trimmedName)
}
if baseSlug == "" {
baseSlug = fmt.Sprintf("org-%s", uuid.NewString()[:8])
}
var org *database.Organization
var err error
// Try a handful of suffixes on unique constraint violation
for i := 0; i < 5; i++ {
candidate := baseSlug
if i > 0 {
candidate = fmt.Sprintf("%s-%d", baseSlug, i+1)
}
org, err = db.CreateOrg(ctx, trimmedName, candidate)
if err != nil {
if pgErr, ok := err.(*pgconn.PgError); ok && pgErr.Code == "23505" {
// Unique violation; try next suffix
continue
}
return nil, err
}
break
}
org, err := db.CreateOrg(ctx, name, slug)
if err != nil {
return nil, err
}
err = db.AddMembership(ctx, userID, org.ID, "owner")
if err != nil {
if err = db.AddMembership(ctx, userID, org.ID, "owner"); err != nil {
return nil, err
}
return org, nil
}
// slugify converts a string to a URL-safe slug with hyphens.
func slugify(s string) string {
lower := strings.ToLower(strings.TrimSpace(s))
if lower == "" {
return ""
}
// Replace non-alphanumeric with hyphen
re := regexp.MustCompile(`[^a-z0-9]+`)
slug := re.ReplaceAllString(lower, "-")
slug = strings.Trim(slug, "-")
// Collapse multiple hyphens
slug = strings.ReplaceAll(slug, "--", "-")
return slug
}