prime8dev

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

projects.go (3888B)


      1 package content
      2 
      3 import (
      4 	"context"
      5 	"log/slog"
      6 	"strings"
      7 	"sync"
      8 	"time"
      9 
     10 	"git.prime8.dev/moncheeta/prime8dev/internal/config"
     11 	"git.prime8.dev/moncheeta/prime8dev/internal/external"
     12 	"git.prime8.dev/moncheeta/prime8dev/internal/utils"
     13 	"github.com/patrickmn/go-cache"
     14 )
     15 
     16 const (
     17 	projectsCacheKey     = "projects"
     18 	projectsCacheTTL     = 1 * time.Hour
     19 	projectsCacheCleanup = 4 * time.Hour
     20 )
     21 
     22 var (
     23 	projectsCache             = cache.New(projectsCacheTTL, projectsCacheCleanup)
     24 	projectsRefreshMu         sync.Mutex
     25 	projectsRefreshInProgress bool
     26 )
     27 
     28 func LoadProjects(gitService *external.Git) ([]config.Project, error) {
     29 	if gitService == nil {
     30 		gitService = external.NewGit()
     31 	}
     32 
     33 	if cached, found := projectsCache.Get(projectsCacheKey); found {
     34 		if projects, ok := cached.([]config.Project); ok {
     35 			return cloneProjects(projects), nil
     36 		}
     37 	}
     38 
     39 	projects := config.GetProjects()
     40 	projectsCache.Set(projectsCacheKey, projects, projectsCacheTTL)
     41 	startProjectsRefresh(gitService)
     42 
     43 	return cloneProjects(projects), nil
     44 }
     45 
     46 func GetProjectByID(gitService *external.Git, id string) (*config.Project, error) {
     47 	id = strings.TrimSpace(id)
     48 	if id == "" {
     49 		return nil, nil
     50 	}
     51 
     52 	projects, err := LoadProjects(gitService)
     53 	if err != nil {
     54 		return nil, err
     55 	}
     56 
     57 	for i := range projects {
     58 		if projects[i].ID != id {
     59 			continue
     60 		}
     61 		if gitService != nil && strings.TrimSpace(projects[i].Markdown) == "" && strings.TrimSpace(projects[i].HTML) == "" {
     62 			if readme, err := fetchProjectReadme(gitService, projects[i]); err == nil {
     63 				projects[i].Markdown = readme.Markdown
     64 				projects[i].HTML = readme.HTML
     65 				projectsCache.Set(projectsCacheKey, cloneProjects(projects), projectsCacheTTL)
     66 			}
     67 		}
     68 
     69 		project := projects[i]
     70 		return &project, nil
     71 	}
     72 
     73 	return nil, nil
     74 }
     75 
     76 func fetchProjects(gitService *external.Git) ([]config.Project, error) {
     77 	projects := config.GetProjects()
     78 	for i := range projects {
     79 		readme, err := fetchProjectReadme(gitService, projects[i])
     80 		if err != nil {
     81 			slog.Warn("project metadata fetch failed", "project_id", projects[i].ID, "error", err)
     82 			continue
     83 		}
     84 
     85 		projects[i].Markdown = readme.Markdown
     86 		projects[i].HTML = readme.HTML
     87 	}
     88 
     89 	return projects, nil
     90 }
     91 
     92 func fetchProjectReadme(gitService *external.Git, project config.Project) (config.Project, error) {
     93 	metadata, err := gitService.FetchMetadata(context.Background(), project)
     94 	if err != nil {
     95 		return config.Project{}, err
     96 	}
     97 	if metadata == nil {
     98 		return config.Project{}, nil
     99 	}
    100 
    101 	html, err := projectReadmeHTML(metadata)
    102 	if err != nil {
    103 		slog.Warn("project readme markdown render failed", "project_id", project.ID, "error", err)
    104 		return config.Project{}, err
    105 	}
    106 
    107 	return config.Project{
    108 		Markdown: strings.TrimSpace(metadata.Readme),
    109 		HTML:     strings.TrimSpace(html),
    110 	}, nil
    111 }
    112 
    113 func projectReadmeHTML(metadata *external.GitMetadata) (string, error) {
    114 	if metadata == nil {
    115 		return "", nil
    116 	}
    117 
    118 	markdown := strings.TrimSpace(metadata.Readme)
    119 	if markdown == "" {
    120 		return "", nil
    121 	}
    122 
    123 	rendered, err := utils.MarkdownToHTML(utils.RemoveFirstH1(markdown))
    124 	if err != nil {
    125 		return "", err
    126 	}
    127 
    128 	return rendered, nil
    129 }
    130 
    131 func cloneProjects(projects []config.Project) []config.Project {
    132 	cloned := make([]config.Project, len(projects))
    133 	copy(cloned, projects)
    134 	return cloned
    135 }
    136 
    137 func startProjectsRefresh(gitService *external.Git) {
    138 	if gitService == nil {
    139 		return
    140 	}
    141 
    142 	projectsRefreshMu.Lock()
    143 	if projectsRefreshInProgress {
    144 		projectsRefreshMu.Unlock()
    145 		return
    146 	}
    147 	projectsRefreshInProgress = true
    148 	projectsRefreshMu.Unlock()
    149 
    150 	go func() {
    151 		defer func() {
    152 			projectsRefreshMu.Lock()
    153 			projectsRefreshInProgress = false
    154 			projectsRefreshMu.Unlock()
    155 		}()
    156 
    157 		projects, err := fetchProjects(gitService)
    158 		if err != nil {
    159 			slog.Warn("project metadata background refresh failed", "error", err)
    160 			return
    161 		}
    162 
    163 		projectsCache.Set(projectsCacheKey, projects, projectsCacheTTL)
    164 	}()
    165 }