// SPDX-FileCopyrightText: Amolith // // SPDX-License-Identifier: Apache-2.0 package project import ( "crypto/sha256" "database/sql" "fmt" "log" "sort" "strings" "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 { URL string Name string Forge string Running string Releases []Release } type Release struct { 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, proj Project) (Project, error) { ret, err := db.GetReleases(dbConn, proj.URL) if err != nil { return proj, err } if len(ret) == 0 { return fetchReleases(dbConn, proj) } for _, row := range ret { proj.Releases = append(proj.Releases, Release{ 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, 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{ Tag: release.Tag, Content: release.Content, URL: release.URL, Date: release.Date, }) err = upsert(dbConn, 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{ Tag: release.Tag, Content: release.Content, URL: release.URL, Date: release.Date, }) err = upsert(dbConn, 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 } // upsert updates or inserts a project release into the database func upsert(dbConn *sql.DB, url string, releases []Release) error { for _, release := range releases { date := release.Date.Format("2006-01-02 15:04:05") idByte := sha256.Sum256([]byte(url + release.URL + release.Tag + date)) id := fmt.Sprintf("%x", idByte) err := db.UpsertRelease(dbConn, id, url, release.URL, release.Tag, release.Content, date) if err != nil { log.Printf("Error upserting release: %v", err) return err } } return nil } func Track(dbConn *sql.DB, manualRefresh *chan struct{}, name, url, forge, release string) { err := db.UpsertProject(dbConn, url, name, forge, release) if err != nil { fmt.Println("Error upserting project:", err) } *manualRefresh <- struct{}{} } func Untrack(dbConn *sql.DB, manualRefresh *chan struct{}, url string) { err := db.DeleteProject(dbConn, url) if err != nil { fmt.Println("Error deleting project:", err) } *manualRefresh <- struct{}{} err = git.RemoveRepo(url) if err != nil { log.Println(err) } } func RefreshLoop(dbConn *sql.DB, 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, 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 = upsert(dbConn, 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, url string) (Project, error) { projectDB, err := db.GetProject(dbConn, url) if err != nil { return Project{}, err } p := Project{ URL: projectDB["url"], Name: projectDB["name"], Forge: projectDB["forge"], Running: projectDB["version"], } return p, err } // 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{ URL: p["url"], Name: p["name"], Forge: p["forge"], Running: p["version"], } } return projects, nil }