Update the help text and flag description for `gh release upload --clobber` to make it clear that existing assets are deleted before new ones are uploaded, and that original assets will be lost if the upload fails. Fixes #8822 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
156 lines
3.9 KiB
Go
156 lines
3.9 KiB
Go
package upload
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
"github.com/cli/cli/v2/internal/text"
|
|
"github.com/cli/cli/v2/pkg/cmd/release/shared"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type UploadOptions struct {
|
|
HttpClient func() (*http.Client, error)
|
|
IO *iostreams.IOStreams
|
|
BaseRepo func() (ghrepo.Interface, error)
|
|
|
|
TagName string
|
|
Assets []*shared.AssetForUpload
|
|
|
|
// maximum number of simultaneous uploads
|
|
Concurrency int
|
|
OverwriteExisting bool
|
|
}
|
|
|
|
func NewCmdUpload(f *cmdutil.Factory, runF func(*UploadOptions) error) *cobra.Command {
|
|
opts := &UploadOptions{
|
|
IO: f.IOStreams,
|
|
HttpClient: f.HttpClient,
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "upload <tag> <files>...",
|
|
Short: "Upload assets to a release",
|
|
Long: heredoc.Docf(`
|
|
Upload asset files to a GitHub Release.
|
|
|
|
To define a display label for an asset, append text starting with %[1]s#%[1]s after the
|
|
file name.
|
|
|
|
When using %[1]s--clobber%[1]s, existing assets are deleted before new assets are uploaded.
|
|
If the upload fails, the original assets will be lost.
|
|
`, "`"),
|
|
Args: cobra.MinimumNArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// support `-R, --repo` override
|
|
opts.BaseRepo = f.BaseRepo
|
|
|
|
opts.TagName = args[0]
|
|
|
|
var err error
|
|
opts.Assets, err = shared.AssetsFromArgs(args[1:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
opts.Concurrency = 5
|
|
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
return uploadRun(opts)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVar(&opts.OverwriteExisting, "clobber", false, "Delete and re-upload existing assets of the same name")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func uploadRun(opts *UploadOptions) error {
|
|
httpClient, err := opts.HttpClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
baseRepo, err := opts.BaseRepo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
release, err := shared.FetchRelease(context.Background(), httpClient, baseRepo, opts.TagName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
uploadURL := release.UploadURL
|
|
if idx := strings.IndexRune(uploadURL, '{'); idx > 0 {
|
|
uploadURL = uploadURL[:idx]
|
|
}
|
|
|
|
var existingNames []string
|
|
for _, a := range opts.Assets {
|
|
sanitizedFileName := sanitizeFileName(a.Name)
|
|
for _, ea := range release.Assets {
|
|
if ea.Name == sanitizedFileName {
|
|
a.ExistingURL = ea.APIURL
|
|
existingNames = append(existingNames, ea.Name)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(existingNames) > 0 && !opts.OverwriteExisting {
|
|
return fmt.Errorf("asset under the same name already exists: %v", existingNames)
|
|
}
|
|
|
|
opts.IO.StartProgressIndicator()
|
|
err = shared.ConcurrentUpload(httpClient, uploadURL, opts.Concurrency, opts.Assets)
|
|
opts.IO.StopProgressIndicator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.IO.IsStdoutTTY() {
|
|
iofmt := opts.IO.ColorScheme()
|
|
fmt.Fprintf(opts.IO.Out, "Successfully uploaded %s to %s\n",
|
|
text.Pluralize(len(opts.Assets), "asset"),
|
|
iofmt.Bold(release.TagName))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// this method attempts to mimic the same functionality on the client that the platform does on
|
|
// uploaded assets in order to allow the --clobber logic work correctly, since that feature is
|
|
// one that only exists in the client
|
|
func sanitizeFileName(name string) string {
|
|
value := text.RemoveDiacritics(name)
|
|
// Stripped all non-ascii characters, provide default name.
|
|
if strings.HasPrefix(value, ".") {
|
|
value = "default" + value
|
|
}
|
|
|
|
// Replace special characters with the separator
|
|
value = regexp.MustCompile(`(?i)[^a-z0-9\-_\+@]+`).ReplaceAllLiteralString(value, ".")
|
|
|
|
// No more than one of the separator in a row.
|
|
value = regexp.MustCompile(`\.{2,}`).ReplaceAllLiteralString(value, ".")
|
|
|
|
// Remove leading/trailing separator.
|
|
value = strings.Trim(value, ".")
|
|
|
|
// Just file extension left, add default name.
|
|
if name != value && !strings.Contains(value, ".") {
|
|
value = "default." + value
|
|
}
|
|
|
|
return value
|
|
}
|