281 lines
6.9 KiB
Go
281 lines
6.9 KiB
Go
// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package project
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"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) {
|
|
ret, err := db.GetReleases(dbConn, proj.ID)
|
|
if err != nil {
|
|
return proj, err
|
|
}
|
|
|
|
// TODO: figure out a clean way to remove this so the home page loads
|
|
// immediately.
|
|
if len(ret) == 0 {
|
|
return proj, nil
|
|
}
|
|
|
|
for _, row := range ret {
|
|
proj.Releases = append(proj.Releases, Release{
|
|
ID: row["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 {
|
|
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 = upsertRelease(dbConn, mu, p.URL, 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 = upsertRelease(dbConn, mu, p.URL, 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
|
|
}
|
|
|
|
// upsertRelease updates or inserts a release in the database
|
|
func upsertRelease(dbConn *sql.DB, mu *sync.Mutex, url string, releases []Release) error {
|
|
for _, release := range releases {
|
|
date := release.Date.Format("2006-01-02 15:04:05")
|
|
id := GenReleaseID(url, release.URL, release.Tag)
|
|
err := db.UpsertRelease(dbConn, mu, id, url, 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) {
|
|
err := db.DeleteProject(dbConn, mu, id)
|
|
if err != nil {
|
|
fmt.Println("Error deleting project:", err)
|
|
}
|
|
|
|
err = git.RemoveRepo(id)
|
|
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 = upsertRelease(dbConn, mu, projectsList[i].URL, 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, id string) (Project, error) {
|
|
projectDB, err := db.GetProject(dbConn, id)
|
|
if err != nil {
|
|
return Project{}, err
|
|
}
|
|
p := Project{
|
|
ID: projectDB["id"],
|
|
URL: projectDB["url"],
|
|
Name: projectDB["name"],
|
|
Forge: projectDB["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, id string) (Project, error) {
|
|
project, err := GetProject(dbConn, id)
|
|
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"],
|
|
}
|
|
}
|
|
|
|
return 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)
|
|
}
|
|
|
|
return projects, nil
|
|
}
|