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 }