prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

projects.go (3741B)


      1 package handlers
      2 
      3 import (
      4 	"errors"
      5 	"html/template"
      6 	"mime"
      7 	"net/http"
      8 	"path/filepath"
      9 	"strings"
     10 
     11 	"git.prime8.dev/moncheeta/prime8dev/internal/config"
     12 	"git.prime8.dev/moncheeta/prime8dev/internal/content"
     13 	"git.prime8.dev/moncheeta/prime8dev/internal/external"
     14 	"git.prime8.dev/moncheeta/prime8dev/internal/utils"
     15 	"git.prime8.dev/moncheeta/prime8dev/internal/web"
     16 	"github.com/go-chi/chi/v5"
     17 )
     18 
     19 func (h *Handler) ProjectsPageHandler(w http.ResponseWriter, r *http.Request) {
     20 	id := strings.TrimSpace(chi.URLParam(r, "id"))
     21 	about := config.GetAbout()
     22 	projects, err := content.LoadProjects(h.gitService)
     23 	if err != nil {
     24 		http.Error(w, "Failed to load projects", http.StatusInternalServerError)
     25 		return
     26 	}
     27 
     28 	data := web.BaseData("Projects", "projects", about)
     29 	data.Projects = projects
     30 
     31 	if id == "" {
     32 		if len(projects) > 0 {
     33 			applyProjectDetailData(&data, &projects[0])
     34 		}
     35 
     36 		if utils.IsHTMXRequest(r) {
     37 			h.renderTemplate(w, r, "projects", data, detailMaxAge(data, fragmentCacheMaxAgeSeconds))
     38 			return
     39 		}
     40 
     41 		h.renderTemplate(w, r, "layout", data, detailMaxAge(data, fullPageCacheMaxAgeSeconds))
     42 		return
     43 	}
     44 
     45 	project, err := content.GetProjectByID(h.gitService, id)
     46 	if err != nil {
     47 		http.Error(w, "Failed to load project", http.StatusInternalServerError)
     48 		return
     49 	}
     50 	if project == nil {
     51 		http.NotFound(w, r)
     52 		return
     53 	}
     54 
     55 	data.SelectedProjectID = id
     56 	applyProjectDetailData(&data, project)
     57 
     58 	h.renderTemplate(w, r, "layout", data, detailMaxAge(data, fullPageCacheMaxAgeSeconds))
     59 }
     60 
     61 // detailMaxAge never lets a browser cache a page whose README failed to
     62 // load, so a transient fetch error isn't pinned for an hour.
     63 func detailMaxAge(data web.Data, maxAge int) int {
     64 	if data.Project != nil && strings.TrimSpace(string(data.ProjectReadme)) == "" {
     65 		return 0
     66 	}
     67 	return maxAge
     68 }
     69 
     70 func applyProjectDetailData(data *web.Data, project *config.Project) {
     71 	if data == nil || project == nil {
     72 		return
     73 	}
     74 
     75 	data.Project = project
     76 	data.ProjectReadme = template.HTML(project.HTML)
     77 }
     78 
     79 func (h *Handler) ProjectDetailHandler(w http.ResponseWriter, r *http.Request) {
     80 	id := chi.URLParam(r, "id")
     81 	project, err := content.GetProjectByID(h.gitService, id)
     82 	if err != nil {
     83 		http.Error(w, "Failed to load project", http.StatusInternalServerError)
     84 		return
     85 	}
     86 	if project == nil {
     87 		http.Error(w, "Project not found", http.StatusNotFound)
     88 		return
     89 	}
     90 
     91 	data := web.Data{
     92 		Project:       project,
     93 		ProjectReadme: template.HTML(project.HTML),
     94 	}
     95 
     96 	h.renderTemplate(w, r, "project_detail", data, detailMaxAge(data, fragmentCacheMaxAgeSeconds))
     97 }
     98 
     99 // ProjectRawHandler serves a file from HEAD of a public project's repo, so
    100 // README images and links resolve without a separate git web frontend.
    101 func (h *Handler) ProjectRawHandler(w http.ResponseWriter, r *http.Request) {
    102 	project := config.GetProjectByID(chi.URLParam(r, "id"))
    103 	if project == nil {
    104 		http.NotFound(w, r)
    105 		return
    106 	}
    107 
    108 	path := chi.URLParam(r, "*")
    109 	body, err := h.gitService.ReadFile(r.Context(), *project, path)
    110 	if errors.Is(err, external.ErrNotFound) {
    111 		http.NotFound(w, r)
    112 		return
    113 	}
    114 	if err != nil {
    115 		http.Error(w, "Failed to read file", http.StatusInternalServerError)
    116 		return
    117 	}
    118 
    119 	contentType := mime.TypeByExtension(filepath.Ext(path))
    120 	if contentType == "" || strings.HasPrefix(contentType, "text/") {
    121 		contentType = http.DetectContentType(body)
    122 		if strings.HasPrefix(contentType, "text/") {
    123 			contentType = "text/plain; charset=utf-8"
    124 		}
    125 	}
    126 
    127 	w.Header().Set("Content-Type", contentType)
    128 	w.Header().Set("X-Content-Type-Options", "nosniff")
    129 	w.Header().Set("Content-Security-Policy", "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox")
    130 	w.Header().Set("Cache-Control", "public, max-age=300")
    131 	_, _ = w.Write(body)
    132 }