package handler import ( "fmt" "io" "net/http" "os" "path/filepath" "strings" ) const uploadsDir = "data/uploads" func (h *Handler) handleFileUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } profile := getProfile(r.Context()) err := r.ParseMultipartForm(32 << 20) // 32MB max if err != nil { http.Error(w, "Error parsing form", 400) return } dealID := r.FormValue("deal_id") folderID := r.FormValue("folder_id") requestItemID := r.FormValue("request_item_id") if dealID == "" { http.Error(w, "Deal ID is required", 400) return } file, header, err := r.FormFile("file") if err != nil { http.Error(w, "File is required", 400) return } defer file.Close() // Create upload directory dealDir := filepath.Join(uploadsDir, dealID) os.MkdirAll(dealDir, 0755) // Generate unique filename fileID := generateID("file") ext := filepath.Ext(header.Filename) storageName := fileID + ext storagePath := filepath.Join(dealDir, storageName) // Save file to disk dst, err := os.Create(storagePath) if err != nil { http.Error(w, "Error saving file", 500) return } defer dst.Close() io.Copy(dst, file) // Detect MIME type mimeType := header.Header.Get("Content-Type") if mimeType == "" { mimeType = "application/octet-stream" } // Insert file record _, err = h.db.Exec(`INSERT INTO files (id, deal_id, folder_id, name, file_size, mime_type, status, storage_path, uploaded_by) VALUES (?, ?, ?, ?, ?, ?, 'uploaded', ?, ?)`, fileID, dealID, folderID, header.Filename, header.Size, mimeType, storagePath, profile.ID) if err != nil { os.Remove(storagePath) http.Error(w, "Error saving file record", 500) return } // Link to request item if specified if requestItemID != "" { var existing string h.db.QueryRow("SELECT linked_file_ids FROM diligence_requests WHERE id = ?", requestItemID).Scan(&existing) if existing == "" { existing = fileID } else { existing = existing + "," + fileID } h.db.Exec("UPDATE diligence_requests SET linked_file_ids = ? WHERE id = ?", existing, requestItemID) } // Log activity h.logActivity(dealID, profile.ID, profile.OrganizationID, "upload", "file", header.Filename, fileID) http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther) } func (h *Handler) handleFileDelete(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } profile := getProfile(r.Context()) if profile.Role != "owner" && profile.Role != "admin" { http.Error(w, "Forbidden", http.StatusForbidden) return } fileID := r.FormValue("file_id") dealID := r.FormValue("deal_id") if fileID == "" { http.Error(w, "Missing file ID", 400) return } // Get storage path and delete from disk var storagePath string h.db.QueryRow("SELECT COALESCE(storage_path, '') FROM files WHERE id = ?", fileID).Scan(&storagePath) if storagePath != "" { os.Remove(storagePath) } h.db.Exec("DELETE FROM files WHERE id = ? AND deal_id = ?", fileID, dealID) http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther) } func (h *Handler) handleFileDownload(w http.ResponseWriter, r *http.Request) { profile := getProfile(r.Context()) // Parse: /deals/files/download/{fileID} fileID := strings.TrimPrefix(r.URL.Path, "/deals/files/download/") if fileID == "" { http.Error(w, "Missing file ID", 400) return } var name, storagePath, dealID string err := h.db.QueryRow("SELECT name, COALESCE(storage_path, ''), deal_id FROM files WHERE id = ?", fileID).Scan(&name, &storagePath, &dealID) if err != nil { http.NotFound(w, r) return } if storagePath == "" || !fileExists(storagePath) { http.Error(w, "File not found on disk", 404) return } // Log download activity h.logActivity(dealID, profile.ID, profile.OrganizationID, "download", "file", name, fileID) w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, name)) http.ServeFile(w, r, storagePath) } func (h *Handler) logActivity(dealID, userID, orgID, actType, resourceType, resourceName, resourceID string) { id := generateID("act") h.db.Exec(`INSERT INTO deal_activity (id, organization_id, deal_id, user_id, activity_type, resource_type, resource_name, resource_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, id, orgID, dealID, userID, actType, resourceType, resourceName, resourceID) } func (h *Handler) handleFileComments(w http.ResponseWriter, r *http.Request) { profile := getProfile(r.Context()) // Parse: /deals/files/comments/{fileID} fileID := strings.TrimPrefix(r.URL.Path, "/deals/files/comments/") // Get deal_id for this file var dealID string h.db.QueryRow("SELECT deal_id FROM files WHERE id = ?", fileID).Scan(&dealID) if r.Method == http.MethodPost { content := strings.TrimSpace(r.FormValue("content")) if content != "" { id := generateID("cmt") h.db.Exec("INSERT INTO file_comments (id, file_id, deal_id, user_id, content) VALUES (?, ?, ?, ?, ?)", id, fileID, dealID, profile.ID, content) } } // Get comments rows, err := h.db.Query(` SELECT c.id, c.content, c.created_at, COALESCE(p.full_name, 'Unknown') FROM file_comments c LEFT JOIN profiles p ON c.user_id = p.id WHERE c.file_id = ? ORDER BY c.created_at ASC`, fileID) if err != nil { http.Error(w, "Error loading comments", 500) return } defer rows.Close() w.Header().Set("Content-Type", "text/html") // Build HTML response html := `
` hasComments := false for rows.Next() { hasComments = true var id, content, userName string var createdAt string rows.Scan(&id, &content, &createdAt, &userName) html += fmt.Sprintf(`
%s %s

%s

`, userName, createdAt, content) } if !hasComments { html += `

No comments yet.

` } html += `
` // Add comment form html += fmt.Sprintf(`
`, fileID, fileID) w.Write([]byte(html)) } func fileExists(path string) bool { _, err := os.Stat(path) return err == nil }