project.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: Apache-2.0
  4
  5package project
  6
  7import (
  8	"crypto/sha256"
  9	"database/sql"
 10	"fmt"
 11	"log"
 12	"sort"
 13	"strings"
 14	"sync"
 15	"time"
 16
 17	"github.com/unascribed/FlexVer/go/flexver"
 18
 19	"git.sr.ht/~amolith/willow/db"
 20	"git.sr.ht/~amolith/willow/git"
 21	"git.sr.ht/~amolith/willow/rss"
 22)
 23
 24type Project struct {
 25	ID       string
 26	URL      string
 27	Name     string
 28	Forge    string
 29	Running  string
 30	Releases []Release
 31}
 32
 33type Release struct {
 34	ID        string
 35	ProjectID string
 36	URL       string
 37	Tag       string
 38	Content   string
 39	Date      time.Time
 40}
 41
 42// GetReleases returns a list of all releases for a project from the database
 43func GetReleases(dbConn *sql.DB, mu *sync.Mutex, proj Project) (Project, error) {
 44	ret, err := db.GetReleases(dbConn, proj.ID)
 45	if err != nil {
 46		return proj, err
 47	}
 48
 49	if len(ret) == 0 {
 50		return fetchReleases(dbConn, mu, proj)
 51	}
 52
 53	for _, row := range ret {
 54		proj.Releases = append(proj.Releases, Release{
 55			ID:      row["id"],
 56			Tag:     row["tag"],
 57			Content: row["content"],
 58			URL:     row["release_url"],
 59			Date:    time.Time{},
 60		})
 61	}
 62	proj.Releases = SortReleases(proj.Releases)
 63	return proj, nil
 64}
 65
 66// fetchReleases fetches releases from a project's forge given its URI
 67func fetchReleases(dbConn *sql.DB, mu *sync.Mutex, p Project) (Project, error) {
 68	var err error
 69	switch p.Forge {
 70	case "github", "gitea", "forgejo":
 71		rssReleases, err := rss.GetReleases(p.URL)
 72		if err != nil {
 73			fmt.Println("Error getting RSS releases:", err)
 74			return p, err
 75		}
 76		for _, release := range rssReleases {
 77			p.Releases = append(p.Releases, Release{
 78				ID:      GenReleaseID(p.URL, release.URL, release.Tag),
 79				Tag:     release.Tag,
 80				Content: release.Content,
 81				URL:     release.URL,
 82				Date:    release.Date,
 83			})
 84			err = upsertRelease(dbConn, mu, p.URL, p.Releases)
 85			if err != nil {
 86				log.Printf("Error upserting release: %v", err)
 87				return p, err
 88			}
 89		}
 90	default:
 91		gitReleases, err := git.GetReleases(p.URL, p.Forge)
 92		if err != nil {
 93			return p, err
 94		}
 95		for _, release := range gitReleases {
 96			p.Releases = append(p.Releases, Release{
 97				ID:      GenReleaseID(p.URL, release.URL, release.Tag),
 98				Tag:     release.Tag,
 99				Content: release.Content,
100				URL:     release.URL,
101				Date:    release.Date,
102			})
103			err = upsertRelease(dbConn, mu, p.URL, p.Releases)
104			if err != nil {
105				log.Printf("Error upserting release: %v", err)
106				return p, err
107			}
108		}
109	}
110	p.Releases = SortReleases(p.Releases)
111	return p, err
112}
113
114func SortReleases(releases []Release) []Release {
115	sort.Slice(releases, func(i, j int) bool {
116		return !flexver.Less(releases[i].Tag, releases[j].Tag)
117	})
118	return releases
119}
120
121// upsertRelease updates or inserts a release in the database
122func upsertRelease(dbConn *sql.DB, mu *sync.Mutex, url string, releases []Release) error {
123	for _, release := range releases {
124		date := release.Date.Format("2006-01-02 15:04:05")
125		id := GenReleaseID(url, release.URL, release.Tag)
126		err := db.UpsertRelease(dbConn, mu, id, url, release.URL, release.Tag, release.Content, date)
127		if err != nil {
128			log.Printf("Error upserting release: %v", err)
129			return err
130		}
131	}
132	return nil
133}
134
135// GenReleaseID generates a likely-unique ID from its project's URL, its release's URL, and its tag
136func GenReleaseID(projectURL, releaseURL, tag string) string {
137	idByte := sha256.Sum256([]byte(projectURL + releaseURL + tag))
138	return fmt.Sprintf("%x", idByte)
139}
140
141// GenProjectID generates a likely-unique ID from a project's URI, name, and forge
142func GenProjectID(url, name, forge string) string {
143	idByte := sha256.Sum256([]byte(url + name + forge))
144	return fmt.Sprintf("%x", idByte)
145}
146
147func Track(dbConn *sql.DB, mu *sync.Mutex, manualRefresh *chan struct{}, name, url, forge, release string) {
148	id := GenProjectID(url, name, forge)
149	err := db.UpsertProject(dbConn, mu, id, url, name, forge, release)
150	if err != nil {
151		fmt.Println("Error upserting project:", err)
152	}
153	*manualRefresh <- struct{}{}
154}
155
156func Untrack(dbConn *sql.DB, mu *sync.Mutex, id string) {
157	err := db.DeleteProject(dbConn, mu, id)
158	if err != nil {
159		fmt.Println("Error deleting project:", err)
160	}
161
162	err = git.RemoveRepo(id)
163	if err != nil {
164		log.Println(err)
165	}
166}
167
168func RefreshLoop(dbConn *sql.DB, mu *sync.Mutex, interval int, manualRefresh, req *chan struct{}, res *chan []Project) {
169	ticker := time.NewTicker(time.Second * time.Duration(interval))
170
171	fetch := func() []Project {
172		projectsList, err := GetProjects(dbConn)
173		if err != nil {
174			fmt.Println("Error getting projects:", err)
175		}
176		for i, p := range projectsList {
177			p, err := fetchReleases(dbConn, mu, p)
178			if err != nil {
179				fmt.Println(err)
180				continue
181			}
182			projectsList[i] = p
183		}
184		sort.Slice(projectsList, func(i, j int) bool {
185			return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
186		})
187		for i := range projectsList {
188			err = upsertRelease(dbConn, mu, projectsList[i].URL, projectsList[i].Releases)
189			if err != nil {
190				fmt.Println("Error upserting release:", err)
191				continue
192			}
193		}
194		return projectsList
195	}
196
197	projects := fetch()
198
199	for {
200		select {
201		case <-ticker.C:
202			projects = fetch()
203		case <-*manualRefresh:
204			ticker.Reset(time.Second * 3600)
205			projects = fetch()
206		case <-*req:
207			projectsCopy := make([]Project, len(projects))
208			copy(projectsCopy, projects)
209			*res <- projectsCopy
210		}
211	}
212}
213
214// GetProject returns a project from the database
215func GetProject(dbConn *sql.DB, id string) (Project, error) {
216	projectDB, err := db.GetProject(dbConn, id)
217	if err != nil {
218		return Project{}, err
219	}
220	p := Project{
221		ID:      projectDB["id"],
222		URL:     projectDB["url"],
223		Name:    projectDB["name"],
224		Forge:   projectDB["forge"],
225		Running: projectDB["version"],
226	}
227	return p, err
228}
229
230// GetProjectWithReleases returns a single project from the database along with its releases
231func GetProjectWithReleases(dbConn *sql.DB, mu *sync.Mutex, id string) (Project, error) {
232	project, err := GetProject(dbConn, id)
233	if err != nil {
234		return Project{}, err
235	}
236
237	return GetReleases(dbConn, mu, project)
238}
239
240// GetProjects returns a list of all projects from the database
241func GetProjects(dbConn *sql.DB) ([]Project, error) {
242	projectsDB, err := db.GetProjects(dbConn)
243	if err != nil {
244		return nil, err
245	}
246
247	projects := make([]Project, len(projectsDB))
248	for i, p := range projectsDB {
249		projects[i] = Project{
250			ID:      p["id"],
251			URL:     p["url"],
252			Name:    p["name"],
253			Forge:   p["forge"],
254			Running: p["version"],
255		}
256	}
257
258	return projects, nil
259}
260
261// GetProjectsWithReleases returns a list of all projects and all their releases
262// from the database
263func GetProjectsWithReleases(dbConn *sql.DB, mu *sync.Mutex) ([]Project, error) {
264	projects, err := GetProjects(dbConn)
265	if err != nil {
266		return nil, err
267	}
268
269	for i := range projects {
270		projects[i], err = GetReleases(dbConn, mu, projects[i])
271		if err != nil {
272			return nil, err
273		}
274		projects[i].Releases = SortReleases(projects[i].Releases)
275	}
276
277	return projects, nil
278}