forked from Gitlink/gitlink-cli
225 lines
6.1 KiB
Go
225 lines
6.1 KiB
Go
package release
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gitlink-org/gitlink-cli/internal/output"
|
|
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
|
)
|
|
|
|
type releaseAsset struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Filesize string `json:"filesize,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
func runReleaseAssets(ctx *common.RuntimeContext) error {
|
|
release, err := fetchRelease(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Output(output.SuccessEnvelope(releaseAssetsPayload(release), nil))
|
|
}
|
|
|
|
func runReleaseDownload(ctx *common.RuntimeContext) error {
|
|
release, err := fetchRelease(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sourceURL, filename, sourceType, err := selectDownloadSource(ctx, release)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
result, err := ctx.Download(sourceURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
outPath, err := resolveOutputPath(ctx.Arg("output"), filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ctx.Arg("force") != "true" {
|
|
if _, err := os.Stat(outPath); err == nil {
|
|
return fmt.Errorf("output file already exists: %s (use --force to overwrite)", outPath)
|
|
} else if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(outPath, result.Data, 0644); err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
|
|
"path": outPath,
|
|
"bytes": len(result.Data),
|
|
"source": sourceURL,
|
|
"source_type": sourceType,
|
|
"content_type": result.ContentType,
|
|
}, nil))
|
|
}
|
|
|
|
func fetchRelease(ctx *common.RuntimeContext) (map[string]interface{}, error) {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return nil, err
|
|
}
|
|
id, _ := ctx.RequireArg("id")
|
|
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/releases/%s", ctx.RepoPath(), id), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, ok := env.Data.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected release response shape")
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func releaseAssetsPayload(release map[string]interface{}) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"version_id": stringValue(release["version_id"]),
|
|
"tag_name": stringValue(release["tag_name"]),
|
|
"name": stringValue(release["name"]),
|
|
"attachments": releaseAttachments(release),
|
|
"archives": map[string]string{
|
|
"zip": stringValue(release["zipball_url"]),
|
|
"tar": stringValue(release["tarball_url"]),
|
|
},
|
|
}
|
|
}
|
|
|
|
func selectDownloadSource(ctx *common.RuntimeContext, release map[string]interface{}) (string, string, string, error) {
|
|
archive := strings.ToLower(strings.TrimSpace(ctx.Arg("archive")))
|
|
assetSelector := strings.TrimSpace(ctx.Arg("asset"))
|
|
if archive != "" && assetSelector != "" {
|
|
return "", "", "", fmt.Errorf("--asset and --archive cannot be used together")
|
|
}
|
|
if archive != "" {
|
|
return selectArchiveSource(ctx, release, archive)
|
|
}
|
|
|
|
assets := releaseAttachments(release)
|
|
asset, err := selectAsset(assets, assetSelector)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
return asset.URL, safeFilename(asset.Title, "attachment-"+asset.ID), "asset", nil
|
|
}
|
|
|
|
func selectArchiveSource(ctx *common.RuntimeContext, release map[string]interface{}, archive string) (string, string, string, error) {
|
|
tag := stringValue(release["tag_name"])
|
|
if tag == "" {
|
|
tag = ctx.Arg("id")
|
|
}
|
|
switch archive {
|
|
case "zip":
|
|
u := stringValue(release["zipball_url"])
|
|
if u == "" {
|
|
return "", "", "", fmt.Errorf("release does not provide zipball_url")
|
|
}
|
|
return u, safeFilename(fmt.Sprintf("%s-%s.zip", ctx.Repo, tag), "release.zip"), "archive", nil
|
|
case "tar", "tar.gz", "tgz":
|
|
u := stringValue(release["tarball_url"])
|
|
if u == "" {
|
|
return "", "", "", fmt.Errorf("release does not provide tarball_url")
|
|
}
|
|
return u, safeFilename(fmt.Sprintf("%s-%s.tar.gz", ctx.Repo, tag), "release.tar.gz"), "archive", nil
|
|
default:
|
|
return "", "", "", fmt.Errorf("--archive must be zip or tar")
|
|
}
|
|
}
|
|
|
|
func selectAsset(assets []releaseAsset, selector string) (releaseAsset, error) {
|
|
if len(assets) == 0 {
|
|
return releaseAsset{}, fmt.Errorf("release has no attachments")
|
|
}
|
|
if selector == "" {
|
|
if len(assets) == 1 {
|
|
return assets[0], nil
|
|
}
|
|
return releaseAsset{}, fmt.Errorf("release has multiple attachments; specify --asset by id or title")
|
|
}
|
|
for _, asset := range assets {
|
|
if asset.ID == selector || asset.Title == selector {
|
|
return asset, nil
|
|
}
|
|
}
|
|
return releaseAsset{}, fmt.Errorf("release attachment %q not found", selector)
|
|
}
|
|
|
|
func releaseAttachments(release map[string]interface{}) []releaseAsset {
|
|
raw, ok := release["attachments"].([]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
assets := make([]releaseAsset, 0, len(raw))
|
|
for _, item := range raw {
|
|
m, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
assets = append(assets, releaseAsset{
|
|
ID: stringValue(m["id"]),
|
|
Title: stringValue(m["title"]),
|
|
Filesize: stringValue(m["filesize"]),
|
|
Description: stringValue(m["description"]),
|
|
URL: stringValue(m["url"]),
|
|
})
|
|
}
|
|
return assets
|
|
}
|
|
|
|
func resolveOutputPath(out, filename string) (string, error) {
|
|
if strings.TrimSpace(out) == "" {
|
|
out = "."
|
|
}
|
|
if info, err := os.Stat(out); err == nil && info.IsDir() {
|
|
return filepath.Join(out, filename), nil
|
|
} else if err != nil && !os.IsNotExist(err) {
|
|
return "", err
|
|
}
|
|
if strings.HasSuffix(out, string(os.PathSeparator)) || strings.HasSuffix(out, "/") {
|
|
return filepath.Join(out, filename), nil
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func safeFilename(name, fallback string) string {
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
name = fallback
|
|
}
|
|
name = filepath.Base(strings.ReplaceAll(name, "\\", "/"))
|
|
if name == "." || name == string(os.PathSeparator) || name == "" {
|
|
return fallback
|
|
}
|
|
return name
|
|
}
|
|
|
|
func stringValue(v interface{}) string {
|
|
switch x := v.(type) {
|
|
case string:
|
|
return x
|
|
case float64:
|
|
if x == float64(int64(x)) {
|
|
return strconv.FormatInt(int64(x), 10)
|
|
}
|
|
return strconv.FormatFloat(x, 'f', -1, 64)
|
|
case int:
|
|
return strconv.Itoa(x)
|
|
case int64:
|
|
return strconv.FormatInt(x, 10)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|