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,61 +1057,26 @@ func createOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database.D
return
}
// Attempt WebDAV upload when configured
// ONLY use Nextcloud WebDAV storage
storedPath := filepath.ToSlash(filepath.Join(parentPath, header.Filename))
if !strings.HasPrefix(storedPath, "/") {
storedPath = "/" + storedPath
}
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{
UserID: &userID,
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)
if storageClient == nil {
errors.WriteError(w, errors.CodeInternal, "Storage not configured", 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)
// 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 {
errors.LogError(r, err, "WebDAV upload failed")
errors.WriteError(w, errors.CodeInternal, "Failed to upload file to storage", http.StatusInternalServerError)
return
}
// Store metadata in DB; store path relative to workspace root
storedPath = filepath.ToSlash(filepath.Join(parentPath, header.Filename))
if !strings.HasPrefix(storedPath, "/") {
storedPath = "/" + storedPath
}
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")
@@ -1248,51 +1213,21 @@ func createUserFileHandler(w http.ResponseWriter, r *http.Request, db *database.
}
written := int64(len(data))
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{
UserID: &userID,
Action: "upload_user_file",
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)
// ONLY use Nextcloud WebDAV storage
if storageClient == nil {
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
return
}
outPath := filepath.Join(targetDir, header.Filename)
fmt.Printf("[DEBUG] Writing file to: %s\n", outPath)
if err = os.WriteFile(outPath, data, 0o644); err != nil {
fmt.Printf("[DEBUG] Failed to write file: %v\n", err)
errors.LogError(r, err, "Failed to write file to /tmp")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
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")
errors.WriteError(w, errors.CodeInternal, "Failed to upload file to storage", http.StatusInternalServerError)
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)
if err != nil {
@@ -1399,58 +1334,26 @@ func downloadOrgFileHandler(w http.ResponseWriter, r *http.Request, db *database
return
}
// Try to download from Nextcloud first
if storageClient != nil {
rel := strings.TrimPrefix(filePath, "/")
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)
// ONLY use Nextcloud WebDAV storage
if storageClient == nil {
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
return
}
rel := strings.TrimPrefix(filePath, "/")
remotePath := path.Join("/orgs", orgID.String(), rel)
f, err := os.Open(localPath)
reader, size, err := storageClient.Download(r.Context(), remotePath)
if err != nil {
errors.LogError(r, err, "Failed to download from Nextcloud")
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
return
}
defer f.Close()
info, _ := f.Stat()
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"
@@ -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-Type", contentType)
if info != nil {
w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size()))
if size > 0 {
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
@@ -1485,61 +1391,28 @@ func downloadUserFileHandler(w http.ResponseWriter, r *http.Request, db *databas
// Log the requested file path for debugging
fmt.Printf("[DEBUG] Download request - User: %s, Path: %s\n", userID.String(), filePath)
// Try to download from Nextcloud first
if storageClient != nil {
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 {
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)
// ONLY use Nextcloud WebDAV storage
if storageClient == nil {
errors.WriteError(w, errors.CodeInternal, "Storage not configured", http.StatusInternalServerError)
return
}
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)
f, err := os.Open(localPath)
reader, size, err := storageClient.Download(r.Context(), remotePath)
if err != nil {
errors.LogError(r, err, "Failed to download from Nextcloud")
errors.WriteError(w, errors.CodeNotFound, "File not found", http.StatusNotFound)
return
}
defer f.Close()
info, _ := f.Stat()
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"
@@ -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-Type", contentType)
if info != nil {
w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size()))
if size > 0 {
w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
}
io.Copy(w, f)
// Stream the file
io.Copy(w, reader)
}