Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package minio-client for openSUSE:Factory checked in at 2023-10-16 23:01:24 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/minio-client (Old) and /work/SRC/openSUSE:Factory/.minio-client.new.20540 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "minio-client" Mon Oct 16 23:01:24 2023 rev:50 rq:1118013 version:20231014T015703Z Changes: -------- --- /work/SRC/openSUSE:Factory/minio-client/minio-client.changes 2023-10-10 20:59:05.223442759 +0200 +++ /work/SRC/openSUSE:Factory/.minio-client.new.20540/minio-client.changes 2023-10-16 23:01:27.730005074 +0200 @@ -1,0 +2,12 @@ +Sat Oct 14 18:52:28 UTC 2023 - ka...@b1-systems.de + +- Update to version 20231014T015703Z: + * prom: Allow insecure TLS connections if --insecure is provided + (#4716) + * Bump golang.org/x/net from 0.15.0 to 0.17.0 (#4713) + * Validation optimization - reduce HEAD calls during cp and mv + operations (#4710) + * Use the new golang version 1.21.3 (#4714) + * Add --stats to traces (#4669) + +------------------------------------------------------------------- Old: ---- mc-20231004T065256Z.obscpio New: ---- mc-20231014T015703Z.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ minio-client.spec ++++++ --- /var/tmp/diff_new_pack.TpbW7H/_old 2023-10-16 23:01:28.666038875 +0200 +++ /var/tmp/diff_new_pack.TpbW7H/_new 2023-10-16 23:01:28.666038875 +0200 @@ -22,7 +22,7 @@ %define binary_name minio-client Name: minio-client -Version: 20231004T065256Z +Version: 20231014T015703Z Release: 0 Summary: Client for MinIO License: AGPL-3.0-only ++++++ _service ++++++ --- /var/tmp/diff_new_pack.TpbW7H/_old 2023-10-16 23:01:28.702040175 +0200 +++ /var/tmp/diff_new_pack.TpbW7H/_new 2023-10-16 23:01:28.706040319 +0200 @@ -5,7 +5,7 @@ <param name="exclude">.git</param> <param name="changesgenerate">enable</param> <param name="versionformat">@PARENT_TAG@</param> - <param name="revision">RELEASE.2023-10-04T06-52-56Z</param> + <param name="revision">RELEASE.2023-10-14T01-57-03Z</param> <param name="match-tag">RELEASE.*</param> <param name="versionrewrite-pattern">RELEASE\.(.*)-(.*)-(.*)-(.*)-(.*)</param> <param name="versionrewrite-replacement">\1\2\3\4\5</param> @@ -19,7 +19,7 @@ <param name="compression">gz</param> </service> <service name="go_modules" mode="manual"> - <param name="archive">mc-20231004T065256Z.obscpio</param> + <param name="archive">mc-20231014T015703Z.obscpio</param> </service> </services> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.TpbW7H/_old 2023-10-16 23:01:28.726041042 +0200 +++ /var/tmp/diff_new_pack.TpbW7H/_new 2023-10-16 23:01:28.730041186 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/minio/mc</param> - <param name="changesrevision">eca8310ac822cf0e533c6bd3fb85c8d6099d1465</param></service></servicedata> + <param name="changesrevision">d158b9a478a6a5a74795f01097d069be82edfff6</param></service></servicedata> (No newline at EOF) ++++++ mc-20231004T065256Z.obscpio -> mc-20231014T015703Z.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/.github/workflows/vulncheck.yml new/mc-20231014T015703Z/.github/workflows/vulncheck.yml --- old/mc-20231004T065256Z/.github/workflows/vulncheck.yml 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/.github/workflows/vulncheck.yml 2023-10-14 03:57:03.000000000 +0200 @@ -16,7 +16,7 @@ - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.21.1 + go-version: 1.21.3 check-latest: true - name: Get official govulncheck run: go install golang.org/x/vuln/cmd/govulncheck@latest diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/admin-scanner-status.go new/mc-20231014T015703Z/cmd/admin-scanner-status.go --- old/mc-20231004T065256Z/cmd/admin-scanner-status.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/admin-scanner-status.go 2023-10-14 03:57:03.000000000 +0200 @@ -208,11 +208,13 @@ return m, tea.Quit } return m, nil + case spinner.TickMsg: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd } - var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) - return m, cmd + return m, nil } func (m *scannerMetricsUI) View() string { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/admin-scanner-trace.go new/mc-20231014T015703Z/cmd/admin-scanner-trace.go --- old/mc-20231004T065256Z/cmd/admin-scanner-trace.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/admin-scanner-trace.go 2023-10-14 03:57:03.000000000 +0200 @@ -169,7 +169,7 @@ if traceInfo.Err != nil { fatalIf(probe.NewError(traceInfo.Err), "Unable to listen to http trace") } - if matchTrace(mopts, traceInfo) { + if mopts.matches(traceInfo) { printTrace(verbose, traceInfo) } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/admin-trace.go new/mc-20231014T015703Z/cmd/admin-trace.go --- old/mc-20231004T065256Z/cmd/admin-trace.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/admin-trace.go 2023-10-14 03:57:03.000000000 +0200 @@ -27,8 +27,12 @@ "path" "sort" "strings" + "sync" "time" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/dustin/go-humanize" "github.com/fatih/color" "github.com/minio/cli" @@ -36,6 +40,7 @@ "github.com/minio/madmin-go/v3" "github.com/minio/mc/pkg/probe" "github.com/minio/pkg/v2/console" + "github.com/olekukonko/tablewriter" ) var adminTraceFlags = []cli.Flag{ @@ -84,6 +89,16 @@ Usage: "trace only failed requests", }, cli.BoolFlag{ + Name: "stats", + Usage: "accumulate stats", + }, + cli.IntFlag{ + Name: "stats-n", + Usage: "maximum number of stat entries", + Value: 15, + Hidden: true, + }, + cli.BoolFlag{ Name: "filter-request", Usage: "trace calls only with request bytes greater than this threshold, use with filter-size", }, @@ -260,7 +275,7 @@ responseSize uint64 } -func matchTrace(opts matchOpts, traceInfo madmin.ServiceTraceInfo) bool { +func (opts matchOpts) matches(traceInfo madmin.ServiceTraceInfo) bool { // Filter request path if passed by the user if len(opts.apiPaths) > 0 { matched := false @@ -450,14 +465,16 @@ } for _, api := range apis { - fn, ok := traceCallTypes[api] - if !ok { - fn, ok = traceCallTypeAliases[api] - } - if !ok { - return madmin.ServiceTraceOpts{}, fmt.Errorf("unknown call name: `%s`", api) + for _, api := range strings.Split(api, ",") { + fn, ok := traceCallTypes[api] + if !ok { + fn, ok = traceCallTypeAliases[api] + } + if !ok { + return madmin.ServiceTraceOpts{}, fmt.Errorf("unknown call name: `%s`", api) + } + fn(&opts) } - fn(&opts) } return } @@ -468,6 +485,7 @@ checkAdminTraceSyntax(ctx) verbose := ctx.Bool("verbose") + stats := ctx.Bool("stats") aliasedURL := ctx.Args().Get(0) console.SetColor("Stat", color.New(color.FgYellow)) @@ -506,11 +524,27 @@ // Start listening on all trace activity. traceCh := client.ServiceTrace(ctxt, opts) + if stats { + filteredTraces := make(chan madmin.ServiceTraceInfo, 1) + go func() { + for t := range traceCh { + if mopts.matches(t) { + filteredTraces <- t + } + } + }() + ui := tea.NewProgram(initTraceStatsUI(ctx.Int("stats-n"), filteredTraces)) + if _, e := ui.Run(); e != nil { + cancel() + fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to fetch scanner metrics") + } + return nil + } for traceInfo := range traceCh { if traceInfo.Err != nil { fatalIf(probe.NewError(traceInfo.Err), "Unable to listen to http trace") } - if matchTrace(mopts, traceInfo) { + if mopts.matches(traceInfo) { printTrace(verbose, traceInfo) } } @@ -842,3 +876,211 @@ fmt.Fprint(b, nodeNameStr) return b.String() } + +type statItem struct { + Name string + Count int `json:"count"` + Duration time.Duration `json:"duration"` + Errors int `json:"errors,omitempty"` + CallStatsCount int `json:"callStatsCount,omitempty"` + CallStats callStats `json:"callStats,omitempty"` +} + +type statTrace struct { + Calls map[string]statItem `json:"calls"` + Started time.Time + mu sync.Mutex +} + +func (s *statTrace) JSON() string { + s.mu.Lock() + defer s.mu.Unlock() + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetIndent("", " ") + // Disable escaping special chars to display XML tags correctly + enc.SetEscapeHTML(false) + fatalIf(probe.NewError(enc.Encode(s)), "Unable to marshal into JSON.") + + // strip off extra newline added by json encoder + return strings.TrimSuffix(buf.String(), "\n") +} + +func (s *statTrace) String() string { + return "" +} + +func (s *statTrace) add(t madmin.ServiceTraceInfo) { + id := t.Trace.FuncName + s.mu.Lock() + defer s.mu.Unlock() + got := s.Calls[id] + if got.Name == "" { + got.Name = id + } + got.Count++ + got.Duration += t.Trace.Duration + if t.Trace.Error != "" { + got.Errors++ + } + if t.Trace.HTTP != nil { + got.CallStatsCount++ + got.CallStats.Rx += t.Trace.HTTP.CallStats.InputBytes + got.CallStats.Tx += t.Trace.HTTP.CallStats.OutputBytes + } + s.Calls[id] = got +} + +func initTraceStatsUI(maxEntries int, traces <-chan madmin.ServiceTraceInfo) *traceStatsUI { + s := spinner.New() + s.Spinner = spinner.Points + s.Spinner.FPS = time.Second / 4 + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + console.SetColor("metrics-duration", color.New(color.FgWhite)) + console.SetColor("metrics-dur", color.New(color.FgGreen)) + console.SetColor("metrics-dur-med", color.New(color.FgYellow)) + console.SetColor("metrics-dur-high", color.New(color.FgRed)) + console.SetColor("metrics-error", color.New(color.FgYellow)) + console.SetColor("metrics-title", color.New(color.FgCyan)) + console.SetColor("metrics-top-title", color.New(color.FgHiCyan)) + console.SetColor("metrics-number", color.New(color.FgWhite)) + console.SetColor("metrics-number-secondary", color.New(color.FgBlue)) + console.SetColor("metrics-zero", color.New(color.FgWhite)) + stats := &statTrace{Calls: make(map[string]statItem, 20), Started: time.Now()} + go func() { + for t := range traces { + stats.add(t) + } + }() + return &traceStatsUI{ + started: time.Now(), + spinner: s, + maxEntries: maxEntries, + current: stats, + } +} + +type traceStatsUI struct { + current *statTrace + started time.Time + spinner spinner.Model + quitting bool + maxEntries int +} + +func (m *traceStatsUI) Init() tea.Cmd { + return m.spinner.Tick +} + +func (m *traceStatsUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.quitting { + return m, tea.Quit + } + switch msg := msg.(type) { + + case tea.KeyMsg: + switch msg.String() { + case "q", "esc", "ctrl+c": + m.quitting = true + return m, tea.Quit + default: + return m, nil + } + case spinner.TickMsg: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + } + return m, nil +} + +func (m *traceStatsUI) View() string { + var s strings.Builder + + if !m.quitting { + s.WriteString(fmt.Sprintf("%s %s\n", console.Colorize("metrics-top-title", "Duration: "+time.Since(m.current.Started).Round(time.Second).String()), m.spinner.View())) + } + + // Set table header + table := tablewriter.NewWriter(&s) + table.SetAutoWrapText(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetBorder(true) + table.SetRowLine(false) + addRow := func(s string) { + table.Append([]string{s}) + } + _ = addRow + addRowF := func(format string, vals ...interface{}) { + s := fmt.Sprintf(format, vals...) + table.Append([]string{s}) + } + _ = addRowF + var entries []statItem + m.current.mu.Lock() + totalCnt := 0 + dur := time.Since(m.current.Started) + for _, v := range m.current.Calls { + totalCnt += v.Count + entries = append(entries, v) + } + m.current.mu.Unlock() + if len(entries) == 0 { + s.WriteString("(waiting for data)") + return s.String() + } + sort.Slice(entries, func(i, j int) bool { + if entries[i].Count == entries[j].Count { + return entries[i].Name < entries[j].Name + } + return entries[i].Count > entries[j].Count + }) + if m.maxEntries > 0 && len(entries) > m.maxEntries { + entries = entries[:m.maxEntries] + } + + table.Append([]string{ + console.Colorize("metrics-top-title", "Call"), + console.Colorize("metrics-top-title", "Count"), + console.Colorize("metrics-top-title", "RPM"), + console.Colorize("metrics-top-title", "Avg Time"), + console.Colorize("metrics-top-title", "Errors"), + console.Colorize("metrics-top-title", "RX Avg"), + console.Colorize("metrics-top-title", "TX Avg"), + }) + for _, v := range entries { + if v.Count <= 0 { + continue + } + errs := "0" + if v.Errors > 0 { + errs = console.Colorize("metrics-error", fmt.Sprintf("%v", v.Errors)) + } + avg := v.Duration / time.Duration(v.Count) + avgColor := "metrics-dur" + if avg > 10*time.Second { + avgColor = "metrics-dur-high" + } else if avg > time.Second { + avgColor = "metrics-dur-med" + } + rx := "-" + tx := "-" + if v.CallStatsCount > 0 { + rx = humanize.IBytes(uint64(v.CallStats.Rx / v.CallStatsCount)) + tx = humanize.IBytes(uint64(v.CallStats.Tx / v.CallStatsCount)) + } + table.Append([]string{ + console.Colorize("metrics-title", metricsTitle(v.Name)), + console.Colorize("metrics-number", fmt.Sprintf("%d ", v.Count)) + + console.Colorize("metrics-number-secondary", fmt.Sprintf("(%0.1f%%)", float64(v.Count)/float64(totalCnt)*100)), + console.Colorize("metrics-number", fmt.Sprintf("%0.1f", float64(v.Count)/dur.Minutes())), + console.Colorize(avgColor, fmt.Sprintf("%v", avg.Round(time.Microsecond))), + errs, + rx, + tx, + }) + } + table.Render() + return s.String() +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/common-methods.go new/mc-20231014T015703Z/cmd/common-methods.go --- old/mc-20231004T065256Z/cmd/common-methods.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/common-methods.go 2023-10-14 03:57:03.000000000 +0200 @@ -108,12 +108,12 @@ // Check if the passed URL represents a folder. It may or may not exist yet. // If it exists, we can easily check if it is a folder, if it doesn't exist, // we can guess if the url is a folder from how it looks. -func isAliasURLDir(ctx context.Context, aliasURL string, keys map[string][]prefixSSEPair, timeRef time.Time) bool { +func isAliasURLDir(ctx context.Context, aliasURL string, keys map[string][]prefixSSEPair, timeRef time.Time) (bool, *ClientContent) { // If the target url exists, check if it is a directory // and return immediately. _, targetContent, err := url2Stat(ctx, aliasURL, "", false, keys, timeRef, false) if err == nil { - return targetContent.Type.IsDir() + return targetContent.Type.IsDir(), targetContent } _, expandedURL, _ := mustExpandAlias(aliasURL) @@ -121,7 +121,7 @@ // Check if targetURL is an FS or S3 aliased url if expandedURL == aliasURL { // This is an FS url, check if the url has a separator at the end - return strings.HasSuffix(aliasURL, string(filepath.Separator)) + return strings.HasSuffix(aliasURL, string(filepath.Separator)), targetContent } // This is an S3 url, then: @@ -134,14 +134,14 @@ switch len(fields) { // Nothing or alias format case 0, 1: - return false + return false, targetContent // alias/bucket format case 2: - return true + return true, targetContent } // default case.. // alias/bucket/prefix format - return strings.HasSuffix(pathURL, "/") + return strings.HasSuffix(pathURL, "/"), targetContent } // getSourceStreamMetadataFromURL gets a reader from URL. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/cp-main.go new/mc-20231014T015703Z/cmd/cp-main.go --- old/mc-20231004T065256Z/cmd/cp-main.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/cp-main.go 2023-10-14 03:57:03.000000000 +0200 @@ -288,7 +288,7 @@ } // doPrepareCopyURLs scans the source URL and prepares a list of objects for copying. -func doPrepareCopyURLs(ctx context.Context, session *sessionV8, cancelCopy context.CancelFunc) (totalBytes, totalObjects int64) { +func doPrepareCopyURLs(ctx context.Context, session *sessionV8, cancelCopy context.CancelFunc) (totalBytes, totalObjects int64, errSeen bool) { // Separate source and target. 'cp' can take only one target, // but any number of sources. sourceURLs := session.Header.CommandArgs[:len(session.Header.CommandArgs)-1] @@ -333,16 +333,10 @@ done = true break } + if cpURLs.Error != nil { - // Print in new line and adjust to top so that we don't print over the ongoing scan bar - if !globalQuiet && !globalJSON { - console.Eraseline() - } - if strings.Contains(cpURLs.Error.ToGoError().Error(), " is a folder.") { - errorIf(cpURLs.Error.Trace(), "Folder cannot be copied. Please use `...` suffix.") - } else { - errorIf(cpURLs.Error.Trace(), "Unable to prepare URL for copying.") - } + printCopyURLsError(&cpURLs) + errSeen = true break } @@ -377,11 +371,29 @@ return } +func printCopyURLsError(cpURLs *URLs) { + // Print in new line and adjust to top so that we + // don't print over the ongoing scan bar + if !globalQuiet && !globalJSON { + console.Eraseline() + } + + if strings.Contains(cpURLs.Error.ToGoError().Error(), + " is a folder.") { + errorIf(cpURLs.Error.Trace(), + "Folder cannot be copied. Please use `...` suffix.") + } else { + errorIf(cpURLs.Error.Trace(), + "Unable to prepare URL for copying.") + } +} + func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.Context, session *sessionV8, encKeyDB map[string][]prefixSSEPair, isMvCmd bool) error { var isCopied func(string) bool var totalObjects, totalBytes int64 cpURLsCh := make(chan URLs, 10000) + errSeen := false // Store a progress bar or an accounter var pg ProgressReader @@ -405,7 +417,7 @@ isCopied = isLastFactory(session.Header.LastCopied) if !session.HasData() { - totalBytes, totalObjects = doPrepareCopyURLs(ctx, session, cancelCopy) + totalBytes, totalObjects, errSeen = doPrepareCopyURLs(ctx, session, cancelCopy) } else { totalBytes, totalObjects = session.Header.TotalBytes, session.Header.TotalObjects } @@ -431,6 +443,7 @@ cpURLsCh <- cpURLs } }() + } else { // Access recursive flag inside the session header. isRecursive := cli.Bool("recursive") @@ -452,23 +465,14 @@ versionID: versionID, isZip: cli.Bool("zip"), } + for cpURLs := range prepareCopyURLs(ctx, opts) { if cpURLs.Error != nil { - // Print in new line and adjust to top so that we - // don't print over the ongoing scan bar - if !globalQuiet && !globalJSON { - console.Eraseline() - } - if strings.Contains(cpURLs.Error.ToGoError().Error(), - " is a folder.") { - errorIf(cpURLs.Error.Trace(), - "Folder cannot be copied. Please use `...` suffix.") - } else { - errorIf(cpURLs.Error.Trace(), - "Unable to start copying.") - } + errSeen = true + printCopyURLsError(&cpURLs) break } + totalBytes += cpURLs.SourceContent.Size pg.SetTotal(totalBytes) totalObjects++ @@ -570,7 +574,6 @@ }() var retErr error - errSeen := false cpAllFilesErr := true loop: @@ -639,14 +642,24 @@ } if progressReader, ok := pg.(*progressBar); ok { - if (errSeen && totalObjects == 1) || (cpAllFilesErr && totalObjects > 1) { - console.Eraseline() + if errSeen || (cpAllFilesErr && totalObjects > 0) { + // We only erase a line if we are displaying a progress bar + if !globalQuiet && !globalJSON { + console.Eraseline() + } } else if progressReader.ProgressBar.Get() > 0 { progressReader.ProgressBar.Finish() } } else { if accntReader, ok := pg.(*accounter); ok { - printMsg(accntReader.Stat()) + if errSeen || (cpAllFilesErr && totalObjects > 0) { + // We only erase a line if we are displaying a progress bar + if !globalQuiet && !globalJSON { + console.Eraseline() + } + } else { + printMsg(accntReader.Stat()) + } } } @@ -670,7 +683,7 @@ } // check 'copy' cli arguments. - checkCopySyntax(ctx, cliCtx, encKeyDB, false) + checkCopySyntax(cliCtx) // Additional command specific theme customization. console.SetColor("Copy", color.New(color.FgGreen, color.Bold)) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/cp-url-syntax.go new/mc-20231014T015703Z/cmd/cp-url-syntax.go --- old/mc-20231004T065256Z/cmd/cp-url-syntax.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/cp-url-syntax.go 2023-10-14 03:57:03.000000000 +0200 @@ -18,21 +18,14 @@ package cmd import ( - "context" "fmt" "runtime" - "time" "github.com/minio/cli" - "github.com/minio/mc/pkg/probe" - "github.com/minio/pkg/v2/console" ) -func checkCopySyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair, isMvCmd bool) { +func checkCopySyntax(cliCtx *cli.Context) { if len(cliCtx.Args()) < 2 { - if isMvCmd { - showCommandHelpAndExit(cliCtx, 1) // last argument is exit code. - } showCommandHelpAndExit(cliCtx, 1) // last argument is exit code. } @@ -44,9 +37,7 @@ srcURLs := URLs[:len(URLs)-1] tgtURL := URLs[len(URLs)-1] - isRecursive := cliCtx.Bool("recursive") isZip := cliCtx.Bool("zip") - timeRef := parseRewindFlag(cliCtx.String("rewind")) versionID := cliCtx.String("version-id") if versionID != "" && len(srcURLs) > 1 { @@ -57,24 +48,6 @@ fatalIf(errDummy().Trace(cliCtx.Args()...), "--zip and --rewind cannot be used together") } - // Verify if source(s) exists. - for _, srcURL := range srcURLs { - var err *probe.Error - if !isRecursive { - _, _, err = url2Stat(ctx, srcURL, versionID, false, encKeyDB, timeRef, isZip) - } else { - _, _, err = firstURL2Stat(ctx, srcURL, timeRef, isZip) - } - if err != nil { - msg := "Unable to validate source `" + srcURL + "`" - if versionID != "" { - msg += " (" + versionID + ")" - } - msg += ": " + err.ToGoError().Error() - console.Fatalln(msg) - } - } - // Check if bucket name is passed for URL type arguments. url := newClientURL(tgtURL) if url.Host != "" { @@ -91,129 +64,8 @@ fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("Both object retention flags `--%s` and `--%s` are required.\n", rdFlag, rmFlag)) } - operation := "copy" - if isMvCmd { - operation = "move" - } - - // Guess CopyURLsType based on source and target URLs. - opts := prepareCopyURLsOpts{ - sourceURLs: srcURLs, - targetURL: tgtURL, - isRecursive: isRecursive, - encKeyDB: encKeyDB, - olderThan: "", - newerThan: "", - timeRef: timeRef, - versionID: versionID, - isZip: isZip, - } - copyURLsType, _, err := guessCopyURLType(ctx, opts) - if err != nil { - fatalIf(errInvalidArgument().Trace(), "Unable to guess the type of "+operation+" operation.") - } - - switch copyURLsType { - case copyURLsTypeA: // File -> File. - // Check source. - if len(srcURLs) != 1 { - fatalIf(errInvalidArgument().Trace(), "Invalid number of source arguments.") - } - checkCopySyntaxTypeA(ctx, srcURLs[0], versionID, encKeyDB, isZip, timeRef) - case copyURLsTypeB: // File -> Folder. - // Check source. - if len(srcURLs) != 1 { - fatalIf(errInvalidArgument().Trace(), "Invalid number of source arguments.") - } - checkCopySyntaxTypeB(ctx, srcURLs[0], versionID, tgtURL, encKeyDB, isZip, timeRef) - case copyURLsTypeC: // Folder... -> Folder. - checkCopySyntaxTypeC(ctx, srcURLs, tgtURL, isRecursive, isZip, encKeyDB, isMvCmd, timeRef) - case copyURLsTypeD: // File1...FileN -> Folder. - checkCopySyntaxTypeD(ctx, tgtURL, encKeyDB, timeRef) - default: - fatalIf(errInvalidArgument().Trace(), "Unable to guess the type of "+operation+" operation.") - } - // Preserve functionality not supported for windows if cliCtx.Bool("preserve") && runtime.GOOS == "windows" { fatalIf(errInvalidArgument().Trace(), "Permissions are not preserved on windows platform.") } } - -// checkCopySyntaxTypeA verifies if the source and target are valid file arguments. -func checkCopySyntaxTypeA(ctx context.Context, srcURL, versionID string, keys map[string][]prefixSSEPair, isZip bool, timeRef time.Time) { - _, srcContent, err := url2Stat(ctx, srcURL, versionID, false, keys, timeRef, isZip) - fatalIf(err.Trace(srcURL), "Unable to stat source `"+srcURL+"`.") - - if !srcContent.Type.IsRegular() { - fatalIf(errInvalidArgument().Trace(), "Source `"+srcURL+"` is not a file.") - } -} - -// checkCopySyntaxTypeB verifies if the source is a valid file and target is a valid folder. -func checkCopySyntaxTypeB(ctx context.Context, srcURL, versionID, tgtURL string, keys map[string][]prefixSSEPair, isZip bool, timeRef time.Time) { - _, srcContent, err := url2Stat(ctx, srcURL, versionID, false, keys, timeRef, isZip) - fatalIf(err.Trace(srcURL), "Unable to stat source `"+srcURL+"`.") - - if !srcContent.Type.IsRegular() { - fatalIf(errInvalidArgument().Trace(srcURL), "Source `"+srcURL+"` is not a file.") - } - - // Check target. - if _, tgtContent, err := url2Stat(ctx, tgtURL, "", false, keys, timeRef, false); err == nil { - if !tgtContent.Type.IsDir() { - fatalIf(errInvalidArgument().Trace(tgtURL), "Target `"+tgtURL+"` is not a folder.") - } - } -} - -// checkCopySyntaxTypeC verifies if the source is a valid recursive dir and target is a valid folder. -func checkCopySyntaxTypeC(ctx context.Context, srcURLs []string, tgtURL string, isRecursive, isZip bool, keys map[string][]prefixSSEPair, isMvCmd bool, timeRef time.Time) { - // Check source. - if len(srcURLs) != 1 { - fatalIf(errInvalidArgument().Trace(), "Invalid number of source arguments.") - } - - // Check target. - if _, tgtContent, err := url2Stat(ctx, tgtURL, "", false, keys, timeRef, false); err == nil { - if !tgtContent.Type.IsDir() { - fatalIf(errInvalidArgument().Trace(tgtURL), "Target `"+tgtURL+"` is not a folder.") - } - } - - for _, srcURL := range srcURLs { - c, srcContent, err := url2Stat(ctx, srcURL, "", false, keys, timeRef, isZip) - fatalIf(err.Trace(srcURL), "Unable to stat source `"+srcURL+"`.") - - if srcContent.Type.IsDir() { - // Require --recursive flag if we are copying a directory - if !isRecursive { - operation := "copy" - if isMvCmd { - operation = "move" - } - fatalIf(errInvalidArgument().Trace(srcURL), fmt.Sprintf("To %v a folder requires --recursive flag.", operation)) - } - - // Check if we are going to copy a directory into itself - if isURLContains(srcURL, tgtURL, string(c.GetURL().Separator)) { - operation := "Copying" - if isMvCmd { - operation = "Moving" - } - fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("%v a folder into itself is not allowed.", operation)) - } - } - } -} - -// checkCopySyntaxTypeD verifies if the source is a valid list of files and target is a valid folder. -func checkCopySyntaxTypeD(ctx context.Context, tgtURL string, keys map[string][]prefixSSEPair, timeRef time.Time) { - // Source can be anything: file, dir, dir... - // Check target if it is a dir - if _, tgtContent, err := url2Stat(ctx, tgtURL, "", false, keys, timeRef, false); err == nil { - if !tgtContent.Type.IsDir() { - fatalIf(errInvalidArgument().Trace(tgtURL), "Target `"+tgtURL+"` is not a folder.") - } - } -} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/cp-url.go new/mc-20231014T015703Z/cmd/cp-url.go --- old/mc-20231004T065256Z/cmd/cp-url.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/cp-url.go 2023-10-14 03:57:03.000000000 +0200 @@ -53,127 +53,182 @@ // guessCopyURLType guesses the type of clientURL. This approach all allows prepareURL // functions to accurately report failure causes. -func guessCopyURLType(ctx context.Context, o prepareCopyURLsOpts) (copyURLsType, string, *probe.Error) { +func guessCopyURLType(ctx context.Context, o prepareCopyURLsOpts) (*copyURLsContent, *probe.Error) { + cc := new(copyURLsContent) + + // Extract alias before fiddling with the clientURL. + cc.sourceURL = o.sourceURLs[0] + cc.sourceAlias, _, _ = mustExpandAlias(cc.sourceURL) + // Find alias and expanded clientURL. + cc.targetAlias, cc.targetURL, _ = mustExpandAlias(o.targetURL) + if len(o.sourceURLs) == 1 { // 1 Source, 1 Target var err *probe.Error - var sourceContent *ClientContent - sourceURL := o.sourceURLs[0] if !o.isRecursive { - _, sourceContent, err = url2Stat(ctx, sourceURL, o.versionID, false, o.encKeyDB, o.timeRef, o.isZip) + _, cc.sourceContent, err = url2Stat(ctx, cc.sourceURL, o.versionID, false, o.encKeyDB, o.timeRef, o.isZip) } else { - _, sourceContent, err = firstURL2Stat(ctx, sourceURL, o.timeRef, o.isZip) + _, cc.sourceContent, err = firstURL2Stat(ctx, cc.sourceURL, o.timeRef, o.isZip) } + if err != nil { - return copyURLsTypeInvalid, "", err + cc.copyType = copyURLsTypeInvalid + return cc, err } // If recursion is ON, it is type C. // If source is a folder, it is Type C. - if sourceContent.Type.IsDir() || o.isRecursive { - return copyURLsTypeC, "", nil + if cc.sourceContent.Type.IsDir() || o.isRecursive { + cc.copyType = copyURLsTypeC + return cc, nil } // If target is a folder, it is Type B. - if isAliasURLDir(ctx, o.targetURL, o.encKeyDB, o.timeRef) { - return copyURLsTypeB, sourceContent.VersionID, nil + var isDir bool + isDir, cc.targetContent = isAliasURLDir(ctx, o.targetURL, o.encKeyDB, o.timeRef) + if isDir { + cc.copyType = copyURLsTypeB + cc.sourceVersionID = cc.sourceContent.VersionID + return cc, nil } + // else Type A. - return copyURLsTypeA, sourceContent.VersionID, nil + cc.copyType = copyURLsTypeA + cc.sourceVersionID = cc.sourceContent.VersionID + return cc, nil } + var isDir bool // Multiple source args and target is a folder. It is Type D. - if isAliasURLDir(ctx, o.targetURL, o.encKeyDB, o.timeRef) { - return copyURLsTypeD, "", nil + isDir, cc.targetContent = isAliasURLDir(ctx, o.targetURL, o.encKeyDB, o.timeRef) + if isDir { + cc.copyType = copyURLsTypeD + return cc, nil } - return copyURLsTypeInvalid, "", errInvalidArgument().Trace() + cc.copyType = copyURLsTypeInvalid + return cc, errInvalidArgument().Trace() } // SINGLE SOURCE - Type A: copy(f, f) -> copy(f, f) // prepareCopyURLsTypeA - prepares target and source clientURLs for copying. -func prepareCopyURLsTypeA(ctx context.Context, sourceURL, sourceVersion, targetURL string, encKeyDB map[string][]prefixSSEPair, isZip bool) URLs { - // Extract alias before fiddling with the clientURL. - sourceAlias, _, _ := mustExpandAlias(sourceURL) - // Find alias and expanded clientURL. - targetAlias, targetURL, _ := mustExpandAlias(targetURL) - - _, sourceContent, err := url2Stat(ctx, sourceURL, sourceVersion, false, encKeyDB, time.Time{}, isZip) - if err != nil { - // Source does not exist or insufficient privileges. - return URLs{Error: err.Trace(sourceURL)} +func prepareCopyURLsTypeA(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) URLs { + var err *probe.Error + if cc.sourceContent == nil { + _, cc.sourceContent, err = url2Stat(ctx, cc.sourceURL, cc.sourceVersionID, false, o.encKeyDB, time.Time{}, o.isZip) + if err != nil { + // Source does not exist or insufficient privileges. + return URLs{Error: err.Trace(cc.sourceURL)} + } } - if !sourceContent.Type.IsRegular() { + + if !cc.sourceContent.Type.IsRegular() { // Source is not a regular file - return URLs{Error: errInvalidSource(sourceURL).Trace(sourceURL)} + return URLs{Error: errInvalidSource(cc.sourceURL).Trace(cc.sourceURL)} } - // All OK.. We can proceed. Type A - return makeCopyContentTypeA(sourceAlias, sourceContent, targetAlias, targetURL) + return makeCopyContentTypeA(cc) } // prepareCopyContentTypeA - makes CopyURLs content for copying. -func makeCopyContentTypeA(sourceAlias string, sourceContent *ClientContent, targetAlias, targetURL string) URLs { - targetContent := ClientContent{URL: *newClientURL(targetURL)} +func makeCopyContentTypeA(cc copyURLsContent) URLs { + targetContent := ClientContent{URL: *newClientURL(cc.targetURL)} return URLs{ - SourceAlias: sourceAlias, - SourceContent: sourceContent, - TargetAlias: targetAlias, + SourceAlias: cc.sourceAlias, + SourceContent: cc.sourceContent, + TargetAlias: cc.targetAlias, TargetContent: &targetContent, } } // SINGLE SOURCE - Type B: copy(f, d) -> copy(f, d/f) -> A // prepareCopyURLsTypeB - prepares target and source clientURLs for copying. -func prepareCopyURLsTypeB(ctx context.Context, sourceURL, sourceVersion, targetURL string, encKeyDB map[string][]prefixSSEPair, isZip bool) URLs { - // Extract alias before fiddling with the clientURL. - sourceAlias, _, _ := mustExpandAlias(sourceURL) - // Find alias and expanded clientURL. - targetAlias, targetURL, _ := mustExpandAlias(targetURL) - - _, sourceContent, err := url2Stat(ctx, sourceURL, sourceVersion, false, encKeyDB, time.Time{}, isZip) - if err != nil { - // Source does not exist or insufficient privileges. - return URLs{Error: err.Trace(sourceURL)} +func prepareCopyURLsTypeB(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) URLs { + var err *probe.Error + if cc.sourceContent == nil { + _, cc.sourceContent, err = url2Stat(ctx, cc.sourceURL, cc.sourceVersionID, false, o.encKeyDB, time.Time{}, o.isZip) + if err != nil { + // Source does not exist or insufficient privileges. + return URLs{Error: err.Trace(cc.sourceURL)} + } } - if !sourceContent.Type.IsRegular() { - if sourceContent.Type.IsDir() { - return URLs{Error: errSourceIsDir(sourceURL).Trace(sourceURL)} + if !cc.sourceContent.Type.IsRegular() { + if cc.sourceContent.Type.IsDir() { + return URLs{Error: errSourceIsDir(cc.sourceURL).Trace(cc.sourceURL)} } // Source is not a regular file. - return URLs{Error: errInvalidSource(sourceURL).Trace(sourceURL)} + return URLs{Error: errInvalidSource(cc.sourceURL).Trace(cc.sourceURL)} } + if cc.targetContent == nil { + _, cc.targetContent, err = url2Stat(ctx, cc.targetURL, "", false, o.encKeyDB, time.Time{}, false) + if err == nil { + if !cc.targetContent.Type.IsDir() { + return URLs{Error: errInvalidTarget(cc.targetURL).Trace(cc.targetURL)} + } + } + } // All OK.. We can proceed. Type B: source is a file, target is a folder and exists. - return makeCopyContentTypeB(sourceAlias, sourceContent, targetAlias, targetURL) + return makeCopyContentTypeB(cc) } // makeCopyContentTypeB - CopyURLs content for copying. -func makeCopyContentTypeB(sourceAlias string, sourceContent *ClientContent, targetAlias, targetURL string) URLs { +func makeCopyContentTypeB(cc copyURLsContent) URLs { // All OK.. We can proceed. Type B: source is a file, target is a folder and exists. - targetURLParse := newClientURL(targetURL) - targetURLParse.Path = filepath.ToSlash(filepath.Join(targetURLParse.Path, filepath.Base(sourceContent.URL.Path))) - return makeCopyContentTypeA(sourceAlias, sourceContent, targetAlias, targetURLParse.String()) + targetURLParse := newClientURL(cc.targetURL) + targetURLParse.Path = filepath.ToSlash(filepath.Join(targetURLParse.Path, filepath.Base(cc.sourceContent.URL.Path))) + cc.targetURL = targetURLParse.String() + return makeCopyContentTypeA(cc) } // SINGLE SOURCE - Type C: copy(d1..., d2) -> []copy(d1/f, d1/d2/f) -> []A // prepareCopyRecursiveURLTypeC - prepares target and source clientURLs for copying. -func prepareCopyURLsTypeC(ctx context.Context, sourceURL, targetURL string, isRecursive, isZip bool, timeRef time.Time) <-chan URLs { - // Extract alias before fiddling with the clientURL. - sourceAlias, _, _ := mustExpandAlias(sourceURL) - // Find alias and expanded clientURL. - targetAlias, targetURL, _ := mustExpandAlias(targetURL) - copyURLsCh := make(chan URLs) - go func(sourceURL, targetURL string, copyURLsCh chan URLs) { - defer close(copyURLsCh) - sourceClient, err := newClient(sourceURL) +func prepareCopyURLsTypeC(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) <-chan URLs { + copyURLsCh := make(chan URLs, 1) + + returnErrorAndCloseChannel := func(err *probe.Error) chan URLs { + copyURLsCh <- URLs{Error: err} + close(copyURLsCh) + return copyURLsCh + } + + c, err := newClient(cc.sourceURL) + if err != nil { + return returnErrorAndCloseChannel(err.Trace(cc.sourceURL)) + } + + if cc.targetContent == nil { + _, cc.targetContent, err = url2Stat(ctx, cc.targetURL, "", false, o.encKeyDB, time.Time{}, o.isZip) + if err == nil { + if !cc.targetContent.Type.IsDir() { + return returnErrorAndCloseChannel(errTargetIsNotDir(cc.targetURL).Trace(cc.targetURL)) + } + } + } + + if cc.sourceContent == nil { + _, cc.sourceContent, err = url2Stat(ctx, cc.sourceURL, "", false, o.encKeyDB, time.Time{}, o.isZip) if err != nil { - // Source initialization failed. - copyURLsCh <- URLs{Error: err.Trace(sourceURL)} - return + return returnErrorAndCloseChannel(err.Trace(cc.sourceURL)) } + } - for sourceContent := range sourceClient.List(ctx, ListOptions{Recursive: isRecursive, TimeRef: timeRef, ShowDir: DirNone, ListZip: isZip}) { + if cc.sourceContent.Type.IsDir() { + // Require --recursive flag if we are copying a directory + if !o.isRecursive { + return returnErrorAndCloseChannel(errRequiresRecursive(cc.sourceURL).Trace(cc.sourceURL)) + } + + // Check if we are going to copy a directory into itself + if isURLContains(cc.sourceURL, cc.targetURL, string(c.GetURL().Separator)) { + return returnErrorAndCloseChannel(errCopyIntoSelf(cc.sourceURL).Trace(cc.targetURL)) + } + } + + go func(sourceClient Client, cc copyURLsContent, o prepareCopyURLsOpts, copyURLsCh chan URLs) { + defer close(copyURLsCh) + + for sourceContent := range sourceClient.List(ctx, ListOptions{Recursive: o.isRecursive, TimeRef: o.timeRef, ShowDir: DirNone, ListZip: o.isZip}) { if sourceContent.Err != nil { // Listing failed. copyURLsCh <- URLs{Error: sourceContent.Err.Trace(sourceClient.GetURL().String())} @@ -185,38 +240,50 @@ continue } + // Clone cc + newCC := cc + newCC.sourceContent = sourceContent // All OK.. We can proceed. Type B: source is a file, target is a folder and exists. - copyURLsCh <- makeCopyContentTypeC(sourceAlias, sourceClient.GetURL(), sourceContent, targetAlias, targetURL) + copyURLsCh <- makeCopyContentTypeC(newCC, sourceClient.GetURL()) } - }(sourceURL, targetURL, copyURLsCh) + }(c, cc, o, copyURLsCh) + return copyURLsCh } // makeCopyContentTypeC - CopyURLs content for copying. -func makeCopyContentTypeC(sourceAlias string, sourceURL ClientURL, sourceContent *ClientContent, targetAlias, targetURL string) URLs { - newSourceURL := sourceContent.URL - pathSeparatorIndex := strings.LastIndex(sourceURL.Path, string(sourceURL.Separator)) +func makeCopyContentTypeC(cc copyURLsContent, sourceClientURL ClientURL) URLs { + newSourceURL := cc.sourceContent.URL + pathSeparatorIndex := strings.LastIndex(sourceClientURL.Path, string(sourceClientURL.Separator)) newSourceSuffix := filepath.ToSlash(newSourceURL.Path) if pathSeparatorIndex > 1 { - sourcePrefix := filepath.ToSlash(sourceURL.Path[:pathSeparatorIndex]) + sourcePrefix := filepath.ToSlash(sourceClientURL.Path[:pathSeparatorIndex]) newSourceSuffix = strings.TrimPrefix(newSourceSuffix, sourcePrefix) } - newTargetURL := urlJoinPath(targetURL, newSourceSuffix) - return makeCopyContentTypeA(sourceAlias, sourceContent, targetAlias, newTargetURL) + newTargetURL := urlJoinPath(cc.targetURL, newSourceSuffix) + cc.targetURL = newTargetURL + return makeCopyContentTypeA(cc) } // MULTI-SOURCE - Type D: copy([](f|d...), d) -> []B // prepareCopyURLsTypeE - prepares target and source clientURLs for copying. -func prepareCopyURLsTypeD(ctx context.Context, sourceURLs []string, targetURL string, isRecursive bool, timeRef time.Time) <-chan URLs { - copyURLsCh := make(chan URLs) - go func(sourceURLs []string, targetURL string, copyURLsCh chan URLs) { +func prepareCopyURLsTypeD(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) <-chan URLs { + copyURLsCh := make(chan URLs, 1) + + go func(ctx context.Context, cc copyURLsContent, o prepareCopyURLsOpts) { defer close(copyURLsCh) - for _, sourceURL := range sourceURLs { - for cpURLs := range prepareCopyURLsTypeC(ctx, sourceURL, targetURL, isRecursive, false, timeRef) { + + for _, sourceURL := range o.sourceURLs { + // Clone CC + newCC := cc + newCC.sourceURL = sourceURL + + for cpURLs := range prepareCopyURLsTypeC(ctx, newCC, o) { copyURLsCh <- cpURLs } } - }(sourceURLs, targetURL, copyURLsCh) + }(ctx, cc, o) + return copyURLsCh } @@ -231,25 +298,39 @@ isZip bool } +type copyURLsContent struct { + targetContent *ClientContent + targetAlias string + targetURL string + sourceContent *ClientContent + sourceAlias string + sourceURL string + copyType copyURLsType + sourceVersionID string +} + // prepareCopyURLs - prepares target and source clientURLs for copying. func prepareCopyURLs(ctx context.Context, o prepareCopyURLsOpts) chan URLs { copyURLsCh := make(chan URLs) go func(o prepareCopyURLsOpts) { defer close(copyURLsCh) - cpType, cpVersion, err := guessCopyURLType(ctx, o) - fatalIf(err.Trace(), "Unable to guess the type of copy operation.") + copyURLsContent, err := guessCopyURLType(ctx, o) + if err != nil { + copyURLsCh <- URLs{Error: errUnableToGuess().Trace(o.sourceURLs...)} + return + } - switch cpType { + switch copyURLsContent.copyType { case copyURLsTypeA: - copyURLsCh <- prepareCopyURLsTypeA(ctx, o.sourceURLs[0], cpVersion, o.targetURL, o.encKeyDB, o.isZip) + copyURLsCh <- prepareCopyURLsTypeA(ctx, *copyURLsContent, o) case copyURLsTypeB: - copyURLsCh <- prepareCopyURLsTypeB(ctx, o.sourceURLs[0], cpVersion, o.targetURL, o.encKeyDB, o.isZip) + copyURLsCh <- prepareCopyURLsTypeB(ctx, *copyURLsContent, o) case copyURLsTypeC: - for cURLs := range prepareCopyURLsTypeC(ctx, o.sourceURLs[0], o.targetURL, o.isRecursive, o.isZip, o.timeRef) { + for cURLs := range prepareCopyURLsTypeC(ctx, *copyURLsContent, o) { copyURLsCh <- cURLs } case copyURLsTypeD: - for cURLs := range prepareCopyURLsTypeD(ctx, o.sourceURLs, o.targetURL, o.isRecursive, o.timeRef) { + for cURLs := range prepareCopyURLsTypeD(ctx, *copyURLsContent, o) { copyURLsCh <- cURLs } default: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/du-main.go new/mc-20231014T015703Z/cmd/du-main.go --- old/mc-20231004T065256Z/cmd/du-main.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/du-main.go 2023-10-14 03:57:03.000000000 +0200 @@ -243,8 +243,10 @@ timeRef := parseRewindFlag(cliCtx.String("rewind")) var duErr error + var isDir bool for _, urlStr := range cliCtx.Args() { - if !isAliasURLDir(ctx, urlStr, nil, time.Time{}) { + isDir, _ = isAliasURLDir(ctx, urlStr, nil, time.Time{}) + if !isDir { fatalIf(errInvalidArgument().Trace(urlStr), fmt.Sprintf("Source `%s` is not a folder. Only folders are supported by 'du' command.", urlStr)) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/mv-main.go new/mc-20231014T015703Z/cmd/mv-main.go --- old/mc-20231004T065256Z/cmd/mv-main.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/mv-main.go 2023-10-14 03:57:03.000000000 +0200 @@ -227,7 +227,7 @@ } // check 'copy' cli arguments. - checkCopySyntax(ctx, cliCtx, encKeyDB, true) + checkCopySyntax(cliCtx) if cliCtx.NArg() == 2 { args := cliCtx.Args() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/od-main.go new/mc-20231014T015703Z/cmd/od-main.go --- old/mc-20231004T065256Z/cmd/od-main.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/od-main.go 2023-10-14 03:57:03.000000000 +0200 @@ -105,13 +105,13 @@ sourceURLs: []string{inFile}, targetURL: outFile, } - odType, _, err := guessCopyURLType(ctx, opts) + copyURLsContent, err := guessCopyURLType(ctx, opts) fatalIf(err, "Unable to guess copy URL type") // Get content of inFile, set up URLs. - switch odType { + switch copyURLsContent.copyType { case copyURLsTypeA: - odURLs = prepareOdUrls(ctx, inFile, "", outFile) + odURLs = makeCopyContentTypeA(*copyURLsContent) case copyURLsTypeB: return URLs{}, fmt.Errorf("invalid source path %s, destination cannot be a directory", outFile) default: @@ -121,25 +121,6 @@ return odURLs, nil } -func prepareOdUrls(ctx context.Context, sourceURL, sourceVersion, targetURL string) URLs { - // Extract alias before fiddling with the clientURL. - sourceAlias, _, _ := mustExpandAlias(sourceURL) - // Find alias and expanded clientURL. - targetAlias, targetURL, _ := mustExpandAlias(targetURL) - - // Placeholder encryption key database - var encKeyDB map[string][]prefixSSEPair - - _, sourceContent, err := url2Stat(ctx, sourceURL, sourceVersion, false, encKeyDB, time.Time{}, false) - if err != nil { - // Source does not exist or insufficient privileges. - return URLs{Error: err} - } - - // All OK.. We can proceed. Type A - return makeCopyContentTypeA(sourceAlias, sourceContent, targetAlias, targetURL) -} - // odCheckType checks if request is a download or upload and calls the appropriate function func odCheckType(ctx context.Context, odURLs URLs, args argKVS) (message, error) { if odURLs.SourceAlias != "" && odURLs.TargetAlias == "" { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/rm-main.go new/mc-20231014T015703Z/cmd/rm-main.go --- old/mc-20231004T065256Z/cmd/rm-main.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/rm-main.go 2023-10-14 03:57:03.000000000 +0200 @@ -253,7 +253,7 @@ // Note: UNC path using / works properly in go 1.9.2 even though it breaks the UNC specification. url = filepath.ToSlash(filepath.Clean(url)) // namespace removal applies only for non FS. So filter out if passed url represents a directory - dir := isAliasURLDir(ctx, url, encKeyDB, time.Time{}) + dir, _ := isAliasURLDir(ctx, url, encKeyDB, time.Time{}) if dir { _, path := url2Alias(url) isNamespaceRemoval = (path == "") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/support-top-api.go new/mc-20231014T015703Z/cmd/support-top-api.go --- old/mc-20231004T065256Z/cmd/support-top-api.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/support-top-api.go 2023-10-14 03:57:03.000000000 +0200 @@ -109,7 +109,7 @@ if apiCallInfo.Err != nil { fatalIf(probe.NewError(apiCallInfo.Err), "Unable to fetch top API events") } - if matchTrace(mopts, apiCallInfo) { + if mopts.matches(apiCallInfo) { p.Send(topAPIResult{ apiCallInfo: apiCallInfo, }) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/typed-errors.go new/mc-20231014T015703Z/cmd/typed-errors.go --- old/mc-20231004T065256Z/cmd/typed-errors.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/typed-errors.go 2023-10-14 03:57:03.000000000 +0200 @@ -39,6 +39,13 @@ return probe.NewError(invalidArgumentErr(errors.New(msg))).Untrace() } +type unableToGuessErr error + +var errUnableToGuess = func() *probe.Error { + msg := "Unable to guess the type of copy operation." + return probe.NewError(unableToGuessErr(errors.New(msg))) +} + type unrecognizedDiffTypeErr error var errUnrecognizedDiffType = func(diff differType) *probe.Error { @@ -97,6 +104,20 @@ return probe.NewError(invalidTargetErr(errors.New(msg))).Untrace() } +type requiresRecuriveErr error + +var errRequiresRecursive = func(URL string) *probe.Error { + msg := "To copy or move '" + URL + "' the --recursive flag is required." + return probe.NewError(requiresRecuriveErr(errors.New(msg))).Untrace() +} + +type copyIntoSelfErr error + +var errCopyIntoSelf = func(URL string) *probe.Error { + msg := "Copying or moving '" + URL + "' into itself is not allowed." + return probe.NewError(copyIntoSelfErr(errors.New(msg))).Untrace() +} + type targetNotFoundErr error var errTargetNotFound = func(URL string) *probe.Error { @@ -113,6 +134,13 @@ return probe.NewError(overwriteNotAllowedErr{errors.New(msg)}) } +type targetIsNotDirErr error + +var errTargetIsNotDir = func(URL string) *probe.Error { + msg := "Target `" + URL + "` is not a folder." + return probe.NewError(targetIsNotDirErr(errors.New(msg))).Untrace() +} + type sourceIsDirErr error var errSourceIsDir = func(URL string) *probe.Error { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/cmd/utils.go new/mc-20231014T015703Z/cmd/utils.go --- old/mc-20231004T065256Z/cmd/utils.go 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/cmd/utils.go 2023-10-14 03:57:03.000000000 +0200 @@ -435,7 +435,8 @@ }).DialContext, Proxy: ieproxy.GetProxyFunc(), TLSClientConfig: &tls.Config{ - RootCAs: globalRootCAs, + RootCAs: globalRootCAs, + InsecureSkipVerify: globalInsecure, // Can't use SSLv3 because of POODLE and BEAST // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher // Can't use TLSv1.1 because of RC4 cipher usage diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/functional-tests.sh new/mc-20231014T015703Z/functional-tests.sh --- old/mc-20231004T065256Z/functional-tests.sh 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/functional-tests.sh 2023-10-14 03:57:03.000000000 +0200 @@ -315,6 +315,16 @@ log_success "$start_time" "${FUNCNAME[0]}" } +function test_od_object() { + show "${FUNCNAME[0]}" + + start_time=$(get_time) + object_name="mc-test-object-$RANDOM" + assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd od if="${FILE_1_MB}" of="${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" + assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd od of="${FILE_1_MB}" if="${SERVER_ALIAS}/${BUCKET_NAME}/${object_name}" + + log_success "$start_time" "${FUNCNAME[0]}" +} function test_put_object() { @@ -983,6 +993,8 @@ # create a user username=foo password=foobar12345 + test_alias="aliasx" + assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin user add "$SERVER_ALIAS" "$username" "$password" # check that user appears in the user list @@ -993,49 +1005,60 @@ # setup temporary alias to make requests as the created user. scheme="https" if [ "$ENABLE_HTTPS" != "1" ]; then - scheme="http" + scheme="http" fi object1_name="mc-test-object-$RANDOM" object2_name="mc-test-object-$RANDOM" - export MC_HOST_foo="${scheme}://${username}:${password}@${SERVER_ENDPOINT}" + + # Adding an alias for the $test_alias + assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd alias set $test_alias "${scheme}://${SERVER_ENDPOINT}" ${username} ${password} + + # check that alias appears in the alias list + "${MC_CMD[@]}" --json alias list | jq -r '.alias' | grep --quiet "^${test_alias}$" + rv=$? + assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure ${rv} "alias ${test_alias} did NOT appear in the list of aliases returned by server" # check that the user can write objects with readwrite policy assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin policy attach "$SERVER_ALIAS" readwrite --user="${username}" - assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "$FILE_1_MB" "foo/${BUCKET_NAME}/${object1_name}" + + # Validate that the correct policy has been added to the user + "${MC_CMD[@]}" --json admin user list "${SERVER_ALIAS}" | jq -r '.policyName' | grep --quiet "^readwrite$" + rv=$? + assert_success "$start_time" "${FUNCNAME[0]}" show_on_failure ${rv} "user ${username} did NOT have the readwrite policy attached" + + assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "$FILE_1_MB" "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that the user cannot write objects with readonly policy assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin policy detach "$SERVER_ALIAS" readwrite --user="${username}" assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin policy attach "$SERVER_ALIAS" readonly --user="${username}" - assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cp "$FILE_1_MB" "foo/${BUCKET_NAME}/${object2_name}" + assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cp "$FILE_1_MB" "${test_alias}/${BUCKET_NAME}/${object2_name}" # check that the user can read with readonly policy - assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cat "foo/${BUCKET_NAME}/${object1_name}" + assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cat "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user can delete with readwrite policy assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin policy attach "$SERVER_ALIAS" readwrite --user="${username}" - assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "foo/${BUCKET_NAME}/${object1_name}" + assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd rm "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user cannot perform admin actions with readwrite policy - assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd admin info "foo" + assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd admin info $test_alias # create object1_name for subsequent tests. - assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "$FILE_1_MB" "foo/${BUCKET_NAME}/${object1_name}" + assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cp "$FILE_1_MB" "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user can be disabled assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin user disable "$SERVER_ALIAS" "$username" # check that disabled cannot perform any action - assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cat "foo/${BUCKET_NAME}/${object1_name}" + assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cat "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user can be enabled and can then perform an allowed action assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin user enable "$SERVER_ALIAS" "$username" - assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cat "foo/${BUCKET_NAME}/${object1_name}" + assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd cat "${test_alias}/${BUCKET_NAME}/${object1_name}" # check that user can be removed, and then is no longer available assert_success "$start_time" "${FUNCNAME[0]}" mc_cmd admin user remove "$SERVER_ALIAS" "$username" - assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cat "foo/${BUCKET_NAME}/${object1_name}" - - unset MC_HOST_foo + assert_failure "$start_time" "${FUNCNAME[0]}" mc_cmd cat "${test_alias}/${BUCKET_NAME}/${object1_name}" log_success "$start_time" "${FUNCNAME[0]}" } @@ -1057,6 +1080,7 @@ test_put_object_multipart test_get_object test_get_object_multipart + test_od_object test_mv_object test_presigned_post_policy_error test_presigned_put_object @@ -1162,8 +1186,20 @@ set +e } +function validate_dependencies() { + jqVersion=$(jq --version) + if [[ $jqVersion == *"jq"* ]]; then + echo "Dependency validation complete" + else + echo "jq is missing, please install: 'sudo apt install jq'" + exit 1 + fi +} + function main() { + validate_dependencies + ( run_test ) rv=$? diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/go.mod new/mc-20231014T015703Z/go.mod --- old/mc-20231004T065256Z/go.mod 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/go.mod 2023-10-14 03:57:03.000000000 +0200 @@ -32,8 +32,8 @@ github.com/rs/xid v1.5.0 github.com/shirou/gopsutil/v3 v3.23.8 github.com/tidwall/gjson v1.16.0 - golang.org/x/crypto v0.13.0 // indirect - golang.org/x/net v0.15.0 + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 golang.org/x/text v0.13.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/h2non/filetype.v1 v1.0.5 @@ -53,7 +53,7 @@ github.com/olekukonko/tablewriter v0.0.5 github.com/prometheus/client_model v0.4.0 github.com/rivo/tview v0.0.0-20230909130259-ba6a2a345459 - golang.org/x/term v0.12.0 + golang.org/x/term v0.13.0 ) require ( @@ -120,7 +120,7 @@ go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.25.0 // indirect golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.12.0 + golang.org/x/sys v0.13.0 google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect google.golang.org/grpc v1.58.0 // indirect google.golang.org/protobuf v1.31.0 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/mc-20231004T065256Z/go.sum new/mc-20231014T015703Z/go.sum --- old/mc-20231004T065256Z/go.sum 2023-10-04 08:52:56.000000000 +0200 +++ new/mc-20231014T015703Z/go.sum 2023-10-14 03:57:03.000000000 +0200 @@ -267,8 +267,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -284,8 +284,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -318,16 +318,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= ++++++ mc.obsinfo ++++++ --- /var/tmp/diff_new_pack.TpbW7H/_old 2023-10-16 23:01:29.022051731 +0200 +++ /var/tmp/diff_new_pack.TpbW7H/_new 2023-10-16 23:01:29.026051875 +0200 @@ -1,5 +1,5 @@ name: mc -version: 20231004T065256Z -mtime: 1696402376 -commit: eca8310ac822cf0e533c6bd3fb85c8d6099d1465 +version: 20231014T015703Z +mtime: 1697248623 +commit: d158b9a478a6a5a74795f01097d069be82edfff6 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/minio-client/vendor.tar.gz /work/SRC/openSUSE:Factory/.minio-client.new.20540/vendor.tar.gz differ: char 5, line 1