package handler import ( "fmt" "html/template" "io" "net/http" "os" "path/filepath" "strconv" "strings" "time" "podstalk/store" ) const maxUploadSize = 200 << 20 // 200 MB const timeLayout = "2006-01-02 15:04:05" func fmtTime(t time.Time) string { return t.Format(timeLayout) } type EpisodeHandler struct { Store *store.Store Tpl *Templates UploadDir string BaseURL string } func (h *EpisodeHandler) ServeDashboard(w http.ResponseWriter, r *http.Request) { userID := UserIDFromContext(r) user, err := h.Store.GetUser(userID) if err != nil { http.Error(w, "user not found", http.StatusInternalServerError) return } episodes, err := h.Store.ListEpisodes(userID) if err != nil { http.Error(w, "failed to load episodes", http.StatusInternalServerError) return } title := user.PodcastTitle if title == "" { title = "Podstalk" } type epView struct { ID int64 Title string Description string AudioPath string ImageURL template.URL PublishedAt string } var epViews []epView for _, ep := range episodes { pubFormatted := ep.PublishedAt if pt, err := time.Parse(timeLayout, ep.PublishedAt); err == nil { pubFormatted = pt.Format("Jan 2, 2006") } ev := epView{ ID: ep.ID, Title: ep.Title, Description: ep.Description, AudioPath: ep.AudioPath, PublishedAt: pubFormatted, } if ep.ImagePath != "" { ev.ImageURL = template.URL("/uploads/" + ep.ImagePath) } else if user.PodcastImage != "" { ev.ImageURL = template.URL("/uploads/" + user.PodcastImage) } epViews = append(epViews, ev) } podcastImageURL := template.URL("") if user.PodcastImage != "" { podcastImageURL = template.URL("/uploads/" + user.PodcastImage) } h.Tpl.Render(w, "dashboard", map[string]any{ "Episodes": epViews, "PodcastTitle": title, "PodcastSlug": user.PodcastSlug, "PodcastImageURL": podcastImageURL, "PodcastAuthor": user.PodcastAuthor, "RSSURL": template.URL(h.BaseURL + "/rss/" + user.PodcastSlug), }) } func (h *EpisodeHandler) ServeNew(w http.ResponseWriter, r *http.Request) { userID := UserIDFromContext(r) if r.Method == http.MethodGet { h.Tpl.Render(w, "episode_form", map[string]any{"Editing": false}) return } if r.Method == http.MethodPost { if err := r.ParseMultipartForm(maxUploadSize); err != nil { h.Tpl.Render(w, "episode_form", map[string]any{ "Editing": false, "Error": "File too large (max 200 MB).", }) return } title := r.FormValue("title") description := r.FormValue("description") publishedStr := r.FormValue("published_at") publishedAt := time.Now() if publishedStr != "" { if t, err := time.Parse("2006-01-02T15:04", publishedStr); err == nil { publishedAt = t } } if title == "" { h.Tpl.Render(w, "episode_form", map[string]any{ "Editing": false, "Error": "Title is required.", }) return } audioPath, err := h.saveUploadedFile(r, "audio_file", "audio") if err != nil { h.Tpl.Render(w, "episode_form", map[string]any{ "Editing": false, "Error": fmt.Sprintf("Audio upload failed: %v", err), }) return } imagePath, _ := h.saveUploadedFile(r, "image_file", "images") _, err = h.Store.CreateEpisode(userID, title, description, audioPath, imagePath, fmtTime(publishedAt)) if err != nil { h.Tpl.Render(w, "episode_form", map[string]any{ "Editing": false, "Error": "Failed to create episode.", }) return } http.Redirect(w, r, "/?success=episode+created", http.StatusSeeOther) } } func (h *EpisodeHandler) ServeEdit(w http.ResponseWriter, r *http.Request) { idStr := r.URL.Query().Get("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "invalid id", http.StatusBadRequest) return } if r.Method == http.MethodGet { ep, err := h.Store.GetEpisode(id) if err != nil { http.Error(w, "episode not found", http.StatusNotFound) return } pubStr := "" if pt, err := time.Parse(timeLayout, ep.PublishedAt); err == nil { pubStr = pt.Format("2006-01-02T15:04") } h.Tpl.Render(w, "episode_form", map[string]any{ "Editing": true, "Episode": ep, "PublishedAt": pubStr, }) return } if r.Method == http.MethodPost { if err := r.ParseMultipartForm(maxUploadSize); err != nil { http.Error(w, "file too large", http.StatusRequestEntityTooLarge) return } title := r.FormValue("title") description := r.FormValue("description") publishedStr := r.FormValue("published_at") publishedAt := time.Now() if publishedStr != "" { if t, err := time.Parse("2006-01-02T15:04", publishedStr); err == nil { publishedAt = t } } if title == "" { ep, _ := h.Store.GetEpisode(id) h.Tpl.Render(w, "episode_form", map[string]any{ "Editing": true, "Episode": ep, "PublishedAt": publishedStr, "Error": "Title is required.", }) return } existing, err := h.Store.GetEpisode(id) if err != nil { http.Error(w, "episode not found", http.StatusNotFound) return } audioPath := existing.AudioPath if newPath, err := h.saveUploadedFile(r, "audio_file", "audio"); err == nil && newPath != "" { h.removeUploadedFile(audioPath) audioPath = newPath } imagePath := existing.ImagePath if newPath, err := h.saveUploadedFile(r, "image_file", "images"); err == nil && newPath != "" { h.removeUploadedFile(imagePath) imagePath = newPath } if err := h.Store.UpdateEpisode(id, title, description, audioPath, imagePath, fmtTime(publishedAt)); err != nil { http.Error(w, "update failed", http.StatusInternalServerError) return } http.Redirect(w, r, "/?success=episode+updated", http.StatusSeeOther) } } func (h *EpisodeHandler) ServeDelete(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } idStr := r.URL.Query().Get("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { http.Error(w, "invalid id", http.StatusBadRequest) return } ep, err := h.Store.GetEpisode(id) if err == nil { h.removeUploadedFile(ep.AudioPath) h.removeUploadedFile(ep.ImagePath) } if err := h.Store.DeleteEpisode(id); err != nil { http.Error(w, "delete failed", http.StatusInternalServerError) return } http.Redirect(w, r, "/?success=episode+deleted", http.StatusSeeOther) } func (h *EpisodeHandler) ServeUpload(w http.ResponseWriter, r *http.Request) { relPath := strings.TrimPrefix(r.URL.Path, "/uploads/") if relPath == "" || strings.Contains(relPath, "..") { http.Error(w, "invalid path", http.StatusBadRequest) return } http.ServeFile(w, r, filepath.Join(h.UploadDir, relPath)) } func (h *EpisodeHandler) saveUploadedFile(r *http.Request, field, category string) (string, error) { file, header, err := r.FormFile(field) if err != nil { return "", err } defer file.Close() base := sanitizeFilename(header.Filename) ext := filepath.Ext(base) if ext == "" { ext = ".bin" } else { base = base[:len(base)-len(ext)] } filename := fmt.Sprintf("%d_%s%s", time.Now().UnixNano(), base, ext) catDir := filepath.Join(h.UploadDir, category) if err := os.MkdirAll(catDir, 0755); err != nil { return "", err } dst, err := os.Create(filepath.Join(catDir, filename)) if err != nil { return "", err } defer dst.Close() if _, err := io.Copy(dst, file); err != nil { return "", err } return category + "/" + filename, nil } func (h *EpisodeHandler) removeUploadedFile(relPath string) { if relPath == "" { return } os.Remove(filepath.Join(h.UploadDir, relPath)) } func sanitizeFilename(name string) string { name = filepath.Base(name) name = strings.Map(func(r rune) rune { if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '.' || r == '-' { return r } if r >= 'A' && r <= 'Z' { return r + 32 } return -1 }, name) return name }