Refactor file download handlers to implement local storage fallback and enhance organization creation with slug generation
This commit is contained in:
@@ -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)
|
||||
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
|
||||
// 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)
|
||||
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user