1
0
mirror of https://github.com/rclone/rclone.git synced 2025-12-06 00:03:32 +00:00

Added rclone archive command to create and read archive files

Co-Authored-By: Nick Craig-Wood <nick@craig-wood.com>
This commit is contained in:
Fawzib Rojas
2025-01-05 15:15:50 +00:00
committed by Nick Craig-Wood
parent 409dc75328
commit cc09978b79
16 changed files with 1485 additions and 2 deletions

View File

@@ -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"

40
cmd/archive/archive.go Normal file
View File

@@ -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 <action> [opts] <source> [<destination>]",
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")
},
}

188
cmd/archive/archive_test.go Normal file
View File

@@ -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)
}

View File

@@ -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

View File

@@ -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] <source> [<destination>]",
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=<value>! is set then the files will have
!<value>! as prefix. It's possible to create invalid file names with
!--prefix=<value>! 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
}

View File

@@ -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

View File

@@ -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] <source> <destination>",
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
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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())
}

235
cmd/archive/files/files.go Normal file
View File

@@ -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())
},
}
}

185
cmd/archive/list/list.go Normal file
View File

@@ -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] <source>",
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)
}

View File

@@ -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

View File

@@ -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

15
go.mod
View File

@@ -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

50
go.sum
View File

@@ -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=