willow/project/project.go

306 lines
7.6 KiB
Go
Raw Normal View History

// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: Apache-2.0
package project
import (
"crypto/sha256"
"database/sql"
"errors"
"fmt"
"log"
"sort"
"strings"
"sync"
"time"
"github.com/unascribed/FlexVer/go/flexver"
"git.sr.ht/~amolith/willow/db"
"git.sr.ht/~amolith/willow/git"
"git.sr.ht/~amolith/willow/rss"
)
type Project struct {
ID string
URL string
Name string
Forge string
Running string
Releases []Release
}
type Release struct {
ID string
ProjectID string
URL string
Tag string
Content string
Date time.Time
}
// GetReleases returns a list of all releases for a project from the database
func GetReleases(dbConn *sql.DB, mu *sync.Mutex, proj Project) (Project, error) {
proj.ID = GenProjectID(proj.URL, proj.Name, proj.Forge)
ret, err := db.GetReleases(dbConn, proj.ID)
if err != nil {
return proj, err
}
if len(ret) == 0 {
proj, err = fetchReleases(dbConn, mu, proj)
if err != nil {
return proj, err
}
err = upsertReleases(dbConn, mu, proj.ID, proj.Releases)
if err != nil {
return proj, err
}
return proj, nil
}
for _, row := range ret {
proj.Releases = append(proj.Releases, Release{
ID: row["id"],
ProjectID: proj.ID,
Tag: row["tag"],
Content: row["content"],
URL: row["release_url"],
Date: time.Time{},
})
}
proj.Releases = SortReleases(proj.Releases)
return proj, nil
}
// fetchReleases fetches releases from a project's forge given its URI
func fetchReleases(dbConn *sql.DB, mu *sync.Mutex, p Project) (Project, error) {
var err error
switch p.Forge {
case "github", "gitea", "forgejo":
rssReleases, err := rss.GetReleases(p.URL)
if err != nil {
2023-10-26 00:16:36 +00:00
fmt.Println("Error getting RSS releases:", err)
return p, err
}
for _, release := range rssReleases {
p.Releases = append(p.Releases, Release{
ID: GenReleaseID(p.URL, release.URL, release.Tag),
Tag: release.Tag,
Content: release.Content,
URL: release.URL,
Date: release.Date,
})
err = upsertReleases(dbConn, mu, p.ID, p.Releases)
if err != nil {
log.Printf("Error upserting release: %v", err)
return p, err
}
}
default:
gitReleases, err := git.GetReleases(p.URL, p.Forge)
if err != nil {
return p, err
}
for _, release := range gitReleases {
p.Releases = append(p.Releases, Release{
ID: GenReleaseID(p.URL, release.URL, release.Tag),
Tag: release.Tag,
Content: release.Content,
URL: release.URL,
Date: release.Date,
})
err = upsertReleases(dbConn, mu, p.ID, p.Releases)
if err != nil {
log.Printf("Error upserting release: %v", err)
return p, err
}
}
}
p.Releases = SortReleases(p.Releases)
return p, err
}
func SortReleases(releases []Release) []Release {
sort.Slice(releases, func(i, j int) bool {
return !flexver.Less(releases[i].Tag, releases[j].Tag)
})
return releases
}
2024-02-24 15:08:13 +00:00
func SortProjects(projects []Project) []Project {
sort.Slice(projects, func(i, j int) bool {
return strings.ToLower(projects[i].Name) < strings.ToLower(projects[j].Name)
})
return projects
}
// upsertReleases updates or inserts a release in the database
func upsertReleases(dbConn *sql.DB, mu *sync.Mutex, projID string, releases []Release) error {
for _, release := range releases {
date := release.Date.Format("2006-01-02 15:04:05")
err := db.UpsertRelease(dbConn, mu, release.ID, projID, release.URL, release.Tag, release.Content, date)
if err != nil {
log.Printf("Error upserting release: %v", err)
return err
}
}
return nil
}
// GenReleaseID generates a likely-unique ID from its project's URL, its release's URL, and its tag
func GenReleaseID(projectURL, releaseURL, tag string) string {
idByte := sha256.Sum256([]byte(projectURL + releaseURL + tag))
return fmt.Sprintf("%x", idByte)
}
// GenProjectID generates a likely-unique ID from a project's URI, name, and forge
func GenProjectID(url, name, forge string) string {
idByte := sha256.Sum256([]byte(url + name + forge))
return fmt.Sprintf("%x", idByte)
}
func Track(dbConn *sql.DB, mu *sync.Mutex, manualRefresh *chan struct{}, name, url, forge, release string) {
id := GenProjectID(url, name, forge)
err := db.UpsertProject(dbConn, mu, id, url, name, forge, release)
if err != nil {
fmt.Println("Error upserting project:", err)
}
*manualRefresh <- struct{}{}
}
func Untrack(dbConn *sql.DB, mu *sync.Mutex, id string) {
proj, err := db.GetProject(dbConn, id)
if err != nil {
fmt.Println("Error getting project:", err)
}
err = db.DeleteProject(dbConn, mu, proj["id"])
if err != nil {
fmt.Println("Error deleting project:", err)
}
// TODO: before removing, check whether other tracked projects use the same
// repo
err = git.RemoveRepo(proj["url"])
if err != nil {
log.Println(err)
}
}
func RefreshLoop(dbConn *sql.DB, mu *sync.Mutex, interval int, manualRefresh, req *chan struct{}, res *chan []Project) {
ticker := time.NewTicker(time.Second * time.Duration(interval))
fetch := func() []Project {
projectsList, err := GetProjects(dbConn)
if err != nil {
fmt.Println("Error getting projects:", err)
}
for i, p := range projectsList {
p, err := fetchReleases(dbConn, mu, p)
if err != nil {
fmt.Println(err)
continue
}
projectsList[i] = p
}
sort.Slice(projectsList, func(i, j int) bool {
return strings.ToLower(projectsList[i].Name) < strings.ToLower(projectsList[j].Name)
})
for i := range projectsList {
err = upsertReleases(dbConn, mu, projectsList[i].ID, projectsList[i].Releases)
if err != nil {
fmt.Println("Error upserting release:", err)
continue
}
}
return projectsList
}
projects := fetch()
for {
select {
case <-ticker.C:
projects = fetch()
case <-*manualRefresh:
ticker.Reset(time.Second * 3600)
projects = fetch()
case <-*req:
projectsCopy := make([]Project, len(projects))
copy(projectsCopy, projects)
*res <- projectsCopy
}
}
}
// GetProject returns a project from the database
func GetProject(dbConn *sql.DB, proj Project) (Project, error) {
projectDB, err := db.GetProject(dbConn, proj.ID)
if err != nil && errors.Is(err, sql.ErrNoRows) {
return proj, nil
} else if err != nil {
return proj, err
}
p := Project{
ID: proj.ID,
URL: proj.URL,
Name: proj.Name,
Forge: proj.Forge,
Running: projectDB["version"],
}
return p, err
}
// GetProjectWithReleases returns a single project from the database along with its releases
func GetProjectWithReleases(dbConn *sql.DB, mu *sync.Mutex, proj Project) (Project, error) {
project, err := GetProject(dbConn, proj)
if err != nil {
return Project{}, err
}
return GetReleases(dbConn, mu, project)
}
// GetProjects returns a list of all projects from the database
func GetProjects(dbConn *sql.DB) ([]Project, error) {
projectsDB, err := db.GetProjects(dbConn)
if err != nil {
return nil, err
}
projects := make([]Project, len(projectsDB))
for i, p := range projectsDB {
projects[i] = Project{
ID: p["id"],
URL: p["url"],
Name: p["name"],
Forge: p["forge"],
Running: p["version"],
}
}
2024-02-24 15:08:13 +00:00
return SortProjects(projects), nil
}
// GetProjectsWithReleases returns a list of all projects and all their releases
// from the database
func GetProjectsWithReleases(dbConn *sql.DB, mu *sync.Mutex) ([]Project, error) {
projects, err := GetProjects(dbConn)
if err != nil {
return nil, err
}
for i := range projects {
projects[i], err = GetReleases(dbConn, mu, projects[i])
if err != nil {
return nil, err
}
projects[i].Releases = SortReleases(projects[i].Releases)
}
2024-02-24 15:08:13 +00:00
return SortProjects(projects), nil
}