|
|
|
@@ -1,224 +0,0 @@
|
|
|
|
|
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
|
|
|
|
|
package updateserver
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/xml"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
|
|
|
|
repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
|
|
|
|
|
"git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Joomla-compatible updates.xml structures for XML marshaling.
|
|
|
|
|
|
|
|
|
|
type xmlUpdates struct {
|
|
|
|
|
XMLName xml.Name `xml:"updates"`
|
|
|
|
|
Updates []xmlUpdate `xml:"update"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type xmlUpdate struct {
|
|
|
|
|
Name string `xml:"name"`
|
|
|
|
|
Description string `xml:"description"`
|
|
|
|
|
Element string `xml:"element"`
|
|
|
|
|
Type string `xml:"type"`
|
|
|
|
|
Client string `xml:"client"`
|
|
|
|
|
Version string `xml:"version"`
|
|
|
|
|
CreationDate string `xml:"creationDate"`
|
|
|
|
|
InfoURL xmlInfoURL `xml:"infourl"`
|
|
|
|
|
Downloads xmlDownloads `xml:"downloads"`
|
|
|
|
|
SHA256 string `xml:"sha256,omitempty"`
|
|
|
|
|
Tags xmlTags `xml:"tags"`
|
|
|
|
|
ChangelogURL string `xml:"changelogurl,omitempty"`
|
|
|
|
|
Maintainer string `xml:"maintainer,omitempty"`
|
|
|
|
|
MaintainerURL string `xml:"maintainerurl,omitempty"`
|
|
|
|
|
TargetPlatform xmlTargetPlat `xml:"targetplatform"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type xmlInfoURL struct {
|
|
|
|
|
Title string `xml:"title,attr"`
|
|
|
|
|
URL string `xml:",chardata"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type xmlDownloads struct {
|
|
|
|
|
DownloadURL []xmlDownloadURL `xml:"downloadurl"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type xmlDownloadURL struct {
|
|
|
|
|
Type string `xml:"type,attr"`
|
|
|
|
|
Format string `xml:"format,attr"`
|
|
|
|
|
URL string `xml:",chardata"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type xmlTags struct {
|
|
|
|
|
Tag string `xml:"tag"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type xmlTargetPlat struct {
|
|
|
|
|
Name string `xml:"name,attr"`
|
|
|
|
|
Version string `xml:"version,attr"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// channelFromTag maps a release tag name to a Joomla update channel.
|
|
|
|
|
func channelFromTag(tagName string, isPrerelease bool) string {
|
|
|
|
|
lower := strings.ToLower(tagName)
|
|
|
|
|
switch {
|
|
|
|
|
case strings.Contains(lower, "-dev") || strings.Contains(lower, "development"):
|
|
|
|
|
return "dev"
|
|
|
|
|
case strings.Contains(lower, "-alpha") || strings.Contains(lower, "alpha"):
|
|
|
|
|
return "alpha"
|
|
|
|
|
case strings.Contains(lower, "-beta") || strings.Contains(lower, "beta"):
|
|
|
|
|
return "beta"
|
|
|
|
|
case strings.Contains(lower, "-rc") || strings.Contains(lower, "release-candidate"):
|
|
|
|
|
return "rc"
|
|
|
|
|
case isPrerelease:
|
|
|
|
|
return "rc"
|
|
|
|
|
default:
|
|
|
|
|
return "stable"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases.
|
|
|
|
|
// It returns the raw XML bytes. The element, maintainer, and target platform
|
|
|
|
|
// are derived from the repo name and owner.
|
|
|
|
|
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository) ([]byte, error) {
|
|
|
|
|
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
|
|
|
|
RepoID: repo.ID,
|
|
|
|
|
ListOptions: db.ListOptionsAll,
|
|
|
|
|
IncludeDrafts: false,
|
|
|
|
|
IncludeTags: false,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("GetReleasesByRepoID: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := repo.LoadOwner(ctx); err != nil {
|
|
|
|
|
return nil, fmt.Errorf("LoadOwner: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
baseURL := setting.AppURL
|
|
|
|
|
if strings.HasSuffix(baseURL, "/") {
|
|
|
|
|
baseURL = baseURL[:len(baseURL)-1]
|
|
|
|
|
}
|
|
|
|
|
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
|
|
|
|
|
|
|
|
|
|
element := strings.ToLower(repo.Name)
|
|
|
|
|
|
|
|
|
|
// Track best (latest) release per channel to emit one entry per channel.
|
|
|
|
|
bestByChannel := make(map[string]*repo_model.Release)
|
|
|
|
|
for _, rel := range releases {
|
|
|
|
|
if rel.IsDraft || rel.IsTag {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
ch := channelFromTag(rel.TagName, rel.IsPrerelease)
|
|
|
|
|
existing, ok := bestByChannel[ch]
|
|
|
|
|
if !ok || rel.CreatedUnix > existing.CreatedUnix {
|
|
|
|
|
bestByChannel[ch] = rel
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var updates xmlUpdates
|
|
|
|
|
for _, ch := range []string{"stable", "rc", "beta", "alpha", "dev"} {
|
|
|
|
|
rel, ok := bestByChannel[ch]
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load attachments for download URLs.
|
|
|
|
|
if err := rel.LoadAttributes(ctx); err != nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the first .zip attachment as the download URL.
|
|
|
|
|
var downloadURL string
|
|
|
|
|
for _, att := range rel.Attachments {
|
|
|
|
|
if strings.HasSuffix(strings.ToLower(att.Name), ".zip") {
|
|
|
|
|
downloadURL = fmt.Sprintf("%s/releases/download/%s/%s", repoLink, rel.TagName, att.Name)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Fall back to the release tag archive if no zip attachment.
|
|
|
|
|
if downloadURL == "" {
|
|
|
|
|
downloadURL = fmt.Sprintf("%s/archive/%s.zip", repoLink, rel.TagName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
version := extractVersion(rel.TagName)
|
|
|
|
|
suffix := channelSuffix(ch)
|
|
|
|
|
if suffix != "" {
|
|
|
|
|
version = version + suffix
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
u := xmlUpdate{
|
|
|
|
|
Name: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
|
|
|
|
|
Description: fmt.Sprintf("%s - %s %s build.", repo.Owner.Name, repo.Name, ch),
|
|
|
|
|
Element: element,
|
|
|
|
|
Type: "component",
|
|
|
|
|
Client: "site",
|
|
|
|
|
Version: version,
|
|
|
|
|
CreationDate: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
|
|
|
|
|
InfoURL: xmlInfoURL{
|
|
|
|
|
Title: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
|
|
|
|
|
URL: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
|
|
|
|
|
},
|
|
|
|
|
Downloads: xmlDownloads{
|
|
|
|
|
DownloadURL: []xmlDownloadURL{
|
|
|
|
|
{Type: "full", Format: "zip", URL: downloadURL},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Tags: xmlTags{Tag: ch},
|
|
|
|
|
ChangelogURL: fmt.Sprintf("%s/raw/branch/%s/CHANGELOG.md", repoLink, repo.DefaultBranch),
|
|
|
|
|
Maintainer: repo.Owner.Name,
|
|
|
|
|
MaintainerURL: fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name),
|
|
|
|
|
TargetPlatform: xmlTargetPlat{
|
|
|
|
|
Name: "joomla",
|
|
|
|
|
Version: ".*",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updates.Updates = append(updates.Updates, u)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
output, err := xml.MarshalIndent(updates, "", " ")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("xml.MarshalIndent: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return append([]byte(xml.Header), output...), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// extractVersion strips common tag prefixes (v, release-, etc.) to get the version.
|
|
|
|
|
func extractVersion(tagName string) string {
|
|
|
|
|
v := tagName
|
|
|
|
|
v = strings.TrimPrefix(v, "v")
|
|
|
|
|
v = strings.TrimPrefix(v, "release-")
|
|
|
|
|
v = strings.TrimPrefix(v, "release/")
|
|
|
|
|
// Strip channel suffixes to get base version.
|
|
|
|
|
for _, suffix := range []string{"-dev", "-alpha", "-beta", "-rc", "-development", "-release-candidate"} {
|
|
|
|
|
if idx := strings.Index(strings.ToLower(v), suffix); idx > 0 {
|
|
|
|
|
v = v[:idx]
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// channelSuffix returns the version suffix for a channel.
|
|
|
|
|
func channelSuffix(channel string) string {
|
|
|
|
|
switch channel {
|
|
|
|
|
case "dev":
|
|
|
|
|
return "-dev"
|
|
|
|
|
case "alpha":
|
|
|
|
|
return "-alpha"
|
|
|
|
|
case "beta":
|
|
|
|
|
return "-beta"
|
|
|
|
|
case "rc":
|
|
|
|
|
return "-rc"
|
|
|
|
|
default:
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}
|