diff --git a/cmd/all/all.go b/cmd/all/all.go index 00bdbfa8b..0f2b94ce9 100644 --- a/cmd/all/all.go +++ b/cmd/all/all.go @@ -5,6 +5,10 @@ import ( // Active commands _ "github.com/rclone/rclone/cmd" _ "github.com/rclone/rclone/cmd/about" + _ "github.com/rclone/rclone/cmd/archive" + _ "github.com/rclone/rclone/cmd/archive/create" + _ "github.com/rclone/rclone/cmd/archive/extract" + _ "github.com/rclone/rclone/cmd/archive/list" _ "github.com/rclone/rclone/cmd/authorize" _ "github.com/rclone/rclone/cmd/backend" _ "github.com/rclone/rclone/cmd/bisync" diff --git a/cmd/archive/archive.go b/cmd/archive/archive.go new file mode 100644 index 000000000..45a0b82c3 --- /dev/null +++ b/cmd/archive/archive.go @@ -0,0 +1,40 @@ +//go:build !plan9 + +// Package archive implements 'rclone archive'. +package archive + +import ( + "errors" + + "github.com/rclone/rclone/cmd" + "github.com/spf13/cobra" +) + +func init() { + cmd.Root.AddCommand(Command) +} + +// Command - archive command +var Command = &cobra.Command{ + Use: "archive [opts] []", + Short: `Perform an action on an archive.`, + Long: `Perform an action on an archive. Requires the use of a +subcommand to specify the protocol, e.g. + + rclone archive list remote:file.zip + +Each subcommand has its own options which you can see in their help. + +See [rclone archive create](/commands/rclone_archive_create/) for the +archive formats supported. +`, + Annotations: map[string]string{ + "versionIntroduced": "v1.72", + }, + RunE: func(command *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.New("archive requires an action, e.g. 'rclone archive list remote:'") + } + return errors.New("unknown action") + }, +} diff --git a/cmd/archive/archive_test.go b/cmd/archive/archive_test.go new file mode 100644 index 000000000..e9678afef --- /dev/null +++ b/cmd/archive/archive_test.go @@ -0,0 +1,188 @@ +package archive_test + +import ( + "context" + "strings" + "testing" + + "github.com/mholt/archives" + _ "github.com/rclone/rclone/backend/local" + _ "github.com/rclone/rclone/backend/memory" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/operations" + "github.com/rclone/rclone/fstest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/rclone/rclone/cmd/archive/create" + "github.com/rclone/rclone/cmd/archive/extract" + "github.com/rclone/rclone/cmd/archive/list" +) + +var ( + t1 = fstest.Time("2017-02-03T04:05:06.499999999Z") +) + +// TestMain drives the tests +func TestMain(m *testing.M) { + fstest.TestMain(m) +} + +func TestCheckValidDestination(t *testing.T) { + var err error + + ctx := context.Background() + r := fstest.NewRun(t) + + // create file + r.WriteObject(ctx, "file1.txt", "111", t1) + + // test checkValidDestination when file exists + err = create.CheckValidDestination(ctx, r.Fremote, "file1.txt") + require.NoError(t, err) + + // test checkValidDestination when file does not exist + err = create.CheckValidDestination(ctx, r.Fremote, "file2.txt") + require.NoError(t, err) + + // test checkValidDestination when dest is a directory + if r.Fremote.Features().CanHaveEmptyDirectories { + err = create.CheckValidDestination(ctx, r.Fremote, "") + require.ErrorIs(t, err, fs.ErrorIsDir) + } + + // test checkValidDestination when dest does not exists + err = create.CheckValidDestination(ctx, r.Fremote, "dir/file.txt") + require.NoError(t, err) +} + +// test archiving to the remote +func testArchiveRemote(t *testing.T, fromLocal bool, subDir string, extension string) { + var err error + ctx := context.Background() + r := fstest.NewRun(t) + var src, dst fs.Fs + var f1, f2, f3 fstest.Item + + // create files to archive on src + if fromLocal { + // create files to archive on local + src = r.Flocal + dst = r.Fremote + f1 = r.WriteFile("file1.txt", "content 1", t1) + f2 = r.WriteFile("dir1/sub1.txt", "sub content 1", t1) + f3 = r.WriteFile("dir2/sub2a.txt", "sub content 2a", t1) + } else { + // create files to archive on remote + src = r.Fremote + dst = r.Flocal + f1 = r.WriteObject(ctx, "file1.txt", "content 1", t1) + f2 = r.WriteObject(ctx, "dir1/sub1.txt", "sub content 1", t1) + f3 = r.WriteObject(ctx, "dir2/sub2a.txt", "sub content 2a", t1) + } + fstest.CheckItems(t, src, f1, f2, f3) + + // create archive on dst + archiveName := "test." + extension + err = create.ArchiveCreate(ctx, dst, archiveName, src, "", "") + require.NoError(t, err) + + // list archive on dst + expected := map[string]int64{ + "file1.txt": 9, + "dir1/": 0, + "dir1/sub1.txt": 13, + "dir2/": 0, + "dir2/sub2a.txt": 14, + } + listFile := func(ctx context.Context, f archives.FileInfo) error { + name := f.NameInArchive + gotSize := f.Size() + if f.IsDir() && !strings.HasSuffix(name, "/") { + name += "/" + gotSize = 0 + } + wantSize, found := expected[name] + assert.True(t, found, name) + assert.Equal(t, wantSize, gotSize) + delete(expected, name) + return nil + } + err = list.ArchiveList(ctx, dst, archiveName, listFile) + require.NoError(t, err) + assert.Equal(t, 0, len(expected), expected) + + // clear the src + require.NoError(t, operations.Purge(ctx, src, "")) + require.NoError(t, src.Mkdir(ctx, "")) + fstest.CheckItems(t, src) + + // extract dst archive back to src + err = extract.ArchiveExtract(ctx, src, subDir, dst, archiveName) + require.NoError(t, err) + + // check files on src are restored from the archive on dst + items := []fstest.Item{f1, f2, f3} + if subDir != "" { + for i := range items { + item := &items[i] + item.Path = subDir + "/" + item.Path + } + } + fstest.CheckListingWithPrecision(t, src, items, nil, fs.ModTimeNotSupported) +} + +func testArchive(t *testing.T) { + var extensions = []string{ + "zip", + "tar", + "tar.gz", + "tar.bz2", + "tar.lz", + "tar.lz4", + "tar.xz", + "tar.zst", + "tar.br", + "tar.sz", + "tar.mz", + } + for _, extension := range extensions { + t.Run(extension, func(t *testing.T) { + for _, subDir := range []string{"", "subdir"} { + name := subDir + if name == "" { + name = "root" + } + t.Run(name, func(t *testing.T) { + t.Run("local", func(t *testing.T) { + testArchiveRemote(t, true, name, extension) + }) + t.Run("remote", func(t *testing.T) { + testArchiveRemote(t, false, name, extension) + }) + }) + } + }) + } +} + +func TestIntegration(t *testing.T) { + testArchive(t) +} + +func TestMemory(t *testing.T) { + if *fstest.RemoteName != "" { + t.Skip("skipping as -remote is set") + } + + // Reset -remote to point to :memory: + oldFstestRemoteName := fstest.RemoteName + remoteName := ":memory:" + fstest.RemoteName = &remoteName + defer func() { + fstest.RemoteName = oldFstestRemoteName + }() + fstest.ResetRun() + + testArchive(t) +} diff --git a/cmd/archive/archive_unsupported.go b/cmd/archive/archive_unsupported.go new file mode 100644 index 000000000..f3aff28fd --- /dev/null +++ b/cmd/archive/archive_unsupported.go @@ -0,0 +1,7 @@ +// Build for unsupported platforms to stop go complaining +// about "no buildable Go source files " + +//go:build plan9 + +// Package archive implements 'rclone archive'. +package archive diff --git a/cmd/archive/create/create.go b/cmd/archive/create/create.go new file mode 100644 index 000000000..c982951d3 --- /dev/null +++ b/cmd/archive/create/create.go @@ -0,0 +1,388 @@ +//go:build !plan9 + +// Package create implements 'rclone archive create'. +package create + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path" + "sort" + "strings" + "time" + + "github.com/mholt/archives" + "github.com/rclone/rclone/cmd" + "github.com/rclone/rclone/cmd/archive" + "github.com/rclone/rclone/cmd/archive/files" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/config/flags" + "github.com/rclone/rclone/fs/filter" + "github.com/rclone/rclone/fs/operations" + "github.com/rclone/rclone/fs/walk" + "github.com/spf13/cobra" +) + +var ( + fullPath = false + prefix = "" + format = "" +) + +func init() { + flagSet := Command.Flags() + flags.BoolVarP(flagSet, &fullPath, "full-path", "", fullPath, "Set prefix for files in archive to source path", "") + flags.StringVarP(flagSet, &prefix, "prefix", "", prefix, "Set prefix for files in archive to entered value or source path", "") + flags.StringVarP(flagSet, &format, "format", "", format, "Create the archive with format or guess from extension.", "") + archive.Command.AddCommand(Command) +} + +// Command - create +var Command = &cobra.Command{ + Use: "create [flags] []", + Short: `Archive source file(s) to destination.`, + // Warning! "!" will be replaced by backticks below + Long: strings.ReplaceAll(` +Creates an archive from the files in source:path and saves the archive to +dest:path. If dest:path is missing, it will write to the console. + +The valid formats for the !--format! flag are listed below. If +!--format! is not set rclone will guess it from the extension of dest:path. + +| Format | Extensions | +|:-------|:-----------| +| zip | .zip | +| tar | .tar | +| tar.gz | .tar.gz, .tgz, .taz | +| tar.bz2| .tar.bz2, .tb2, .tbz, .tbz2, .tz2 | +| tar.lz | .tar.lz | +| tar.lz4| .tar.lz4 | +| tar.xz | .tar.xz, .txz | +| tar.zst| .tar.zst, .tzst | +| tar.br | .tar.br | +| tar.sz | .tar.sz | +| tar.mz | .tar.mz | + +The !--prefix! and !--full-path! flags control the prefix for the files +in the archive. + +If the flag !--full-path! is set then the files will have the full source +path as the prefix. + +If the flag !--prefix=! is set then the files will have +!! as prefix. It's possible to create invalid file names with +!--prefix=! so use with caution. Flag !--prefix! has +priority over !--full-path!. + +Given a directory !/sourcedir! with the following: + + file1.txt + dir1/file2.txt + +Running the command !rclone archive create /sourcedir /dest.tar.gz! +will make an archive with the contents: + + file1.txt + dir1/ + dir1/file2.txt + +Running the command !rclone archive create --full-path /sourcedir /dest.tar.gz! +will make an archive with the contents: + + sourcedir/file1.txt + sourcedir/dir1/ + sourcedir/dir1/file2.txt + +Running the command !rclone archive create --prefix=my_new_path /sourcedir /dest.tar.gz! +will make an archive with the contents: + + my_new_path/file1.txt + my_new_path/dir1/ + my_new_path/dir1/file2.txt +`, "!", "`"), + Annotations: map[string]string{ + "versionIntroduced": "v1.72", + }, + RunE: func(command *cobra.Command, args []string) error { + var src, dst fs.Fs + var dstFile string + if len(args) == 1 { // source only, archive to stdout + src = cmd.NewFsSrc(args) + } else if len(args) == 2 { + src = cmd.NewFsSrc(args) + dst, dstFile = cmd.NewFsDstFile(args[1:2]) + } else { + cmd.CheckArgs(1, 2, command, args) + } + cmd.Run(false, false, command, func() error { + fmt.Printf("dst=%v, dstFile=%q, src=%v, format=%q, prefix=%q\n", dst, dstFile, src, format, prefix) + if prefix != "" { + return ArchiveCreate(context.Background(), dst, dstFile, src, format, prefix) + } else if fullPath { + return ArchiveCreate(context.Background(), dst, dstFile, src, format, src.Root()) + } + return ArchiveCreate(context.Background(), dst, dstFile, src, format, "") + }) + return nil + }, +} + +// Globals +var ( + archiveFormats = map[string]archives.CompressedArchive{ + "zip": archives.CompressedArchive{ + Archival: archives.Zip{ContinueOnError: true}, + }, + "tar": archives.CompressedArchive{ + Archival: archives.Tar{ContinueOnError: true}, + }, + "tar.gz": archives.CompressedArchive{ + Compression: archives.Gz{}, + Archival: archives.Tar{ContinueOnError: true}, + }, + "tar.bz2": archives.CompressedArchive{ + Compression: archives.Bz2{}, + Archival: archives.Tar{ContinueOnError: true}, + }, + "tar.lz": archives.CompressedArchive{ + Compression: archives.Lzip{}, + Archival: archives.Tar{ContinueOnError: true}, + }, + "tar.lz4": archives.CompressedArchive{ + Compression: archives.Lz4{}, + Archival: archives.Tar{ContinueOnError: true}, + }, + "tar.xz": archives.CompressedArchive{ + Compression: archives.Xz{}, + Archival: archives.Tar{ContinueOnError: true}, + }, + "tar.zst": archives.CompressedArchive{ + Compression: archives.Zstd{}, + Archival: archives.Tar{ContinueOnError: true}, + }, + "tar.br": archives.CompressedArchive{ + Compression: archives.Brotli{}, + Archival: archives.Tar{ContinueOnError: true}, + }, + "tar.sz": archives.CompressedArchive{ + Compression: archives.Sz{}, + Archival: archives.Tar{ContinueOnError: true}, + }, + "tar.mz": archives.CompressedArchive{ + Compression: archives.MinLZ{}, + Archival: archives.Tar{ContinueOnError: true}, + }, + } + archiveExtensions = map[string]string{ + // zip + "*.zip": "zip", + // tar + "*.tar": "tar", + // tar.gz + "*.tar.gz": "tar.gz", + "*.tgz": "tar.gz", + "*.taz": "tar.gz", + // tar.bz2 + "*.tar.bz2": "tar.bz2", + "*.tb2": "tar.bz2", + "*.tbz": "tar.bz2", + "*.tbz2": "tar.bz2", + "*.tz2": "tar.bz2", + // tar.lz + "*.tar.lz": "tar.lz", + // tar.lz4 + "*.tar.lz4": "tar.lz4", + // tar.xz + "*.tar.xz": "tar.xz", + "*.txz": "tar.xz", + // tar.zst + "*.tar.zst": "tar.zst", + "*.tzst": "tar.zst", + // tar.br + "*.tar.br": "tar.br", + // tar.sz + "*.tar.sz": "tar.sz", + // tar.mz + "*.tar.mz": "tar.mz", + } +) + +// sorted FileInfo list + +type archivesFileInfoList []archives.FileInfo + +func (a archivesFileInfoList) Len() int { + return len(a) +} + +func (a archivesFileInfoList) Less(i, j int) bool { + if a[i].FileInfo.IsDir() == a[j].FileInfo.IsDir() { + // both are same type, order by name + return strings.Compare(a[i].NameInArchive, a[j].NameInArchive) < 0 + } else if a[i].FileInfo.IsDir() { + return strings.Compare(strings.TrimSuffix(a[i].NameInArchive, "/"), path.Dir(a[j].NameInArchive)) < 0 + } + return strings.Compare(path.Dir(a[i].NameInArchive), strings.TrimSuffix(a[j].NameInArchive, "/")) < 0 +} + +func (a archivesFileInfoList) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +func getCompressor(format string, filename string) (archives.CompressedArchive, error) { + var compressor archives.CompressedArchive + var found bool + // make filename lowercase for checks + filename = strings.ToLower(filename) + + if format == "" { + // format flag not set, get format from the file extension + for pattern, formatName := range archiveExtensions { + ok, err := path.Match(pattern, filename) + if err != nil { + // error in pattern + return archives.CompressedArchive{}, fmt.Errorf("invalid extension pattern '%s'", pattern) + } else if ok { + // pattern matches filename, get compressor + compressor, found = archiveFormats[formatName] + break + } + } + } else { + // format flag set, look for it + compressor, found = archiveFormats[format] + } + + if found { + return compressor, nil + } else if format == "" { + return archives.CompressedArchive{}, fmt.Errorf("format not set and can't be guessed from extension") + } + return archives.CompressedArchive{}, fmt.Errorf("invalid format '%s'", format) +} + +// CheckValidDestination - takes (dst, dstFile) and checks it is valid +func CheckValidDestination(ctx context.Context, dst fs.Fs, dstFile string) error { + var err error + + // check if dst + dstFile is a file + _, err = dst.NewObject(ctx, dstFile) + if err == nil { + // (dst, dstFile) is a valid file we can overwrite + return nil + } else if errors.Is(err, fs.ErrorIsDir) { + // dst is a directory + return fmt.Errorf("destination must not be a directory: %w", err) + } else if !errors.Is(err, fs.ErrorObjectNotFound) { + // dst is a directory (we need a filename) or some other error happened + // not good, leave + return fmt.Errorf("error reading destination: %w", err) + } + + // if we are here dst points to a non existent path + return nil +} + +func loadMetadata(ctx context.Context, o fs.DirEntry) fs.Metadata { + meta, err := fs.GetMetadata(ctx, o) + if err != nil { + meta = make(fs.Metadata, 0) + } + return meta +} + +// ArchiveCreate - compresses/archive source to destination +func ArchiveCreate(ctx context.Context, dst fs.Fs, dstFile string, src fs.Fs, format string, prefix string) error { + var err error + var list archivesFileInfoList + var compArchive archives.CompressedArchive + var totalLength int64 + + // check id dst is valid + err = CheckValidDestination(ctx, dst, dstFile) + if err != nil { + return err + } + + ci := fs.GetConfig(ctx) + fi := filter.GetConfig(ctx) + // get archive format + compArchive, err = getCompressor(format, dstFile) + if err != nil { + return err + } + // get source files + err = walk.ListR(ctx, src, "", false, ci.MaxDepth, walk.ListAll, func(entries fs.DirEntries) error { + // get directories + entries.ForDir(func(o fs.Directory) { + var metadata fs.Metadata + if ci.Metadata { + metadata = loadMetadata(ctx, o) + } + if fi.Include(o.Remote(), o.Size(), o.ModTime(ctx), metadata) { + info := files.NewArchiveFileInfo(ctx, o, prefix, metadata) + list = append(list, info) + } + }) + // get files + entries.ForObject(func(o fs.Object) { + var metadata fs.Metadata + if ci.Metadata { + metadata = loadMetadata(ctx, o) + } + if fi.Include(o.Remote(), o.Size(), o.ModTime(ctx), metadata) { + info := files.NewArchiveFileInfo(ctx, o, prefix, metadata) + list = append(list, info) + totalLength += o.Size() + } + }) + return nil + }) + if err != nil { + return err + } else if list.Len() == 0 { + return fmt.Errorf("no files found in source") + } + sort.Stable(list) + // create archive + if ci.DryRun { + // write nowhere + counter := files.NewCountWriter(nil) + err = compArchive.Archive(ctx, counter, list) + // log totals + fs.Infof(nil, "Total files added %d", list.Len()) + fs.Infof(nil, "Total bytes read %d", totalLength) + fs.Infof(nil, "Compressed file size %d", counter.Count()) + + return err + } else if dst == nil { + // write to stdout + counter := files.NewCountWriter(os.Stdout) + err = compArchive.Archive(ctx, counter, list) + // log totals + fs.Infof(nil, "Total files added %d", list.Len()) + fs.Infof(nil, "Total bytes read %d", totalLength) + fs.Infof(nil, "Compressed file size %d", counter.Count()) + + return err + } + // write to remote + pipeReader, pipeWriter := io.Pipe() + // write to pipewriter in background + counter := files.NewCountWriter(pipeWriter) + go func() { + err := compArchive.Archive(ctx, counter, list) + pipeWriter.CloseWithError(err) + }() + // rcat to remote from pipereader + _, err = operations.Rcat(ctx, dst, dstFile, pipeReader, time.Now(), nil) + // log totals + fs.Infof(nil, "Total files added %d", list.Len()) + fs.Infof(nil, "Total bytes read %d", totalLength) + fs.Infof(nil, "Compressed file size %d", counter.Count()) + + return err +} diff --git a/cmd/archive/create/create_unsupported.go b/cmd/archive/create/create_unsupported.go new file mode 100644 index 000000000..999733e65 --- /dev/null +++ b/cmd/archive/create/create_unsupported.go @@ -0,0 +1,7 @@ +// Build for unsupported platforms to stop go complaining +// about "no buildable Go source files " + +//go:build plan9 + +// Package archive implements 'rclone archive create'. +package create diff --git a/cmd/archive/extract/extract.go b/cmd/archive/extract/extract.go new file mode 100644 index 000000000..852a2fca6 --- /dev/null +++ b/cmd/archive/extract/extract.go @@ -0,0 +1,191 @@ +//go:build !plan9 + +// Package extract implements 'rclone archive extract' +package extract + +import ( + "context" + "errors" + "fmt" + "path" + "strings" + + "github.com/mholt/archives" + "github.com/rclone/rclone/cmd" + "github.com/rclone/rclone/cmd/archive" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/accounting" + "github.com/rclone/rclone/fs/filter" + "github.com/rclone/rclone/fs/operations" + "github.com/spf13/cobra" +) + +func init() { + archive.Command.AddCommand(Command) +} + +// Command - extract +var Command = &cobra.Command{ + Use: "extract [flags] ", + Short: `Extract archives from source to destination.`, + Long: strings.ReplaceAll(` + +Extract the archive contents to a destination directory auto detecting +the format. See [rclone archive create](/commands/rclone_archive_create/) +for the archive formats supported. + +For example on this archive: + +||| +$ rclone archive list --long remote:archive.zip + 6 2025-10-30 09:46:23.000000000 file.txt + 0 2025-10-30 09:46:57.000000000 dir/ + 4 2025-10-30 09:46:57.000000000 dir/bye.txt +||| + +You can run extract like this + +||| +$ rclone archive extract remote:archive.zip remote:extracted +||| + +Which gives this result + +||| +$ rclone tree remote:extracted +/ +├── dir +│ └── bye.txt +└── file.txt +||| + +The source or destination or both can be local or remote. + +Filters can be used to only extract certain files: + +||| +$ rclone archive extract archive.zip partial --include "bye.*" +$ rclone tree partial +/ +└── dir + └── bye.txt +||| + +The [archive backend](/archive/) can also be used to extract files. It +can be used to read only mount archives also but it supports a +different set of archive formats to the archive commands. +`, "|", "`"), + Annotations: map[string]string{ + "versionIntroduced": "v1.72", + }, + RunE: func(command *cobra.Command, args []string) error { + cmd.CheckArgs(2, 2, command, args) + + src, srcFile := cmd.NewFsFile(args[0]) + dst, dstFile := cmd.NewFsFile(args[1]) + + cmd.Run(false, false, command, func() error { + return ArchiveExtract(context.Background(), dst, dstFile, src, srcFile) + }) + return nil + }, +} + +// ArchiveExtract extracts files from (src, srcFile) to (dst, dstDir) +func ArchiveExtract(ctx context.Context, dst fs.Fs, dstDir string, src fs.Fs, srcFile string) error { + var srcObj fs.Object + var filesExtracted = 0 + var err error + + fi := filter.GetConfig(ctx) + ci := fs.GetConfig(ctx) + // get source object + srcObj, err = src.NewObject(ctx, srcFile) + fs.Debugf(nil, "srcFile: %q, src : %v", srcFile, src) + if errors.Is(err, fs.ErrorIsDir) { + return fmt.Errorf("source can't be a directory: %w", err) + } else if errors.Is(err, fs.ErrorObjectNotFound) { + return fmt.Errorf("source not found: %w", err) + } else if err != nil { + return fmt.Errorf("unable to access source: %w", err) + } + fs.Debugf(nil, "Source archive file: %s/%s", src.Root(), srcFile) + // Create destination directory + err = dst.Mkdir(ctx, dstDir) + if err != nil { + return fmt.Errorf("unable to access destination: %w", err) + } + + fs.Debugf(dst, "Destination for extracted files: %q", dstDir) + // start accounting + tr := accounting.Stats(ctx).NewTransfer(srcObj, nil) + defer tr.Done(ctx, err) + // open source + var options []fs.OpenOption + for _, option := range fs.GetConfig(ctx).DownloadHeaders { + options = append(options, option) + } + in0, err := operations.Open(ctx, srcObj, options...) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", srcFile, err) + } + // account and buffer the transfer + // in = tr.Account(ctx, in).WithBuffer() + in := tr.Account(ctx, in0) + // identify format + format, _, err := archives.Identify(ctx, "", in) + if err != nil { + return fmt.Errorf("failed to open check file type: %w", err) + } + fs.Debugf(nil, "Extract %s/%s, format %s to %s", src.Root(), srcFile, strings.TrimPrefix(format.Extension(), "."), dst.Root()) + + // check if extract is supported by format + ex, isExtract := format.(archives.Extraction) + if !isExtract { + return fmt.Errorf("extraction for %s not supported", strings.TrimPrefix(format.Extension(), ".")) + } + // extract files + err = ex.Extract(ctx, in, func(ctx context.Context, f archives.FileInfo) error { + remote := f.NameInArchive + if dstDir != "" { + remote = path.Join(dstDir, remote) + } + // check if file should be extracted + if !fi.Include(remote, f.Size(), f.ModTime(), fs.Metadata{}) { + return nil + } + // process directory + if f.IsDir() { + // directory + fs.Debugf(nil, "mkdir %s", remote) + // leave if --dry-run set + if ci.DryRun { + return nil + } + // create the directory + return operations.Mkdir(ctx, dst, remote) + } + // process file + fs.Debugf(nil, "Extract %s", remote) + // leave if --dry-run set + if ci.DryRun { + filesExtracted++ + return nil + } + // open file + fin, err := f.Open() + if err != nil { + return err + } + // extract the file to destination + _, err = operations.Rcat(ctx, dst, remote, fin, f.ModTime(), nil) + if err == nil { + filesExtracted++ + } + return err + }) + + fs.Infof(nil, "Total files extracted %d", filesExtracted) + + return err +} diff --git a/cmd/archive/extract/extract_unsupported.go b/cmd/archive/extract/extract_unsupported.go new file mode 100644 index 000000000..cb95e4cdc --- /dev/null +++ b/cmd/archive/extract/extract_unsupported.go @@ -0,0 +1,7 @@ +// Build for unsupported platforms to stop go complaining +// about "no buildable Go source files " + +//go:build plan9 + +// Package archive implements 'rclone archive extract'. +package extract diff --git a/cmd/archive/files/countwriter.go b/cmd/archive/files/countwriter.go new file mode 100644 index 000000000..1df580cbd --- /dev/null +++ b/cmd/archive/files/countwriter.go @@ -0,0 +1,34 @@ +package files + +import ( + "io" + "sync/atomic" +) + +// CountWriter counts bytes written through it. +// It is safe for concurrent Count/Reset; Write is as safe as the wrapped Writer. +type CountWriter struct { + w io.Writer + count atomic.Uint64 +} + +// NewCountWriter wraps w (use nil if you want to drop data). +func NewCountWriter(w io.Writer) *CountWriter { + if w == nil { + w = io.Discard + } + return &CountWriter{w: w} +} + +func (cw *CountWriter) Write(p []byte) (int, error) { + n, err := cw.w.Write(p) + if n > 0 { + cw.count.Add(uint64(n)) + } + return n, err +} + +// Count returns the total bytes written. +func (cw *CountWriter) Count() uint64 { + return cw.count.Load() +} diff --git a/cmd/archive/files/countwriter_test.go b/cmd/archive/files/countwriter_test.go new file mode 100644 index 000000000..3168960e5 --- /dev/null +++ b/cmd/archive/files/countwriter_test.go @@ -0,0 +1,109 @@ +package files + +import ( + "errors" + "io" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type stubWriter struct { + n int + err error +} + +func (s stubWriter) Write(p []byte) (int, error) { + if s.n > len(p) { + return len(p), s.err + } + return s.n, s.err +} + +func TestCountWriter(t *testing.T) { + t.Parallel() + + t.Run("initial count is zero", func(t *testing.T) { + cw := NewCountWriter(io.Discard) + require.Equal(t, uint64(0), cw.Count()) + }) + + t.Run("counts bytes with real writes", func(t *testing.T) { + cw := NewCountWriter(io.Discard) + n, err := cw.Write([]byte("abcd")) + require.NoError(t, err) + require.Equal(t, 4, n) + assert.Equal(t, uint64(4), cw.Count()) + + n, err = cw.Write([]byte("xyz")) + require.NoError(t, err) + require.Equal(t, 3, n) + assert.Equal(t, uint64(7), cw.Count()) + }) + + t.Run("nil writer uses io.Discard", func(t *testing.T) { + cw := NewCountWriter(nil) + n, err := cw.Write([]byte("ok")) + require.NoError(t, err) + require.Equal(t, 2, n) + assert.Equal(t, uint64(2), cw.Count()) + }) + + t.Run("zero-length write does not change count", func(t *testing.T) { + cw := NewCountWriter(io.Discard) + n, err := cw.Write(nil) + require.NoError(t, err) + require.Equal(t, 0, n) + assert.Equal(t, uint64(0), cw.Count()) + }) + + t.Run("partial write with error counts n and returns error", func(t *testing.T) { + s := stubWriter{n: 3, err: errors.New("boom")} + cw := NewCountWriter(s) + n, err := cw.Write([]byte("abcdef")) + require.Error(t, err) + require.Equal(t, 3, n) + assert.Equal(t, uint64(3), cw.Count()) + }) + + t.Run("short successful write counts returned n", func(t *testing.T) { + s := stubWriter{n: 1} + cw := NewCountWriter(s) + n, err := cw.Write([]byte("hi")) + require.NoError(t, err) + require.Equal(t, 1, n) + assert.Equal(t, uint64(1), cw.Count()) + }) +} + +func TestCountWriterConcurrent(t *testing.T) { + t.Parallel() + + const ( + goroutines = 32 + loops = 200 + chunkSize = 64 + ) + data := make([]byte, chunkSize) + + cw := NewCountWriter(io.Discard) + + var wg sync.WaitGroup + wg.Add(goroutines) + for g := 0; g < goroutines; g++ { + go func() { + defer wg.Done() + for i := 0; i < loops; i++ { + n, err := cw.Write(data) + assert.NoError(t, err) + assert.Equal(t, chunkSize, n) + } + }() + } + wg.Wait() + + want := uint64(goroutines * loops * chunkSize) + assert.Equal(t, want, cw.Count()) +} diff --git a/cmd/archive/files/files.go b/cmd/archive/files/files.go new file mode 100644 index 000000000..f1c52d0c8 --- /dev/null +++ b/cmd/archive/files/files.go @@ -0,0 +1,235 @@ +// Package files implements io/fs objects +package files + +import ( + "archive/tar" + "context" + "fmt" + "io" + stdfs "io/fs" + "path" + "strconv" + "strings" + "time" + + "github.com/mholt/archives" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/accounting" + "github.com/rclone/rclone/fs/operations" +) + +// fill tar.Header with metadata if available (too bad username/groupname is not available) +func metadataToHeader(metadata fs.Metadata, header *tar.Header) { + var val string + var ok bool + var err error + var mode, uid, gid int64 + var atime, ctime time.Time + var uname, gname string + // check if metadata is valid + if metadata != nil { + // mode + val, ok = metadata["mode"] + if !ok { + mode = 0644 + } else { + mode, err = strconv.ParseInt(val, 8, 64) + if err != nil { + mode = 0664 + } + } + // uid + val, ok = metadata["uid"] + if !ok { + uid = 0 + } else { + uid, err = strconv.ParseInt(val, 10, 32) + if err != nil { + uid = 0 + } + } + // gid + val, ok = metadata["gid"] + if !ok { + gid = 0 + } else { + gid, err = strconv.ParseInt(val, 10, 32) + if err != nil { + gid = 0 + } + } + // access time + val, ok := metadata["atime"] + if !ok { + atime = time.Unix(0, 0) + } else { + atime, err = time.Parse(time.RFC3339Nano, val) + if err != nil { + atime = time.Unix(0, 0) + } + } + // set uname/gname + if uid == 0 { + uname = "root" + } else { + uname = strconv.FormatInt(uid, 10) + } + if gid == 0 { + gname = "root" + } else { + gname = strconv.FormatInt(gid, 10) + } + } else { + mode = 0644 + uid = 0 + gid = 0 + uname = "root" + gname = "root" + atime = header.ModTime + ctime = header.ModTime + } + // set values + header.Mode = mode + header.Uid = int(uid) + header.Gid = int(gid) + header.Uname = uname + header.Gname = gname + header.AccessTime = atime + header.ChangeTime = ctime +} + +// structs for fs.FileInfo,fs.File,SeekableFile + +type fileInfoImpl struct { + header *tar.Header +} + +type fileImpl struct { + entry stdfs.FileInfo + ctx context.Context + reader io.ReadSeekCloser + transfer *accounting.Transfer + err error +} + +func newFileInfo(ctx context.Context, entry fs.DirEntry, prefix string, metadata fs.Metadata) stdfs.FileInfo { + var fi = new(fileInfoImpl) + + fi.header = new(tar.Header) + if prefix != "" { + fi.header.Name = path.Join(strings.TrimPrefix(prefix, "/"), entry.Remote()) + } else { + fi.header.Name = entry.Remote() + } + fi.header.Size = entry.Size() + fi.header.ModTime = entry.ModTime(ctx) + // set metadata + metadataToHeader(metadata, fi.header) + // flag if directory + _, isDir := entry.(fs.Directory) + if isDir { + fi.header.Mode = int64(stdfs.ModeDir) | fi.header.Mode + } + + return fi +} + +func (a *fileInfoImpl) Name() string { + return a.header.Name +} + +func (a *fileInfoImpl) Size() int64 { + return a.header.Size +} + +func (a *fileInfoImpl) Mode() stdfs.FileMode { + return stdfs.FileMode(a.header.Mode) +} + +func (a *fileInfoImpl) ModTime() time.Time { + return a.header.ModTime +} + +func (a *fileInfoImpl) IsDir() bool { + return (a.header.Mode & int64(stdfs.ModeDir)) != 0 +} + +func (a *fileInfoImpl) Sys() any { + return a.header +} + +func (a *fileInfoImpl) String() string { + return fmt.Sprintf("Name=%v Size=%v IsDir=%v UID=%v GID=%v", a.Name(), a.Size(), a.IsDir(), a.header.Uid, a.header.Gid) +} + +// create a fs.File compatible struct +func newFile(ctx context.Context, obj fs.Object, fi stdfs.FileInfo) (stdfs.File, error) { + var f = new(fileImpl) + // create stdfs.File + f.entry = fi + f.ctx = ctx + f.err = nil + // create transfer + f.transfer = accounting.Stats(ctx).NewTransfer(obj, nil) + // get open options + var options []fs.OpenOption + for _, option := range fs.GetConfig(ctx).DownloadHeaders { + options = append(options, option) + } + // open file + f.reader, f.err = operations.Open(ctx, obj, options...) + if f.err != nil { + defer f.transfer.Done(ctx, f.err) + return nil, f.err + } + // Account the transfer + f.reader = f.transfer.Account(ctx, f.reader) + + return f, f.err +} + +func (a *fileImpl) Stat() (stdfs.FileInfo, error) { + return a.entry, nil +} + +func (a *fileImpl) Read(data []byte) (int, error) { + if a.reader == nil { + a.err = fmt.Errorf("file %s not open", a.entry.Name()) + return 0, a.err + } + i, err := a.reader.Read(data) + a.err = err + return i, a.err +} + +func (a *fileImpl) Close() error { + // close file + if a.reader == nil { + a.err = fmt.Errorf("file %s not open", a.entry.Name()) + } else { + a.err = a.reader.Close() + } + // close transfer + a.transfer.Done(a.ctx, a.err) + + return a.err +} + +// NewArchiveFileInfo will take a fs.DirEntry and return a archives.Fileinfo +func NewArchiveFileInfo(ctx context.Context, entry fs.DirEntry, prefix string, metadata fs.Metadata) archives.FileInfo { + fi := newFileInfo(ctx, entry, prefix, metadata) + + return archives.FileInfo{ + FileInfo: fi, + NameInArchive: fi.Name(), + LinkTarget: "", + Open: func() (stdfs.File, error) { + obj, isObject := entry.(fs.Object) + if isObject { + return newFile(ctx, obj, fi) + } + return nil, fmt.Errorf("%s is not a file", fi.Name()) + }, + } + +} diff --git a/cmd/archive/list/list.go b/cmd/archive/list/list.go new file mode 100644 index 000000000..6935b07f6 --- /dev/null +++ b/cmd/archive/list/list.go @@ -0,0 +1,185 @@ +//go:build !plan9 + +// Package list inplements 'rclone archive list' +package list + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/mholt/archives" + "github.com/rclone/rclone/cmd" + "github.com/rclone/rclone/cmd/archive" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/accounting" + "github.com/rclone/rclone/fs/config/flags" + "github.com/rclone/rclone/fs/filter" + "github.com/rclone/rclone/fs/operations" + "github.com/spf13/cobra" +) + +var ( + longList = false + plainList = false + filesOnly = false + dirsOnly = false +) + +func init() { + flagSet := Command.Flags() + flags.BoolVarP(flagSet, &longList, "long", "", longList, "List extra attributtes", "") + flags.BoolVarP(flagSet, &plainList, "plain", "", plainList, "Only list file names", "") + flags.BoolVarP(flagSet, &filesOnly, "files-only", "", false, "Only list files", "") + flags.BoolVarP(flagSet, &dirsOnly, "dirs-only", "", false, "Only list directories", "") + archive.Command.AddCommand(Command) +} + +// Command - list +var Command = &cobra.Command{ + Use: "list [flags] ", + Short: `List archive contents from source.`, + Long: strings.ReplaceAll(` +List the contents of an archive to the console, auto detecting the +format. See [rclone archive create](/commands/rclone_archive_create/) +for the archive formats supported. + +For example: + +||| +$ rclone archive list remote:archive.zip + 6 file.txt + 0 dir/ + 4 dir/bye.txt +||| + +Or with |--long| flag for more info: + +||| +$ rclone archive list --long remote:archive.zip + 6 2025-10-30 09:46:23.000000000 file.txt + 0 2025-10-30 09:46:57.000000000 dir/ + 4 2025-10-30 09:46:57.000000000 dir/bye.txt +||| + +Or with |--plain| flag which is useful for scripting: + +||| +$ rclone archive list --plain /path/to/archive.zip +file.txt +dir/ +dir/bye.txt +||| + +Or with |--dirs-only|: + +||| +$ rclone archive list --plain --dirs-only /path/to/archive.zip +dir/ +||| + +Or with |--files-only|: + +||| +$ rclone archive list --plain --files-only /path/to/archive.zip +file.txt +dir/bye.txt +||| + +Filters may also be used: + +||| +$ rclone archive list --long archive.zip --include "bye.*" + 4 2025-10-30 09:46:57.000000000 dir/bye.txt +||| + +The [archive backend](/archive/) can also be used to list files. It +can be used to read only mount archives also but it supports a +different set of archive formats to the archive commands. +`, "|", "`"), + Annotations: map[string]string{ + "versionIntroduced": "v1.72", + }, + RunE: func(command *cobra.Command, args []string) error { + cmd.CheckArgs(1, 1, command, args) + src, srcFile := cmd.NewFsFile(args[0]) + cmd.Run(false, false, command, func() error { + return ArchiveList(context.Background(), src, srcFile, listFile) + }) + return nil + }, +} + +func listFile(ctx context.Context, f archives.FileInfo) error { + ci := fs.GetConfig(ctx) + fi := filter.GetConfig(ctx) + + // check if excluded + if !fi.Include(f.NameInArchive, f.Size(), f.ModTime(), fs.Metadata{}) { + return nil + } + if filesOnly && f.IsDir() { + return nil + } + if dirsOnly && !f.IsDir() { + return nil + } + // get entry name + name := f.NameInArchive + if f.IsDir() && !strings.HasSuffix(name, "/") { + name += "/" + } + // print info + if longList { + operations.SyncFprintf(os.Stdout, "%s %s %s\n", operations.SizeStringField(f.Size(), ci.HumanReadable, 9), f.ModTime().Format("2006-01-02 15:04:05.000000000"), name) + } else if plainList { + operations.SyncFprintf(os.Stdout, "%s\n", name) + } else { + operations.SyncFprintf(os.Stdout, "%s %s\n", operations.SizeStringField(f.Size(), ci.HumanReadable, 9), name) + } + return nil +} + +// ArchiveList -- print a list of the files in the archive +func ArchiveList(ctx context.Context, src fs.Fs, srcFile string, listFn archives.FileHandler) error { + var srcObj fs.Object + var err error + // get object + srcObj, err = src.NewObject(ctx, srcFile) + if err != nil { + return fmt.Errorf("source is not a file, %w", err) + } + fs.Debugf(nil, "Source archive file: %s/%s", src.Root(), srcFile) + // start accounting + tr := accounting.Stats(ctx).NewTransfer(srcObj, nil) + defer func() { + tr.Done(ctx, err) + }() + // open source + var options []fs.OpenOption + for _, option := range fs.GetConfig(ctx).DownloadHeaders { + options = append(options, option) + } + in0, err := operations.Open(ctx, srcObj, options...) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", srcFile, err) + } + // account and buffer the transfer + // in = tr.Account(ctx, in).WithBuffer() + in := tr.Account(ctx, in0) + // identify format + format, _, err := archives.Identify(ctx, "", in) + + if err != nil { + return fmt.Errorf("failed to open check file type, %w", err) + } + fs.Debugf(nil, "Listing %s/%s, format %s", src.Root(), srcFile, strings.TrimPrefix(format.Extension(), ".")) + // check if extract is supported by format + ex, isExtract := format.(archives.Extraction) + if !isExtract { + return fmt.Errorf("extraction for %s not supported", strings.TrimPrefix(format.Extension(), ".")) + } + // list files + return ex.Extract(ctx, in, listFn) +} diff --git a/cmd/archive/list/list_unsupported.go b/cmd/archive/list/list_unsupported.go new file mode 100644 index 000000000..487994011 --- /dev/null +++ b/cmd/archive/list/list_unsupported.go @@ -0,0 +1,7 @@ +// Build for unsupported platforms to stop go complaining +// about "no buildable Go source files " + +//go:build plan9 + +// Package archive implements 'rclone archive list'. +package list diff --git a/docs/content/archive.md b/docs/content/archive.md index 3745682e3..e12ec2c03 100644 --- a/docs/content/archive.md +++ b/docs/content/archive.md @@ -7,7 +7,9 @@ versionIntroduced: "v1.72" # {{< icon "fas fa-archive" >}} Archive The Archive backend allows read only access to the content of archive -files on cloud storage without downloading them completely. +files on cloud storage without downloading the complete archive. This +means you could mount a large archive file and use only the parts of +it your application requires, rather than having to extract it. The archive files are recognised by their extension. @@ -19,6 +21,18 @@ The archive files are recognised by their extension. The supported archive file types are cloud friendly - a single file can be found and downloaded without downloading the whole archive. +If you just want to create, list or extract archives and don't want to +mount them then you may find the `rclone archive` commands more +convenient. + +- [rclone archive create](/commands/rclone_archive_create/) +- [rclone archive list](/commands/rclone_archive_list/) +- [rclone archive extract](/commands/rclone_archive_extract/) + +These commands supports a wider range of non cloud friendly archives +(but not squashfs) but can't be used for `rclone mount` or any other +rclone commands (eg `rclone check`). + ## Configuration This backend is best used without configuration. @@ -184,7 +198,9 @@ mksquashfs 100files 100files.sqfs -comp zstd -b 1M ## Limitations -Files in archive are read only. It isn't possible to create archives yet. +Files in the archive backend are read only. It isn't possible to +create archives with the archive backend yet. However you **can** create +archives with [rclone archive create](/commands/rclone_archive_create/). Only `.zip` and `.sqfs` archives are supported as these are the only common archiving formats which make it easy to read directory listings diff --git a/go.mod b/go.mod index 715e9be7e..62d28ff6f 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/lanrat/extsort v1.4.2 github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-runewidth v0.0.17 + github.com/mholt/archives v0.1.5 github.com/minio/minio-go/v7 v7.0.95 github.com/mitchellh/go-homedir v1.1.0 github.com/moby/sys/mountinfo v0.7.2 @@ -110,9 +111,11 @@ require ( github.com/ProtonMail/go-srp v0.0.7 // indirect github.com/ProtonMail/gopenpgp/v2 v2.9.0 // indirect github.com/PuerkitoBio/goquery v1.10.3 // indirect + github.com/STARRY-S/zip v0.2.3 // indirect github.com/akavel/rsrc v0.10.2 // indirect github.com/anacrolix/generics v0.1.0 // indirect github.com/anchore/go-lzo v0.1.0 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/appscode/go-querystring v0.0.0-20170504095604-0126cfb3f1dc // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect @@ -131,6 +134,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.38.5 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.1 // indirect + github.com/bodgit/windows v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bradenaw/juniper v0.15.3 // indirect github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect @@ -144,6 +150,7 @@ require ( github.com/creasty/defaults v1.8.0 // indirect github.com/cronokirby/saferith v0.33.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.9.0 // indirect github.com/emersion/go-message v0.18.2 // indirect @@ -184,6 +191,7 @@ require ( github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jtolio/noiseconn v0.0.0-20231127013910-f6d9ecbf1de7 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect github.com/kr/fs v0.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -192,11 +200,14 @@ require ( github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minlz v1.0.1 // indirect github.com/minio/xxml v0.0.3 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/panjf2000/ants/v2 v2.11.3 // indirect github.com/pengsrc/go-shared v0.2.1-0.20190131101655-1999055a4a14 // indirect @@ -219,7 +230,10 @@ require ( github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect github.com/smartystreets/goconvey v1.8.1 // indirect github.com/sony/gobreaker v1.0.0 // indirect + github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/spacemonkeygo/monkit/v3 v3.0.24 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/therootcompany/xz v1.0.1 // indirect github.com/tinylib/msgp v1.4.0 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect @@ -234,6 +248,7 @@ require ( go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect golang.org/x/tools v0.37.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect diff --git a/go.sum b/go.sum index 6b369a8b9..b4a53d4fd 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,10 @@ github.com/ProtonMail/gopenpgp/v2 v2.9.0 h1:ruLzBmwe4dR1hdnrsEJ/S7psSBmV15gFttFU github.com/ProtonMail/gopenpgp/v2 v2.9.0/go.mod h1:IldDyh9Hv1ZCCYatTuuEt1XZJ0OPjxLpTarDfglih7s= github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= +github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= +github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/a8m/tree v0.0.0-20240104212747-2c8764a5f17e h1:KMVieI1/Ub++GYfnhyFPoGE3g5TUiG4srE3TMGr5nM4= github.com/a8m/tree v0.0.0-20240104212747-2c8764a5f17e/go.mod h1:j5astEcUkZQX8lK+KKlQ3NRQ50f4EE8ZjyZpCz3mrH4= github.com/aalpar/deheap v0.0.0-20210914013432-0cc84d79dec3 h1:hhdWprfSpFbN7lz3W1gM40vOgvSh1WCSMxYD6gGB4Hs= @@ -104,6 +108,10 @@ github.com/anacrolix/log v0.17.0 h1:cZvEGRPCbIg+WK+qAxWj/ap2Gj8cx1haOCSVxNZQpK4= github.com/anacrolix/log v0.17.0/go.mod h1:m0poRtlr41mriZlXBQ9SOVZ8yZBkLjOkDhd5Li5pITA= github.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs= github.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/appscode/go-querystring v0.0.0-20170504095604-0126cfb3f1dc h1:LoL75er+LKDHDUfU5tRvFwxH0LjPpZN8OoG8Ll+liGU= @@ -154,6 +162,14 @@ github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPn github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= +github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= +github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradenaw/juniper v0.15.3 h1:RHIAMEDTpvmzV1wg1jMAHGOoI2oJUSPx3lxRldXnFGo= @@ -219,6 +235,9 @@ github.com/dop251/scsu v0.0.0-20220106150536-84ac88021d00 h1:xJBhC00smQpSZw3Kr0E github.com/dop251/scsu v0.0.0-20220106150536-84ac88021d00/go.mod h1:nNICngOdmNImBb/vuL+dSc0aIg3ryNATpjxThNoPw4g= github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 h1:FT+t0UEDykcor4y3dMVKXIiWJETBpRgERYTGlmMd7HU= github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -338,6 +357,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -429,11 +449,15 @@ github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRt github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/koofr/go-httpclient v0.0.0-20240520111329-e20f8f203988 h1:CjEMN21Xkr9+zwPmZPaJJw+apzVbjGL5uK/6g9Q2jGU= github.com/koofr/go-httpclient v0.0.0-20240520111329-e20f8f203988/go.mod h1:/agobYum3uo/8V6yPVnq+R82pyVGCeuWW5arT4Txn8A= github.com/koofr/go-koofrclient v0.0.0-20221207135200-cbd7fc9ad6a6 h1:FHVoZMOVRA+6/y4yRlbiR3WvsrOcKBd/f64H7YiWR2U= @@ -469,12 +493,20 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mholt/archives v0.0.0-20241226194006-fc8400ac3529 h1:XsFbmbdHgEXRCASoX0wlUi1Es+yTDhsmiUo2UVukmLs= +github.com/mholt/archives v0.0.0-20241226194006-fc8400ac3529/go.mod h1:j/Ire/jm42GN7h90F5kzj6hf6ZFzEH66de+hmjEKu+I= +github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= +github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= +github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= +github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= +github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= +github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/minio/xxml v0.0.3 h1:ZIpPQpfyG5uZQnqqC0LZuWtPk/WT8G/qkxvO6jb7zMU= github.com/minio/xxml v0.0.3/go.mod h1:wcXErosl6IezQIMEWSK/LYC2VS7LJ1dAkgvuyIN3aH4= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -491,6 +523,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncw/swift/v2 v2.0.4 h1:hHWVFxn5/YaTWAASmn4qyq2p6OyP/Hm3vMLzkjEqR7w= github.com/ncw/swift/v2 v2.0.4/go.mod h1:cbAO76/ZwcFrFlHdXPjaqWZ9R7Hdar7HpjRXBfbjigk= +github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= +github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= +github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= @@ -566,6 +602,7 @@ github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= @@ -591,8 +628,14 @@ github.com/snabb/httpreaderat v1.0.1/go.mod h1:lpbGrKDWF37yvRbtRvQsbesS6Ty5c83t8 github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= +github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= +github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/spacemonkeygo/monkit/v3 v3.0.24 h1:cKixJ+evHnfJhWNyIZjBy5hoW8LTWmrJXPo18tzLNrk= github.com/spacemonkeygo/monkit/v3 v3.0.24/go.mod h1:XkZYGzknZwkD0AKUnZaSXhRiVTLCkq7CWVa3IsE72gA= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -618,6 +661,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/t3rm1n4l/go-mega v0.0.0-20250926104142-ccb8d3498e6c h1:BLopNCyqewbE8+BtlIp/Juzu8AJGxz0gHdGADnsblVc= github.com/t3rm1n4l/go-mega v0.0.0-20250926104142-ccb8d3498e6c/go.mod h1:ykucQyiE9Q2qx1wLlEtZkkNn1IURib/2O+Mvd25i1Fo= github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8= github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= @@ -630,6 +675,7 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/unknwon/goconfig v1.0.0 h1:rS7O+CmUdli1T+oDm7fYj1MwqNWtEJfNj+FqcUHML8U= @@ -644,6 +690,8 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -694,6 +742,8 @@ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= goftp.io/server/v2 v2.0.2 h1:tkZpqyXys+vC15W5yGMi8Kzmbv1QSgeKr8qJXBnJbm8= goftp.io/server/v2 v2.0.2/go.mod h1:Fl1WdcV7fx1pjOWx7jEHb7tsJ8VwE7+xHu6bVJ6r2qg= golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=