Refactor file handling to exclusively use Nextcloud WebDAV storage, removing local fallback logic

This commit is contained in:
Leon Bösche
2026-01-10 21:46:12 +01:00
parent 6c864612db
commit e64925b438

View File

@@ -1057,60 +1057,25 @@ func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
return return
} }
// Attempt WebDAV upload when configured // ONLY use Nextcloud WebDAV storage
storedPath := filepath.ToSlash(filepath.Join(parentPath, header.Filename)) storedPath := filepath.ToSlash(filepath.Join(parentPath, header.Filename))
if !strings.HasPrefix(storedPath, "/") { if !strings.HasPrefix(storedPath, "/") {
storedPath = "/" + storedPath storedPath = "/" + storedPath
} }
written := int64(len(data)) written := int64(len(data))
if storageClient != nil {
// Build remote path under /orgs/<orgId>
rel := strings.TrimPrefix(storedPath, "/")
remotePath := path.Join("/orgs", orgID.String(), rel)
if err = storageClient.Upload(r.Context(), remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
// Log and fallback to local disk
errors.LogError(r, err, "WebDAV upload failed, falling back to local disk")
} else {
// success -> persist metadata and return
f, err = db.CreateFile(r.Context(), &orgID, &userID, header.Filename, storedPath, "file", written)
if err != nil {
errors.LogError(r, err, "Failed to create org file")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
return
}
auditLogger.Log(r.Context(), audit.Entry{ if storageClient == nil {
UserID: &userID, errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
OrgID: &orgID,
Action: "upload_file",
Success: true,
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
return
}
}
// Fallback: Save to temp directory (WebDAV should be the primary storage)
baseDir := filepath.Join("/tmp", "uploads", "orgs", orgID.String())
targetDir := filepath.Join(baseDir, parentPath)
if err = os.MkdirAll(targetDir, 0o755); err != nil {
errors.LogError(r, err, "Failed to create target dir in /tmp")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
return
}
outPath := filepath.Join(targetDir, header.Filename)
if err = os.WriteFile(outPath, data, 0o644); err != nil {
errors.LogError(r, err, "Failed to write file to /tmp")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
return return
} }
// Store metadata in DB; store path relative to workspace root // Build remote path under /orgs/<orgId>
storedPath = filepath.ToSlash(filepath.Join(parentPath, header.Filename)) rel := strings.TrimPrefix(storedPath, "/")
if !strings.HasPrefix(storedPath, "/") { remotePath := path.Join("/orgs", orgID.String(), rel)
storedPath = "/" + storedPath if err = storageClient.Upload(r.Context(), remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
errors.LogError(r, err, "WebDAV upload failed")
errors.WriteError(w, errors.CodeInternal, "Failed to upload file to storage", http.StatusInternalServerError)
return
} }
f, err = db.CreateFile(r.Context(), &orgID, &userID, header.Filename, storedPath, "file", written) f, err = db.CreateFile(r.Context(), &orgID, &userID, header.Filename, storedPath, "file", written)
if err != nil { if err != nil {
@@ -1248,51 +1213,21 @@ func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
} }
written := int64(len(data)) written := int64(len(data))
fmt.Printf("[DEBUG] Upload: user=%s, file=%s, size=%d, path=%s\n", userID.String(), header.Filename, len(data), storedPath) fmt.Printf("[DEBUG] Upload: user=%s, file=%s, size=%d, path=%s\n", userID.String(), header.Filename, len(data), storedPath)
if storageClient != nil {
rel := strings.TrimPrefix(storedPath, "/")
remotePath := path.Join("/users", userID.String(), rel)
fmt.Printf("[DEBUG] Uploading to WebDAV: %s\n", remotePath)
if err = storageClient.Upload(r.Context(), remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
errors.LogError(r, err, "WebDAV upload failed, falling back to local disk")
} else {
f, err = db.CreateFile(r.Context(), nil, &userID, header.Filename, storedPath, "file", written)
if err != nil {
errors.LogError(r, err, "Failed to create user file")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
return
}
auditLogger.Log(r.Context(), audit.Entry{ // ONLY use Nextcloud WebDAV storage
UserID: &userID, if storageClient == nil {
Action: "upload_user_file", errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
Success: true,
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"id": f.ID})
return
}
}
// Fallback: write to temp directory (WebDAV should be the primary storage)
fmt.Printf("[DEBUG] WebDAV is nil or failed, using local storage fallback\n")
baseDir := filepath.Join("/tmp", "uploads", "users", userID.String())
targetDir := filepath.Join(baseDir, parentPath)
fmt.Printf("[DEBUG] Creating directory: %s\n", targetDir)
if err = os.MkdirAll(targetDir, 0o755); err != nil {
errors.LogError(r, err, "Failed to create target dir in /tmp")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
return return
} }
outPath := filepath.Join(targetDir, header.Filename)
fmt.Printf("[DEBUG] Writing file to: %s\n", outPath) rel := strings.TrimPrefix(storedPath, "/")
if err = os.WriteFile(outPath, data, 0o644); err != nil { remotePath := path.Join("/users", userID.String(), rel)
fmt.Printf("[DEBUG] Failed to write file: %v\n", err) fmt.Printf("[DEBUG] Uploading to WebDAV: %s\n", remotePath)
errors.LogError(r, err, "Failed to write file to /tmp") if err = storageClient.Upload(r.Context(), remotePath, bytes.NewReader(data), int64(len(data))); err != nil {
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) errors.LogError(r, err, "WebDAV upload failed")
errors.WriteError(w, errors.CodeInternal, "Failed to upload file to storage", http.StatusInternalServerError)
return return
} }
fmt.Printf("[DEBUG] File written successfully to local storage: %s\n", outPath)
f, err = db.CreateFile(r.Context(), nil, &userID, header.Filename, storedPath, "file", written) f, err = db.CreateFile(r.Context(), nil, &userID, header.Filename, storedPath, "file", written)
if err != nil { if err != nil {
@@ -1399,58 +1334,26 @@ func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database
return return
} }
// Try to download from Nextcloud first // ONLY use Nextcloud WebDAV storage
if storageClient != nil { if storageClient == nil {
rel := strings.TrimPrefix(filePath, "/") errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
remotePath := path.Join("/orgs", orgID.String(), rel)
reader, size, err := storageClient.Download(r.Context(), remotePath)
if err == nil {
defer reader.Close()
// Set appropriate headers for inline viewing
fileName := path.Base(filePath)
// Determine content type based on file extension
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 size > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
}
// Stream the file
io.Copy(w, reader)
return
}
errors.LogError(r, err, "Failed to download from Nextcloud, trying local storage")
}
// 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 return
} }
f, err := os.Open(localPath) rel := strings.TrimPrefix(filePath, "/")
remotePath := path.Join("/orgs", orgID.String(), rel)
reader, size, err := storageClient.Download(r.Context(), remotePath)
if err != nil { if err != nil {
errors.LogError(r, err, "Failed to download from Nextcloud")
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
return return
} }
defer f.Close() defer reader.Close()
info, _ := f.Stat()
// Set appropriate headers for inline viewing
fileName := path.Base(filePath) fileName := path.Base(filePath)
// Determine content type based on file extension
contentType := "application/octet-stream" contentType := "application/octet-stream"
if strings.HasSuffix(strings.ToLower(fileName), ".pdf") { if strings.HasSuffix(strings.ToLower(fileName), ".pdf") {
contentType = "application/pdf" contentType = "application/pdf"
@@ -1461,10 +1364,13 @@ func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database
} }
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName)) w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName))
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
if info != nil { if size > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size())) w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
} }
io.Copy(w, f)
// Stream the file
io.Copy(w, reader)
} }
// downloadUserFileHandler downloads a file from user's personal workspace // downloadUserFileHandler downloads a file from user's personal workspace
@@ -1485,61 +1391,28 @@ func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *databas
// Log the requested file path for debugging // Log the requested file path for debugging
fmt.Printf("[DEBUG] Download request - User: %s, Path: %s\n", userID.String(), filePath) fmt.Printf("[DEBUG] Download request - User: %s, Path: %s\n", userID.String(), filePath)
// Try to download from Nextcloud first // ONLY use Nextcloud WebDAV storage
if storageClient != nil { if storageClient == nil {
rel := strings.TrimPrefix(filePath, "/") errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
// Keep remote user workspace path consistent with uploads: "/users/<userID>/<rel>"
remotePath := path.Join("/users", userID.String(), rel)
fmt.Printf("[DEBUG] Trying WebDAV path: %s\n", remotePath)
reader, size, err := storageClient.Download(r.Context(), remotePath)
if err == nil {
defer reader.Close()
// Set appropriate headers for inline viewing
fileName := path.Base(filePath)
// Determine content type based on file extension
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 size > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
}
// Stream the file
io.Copy(w, reader)
return
}
errors.LogError(r, err, "Failed to download from Nextcloud, trying local storage")
}
// 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)
fmt.Printf("[DEBUG] Trying local path: %s\n", localPath)
// 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 return
} }
f, err := os.Open(localPath) rel := strings.TrimPrefix(filePath, "/")
// Keep remote user workspace path consistent with uploads: "/users/<userID>/<rel>"
remotePath := path.Join("/users", userID.String(), rel)
fmt.Printf("[DEBUG] Trying WebDAV path: %s\n", remotePath)
reader, size, err := storageClient.Download(r.Context(), remotePath)
if err != nil { if err != nil {
errors.LogError(r, err, "Failed to download from Nextcloud")
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound) errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
return return
} }
defer f.Close() defer reader.Close()
info, _ := f.Stat()
// Set appropriate headers for inline viewing
fileName := path.Base(filePath) fileName := path.Base(filePath)
// Determine content type based on file extension
contentType := "application/octet-stream" contentType := "application/octet-stream"
if strings.HasSuffix(strings.ToLower(fileName), ".pdf") { if strings.HasSuffix(strings.ToLower(fileName), ".pdf") {
contentType = "application/pdf" contentType = "application/pdf"
@@ -1550,8 +1423,11 @@ func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *databas
} }
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName)) w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", fileName))
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
if info != nil { if size > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size())) w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
} }
io.Copy(w, f)
// Stream the file
io.Copy(w, reader)
} }