Files
rclone/cmd/archive/list/list.go
Nick Craig-Wood 67e5f435c6 accounting: fix rcat/copyurl for files.com
The files.com integration tests for rcat/copyurl were failing because
fs/account.Account was declaring a ReadAt method when the underlying
handle did not support it. The files.com SDK decided to use the ReadAt
method to speed transfers up which failed.

ReadAt and Seek methods were added in this commit to support the
archive command:

409dc75328 accounting: add io.Seeker/io.ReaderAt support to accounting.Account

This fixes the problem by adding new methods to the Account object
WithSeeker/WithReaderAt/WithReadAtSeeker which produce an object with
the desired methods or errors if it isn't possible.

This stops Account advertising things it can't do which is bad Go
practice.
2026-04-18 17:48:03 +01:00

190 lines
4.9 KiB
Go

//go:build !plan9
// Package list implements '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()
acc := tr.Account(ctx, in0)
in, err := acc.WithReadAtSeeker()
if err != nil {
return err
}
// 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)
}