copyurl: add --stdout flag to write to stdout

s3-about
Nick Craig-Wood 2019-12-18 17:02:13 +00:00
parent 0b7f959433
commit 422ad38e5b
3 changed files with 93 additions and 19 deletions

View File

@ -2,6 +2,8 @@ package copyurl
import ( import (
"context" "context"
"errors"
"os"
"github.com/rclone/rclone/cmd" "github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs"
@ -12,37 +14,55 @@ import (
var ( var (
autoFilename = false autoFilename = false
stdout = false
) )
func init() { func init() {
cmd.Root.AddCommand(commandDefinition) cmd.Root.AddCommand(commandDefinition)
cmdFlags := commandDefinition.Flags() cmdFlags := commandDefinition.Flags()
flags.BoolVarP(cmdFlags, &autoFilename, "auto-filename", "a", autoFilename, "Get the file name from the url and use it for destination file path") flags.BoolVarP(cmdFlags, &autoFilename, "auto-filename", "a", autoFilename, "Get the file name from the URL and use it for destination file path")
flags.BoolVarP(cmdFlags, &stdout, "stdout", "", stdout, "Write the output to stdout rather than a file")
} }
var commandDefinition = &cobra.Command{ var commandDefinition = &cobra.Command{
Use: "copyurl https://example.com dest:path", Use: "copyurl https://example.com dest:path",
Short: `Copy url content to dest.`, Short: `Copy url content to dest.`,
Long: ` Long: `
Download urls content and copy it to destination Download a URL's content and copy it to the destination without saving
without saving it in tmp storage. it in temporary storage.
Setting --auto-filename flag will cause retrieving file name from url and using it in destination path. Setting --auto-filename will cause the file name to be retreived from
the from URL (after any redirections) and used in the destination
path.
Setting --stdout or making the output file name "-" will cause the
output to be written to standard output.
`, `,
Run: func(command *cobra.Command, args []string) { RunE: func(command *cobra.Command, args []string) (err error) {
cmd.CheckArgs(2, 2, command, args) cmd.CheckArgs(1, 2, command, args)
var dstFileName string var dstFileName string
var fsdst fs.Fs var fsdst fs.Fs
if autoFilename { if !stdout {
fsdst = cmd.NewFsDir(args[1:]) if len(args) < 2 {
} else { return errors.New("need 2 arguments if not using --stdout")
fsdst, dstFileName = cmd.NewFsDstFile(args[1:]) }
if args[1] == "-" {
stdout = true
} else if autoFilename {
fsdst = cmd.NewFsDir(args[1:])
} else {
fsdst, dstFileName = cmd.NewFsDstFile(args[1:])
}
} }
cmd.Run(true, true, command, func() error { cmd.Run(true, true, command, func() error {
_, err := operations.CopyURL(context.Background(), fsdst, dstFileName, args[0], autoFilename) if stdout {
err = operations.CopyURLToWriter(context.Background(), args[0], os.Stdout)
} else {
_, err = operations.CopyURL(context.Background(), fsdst, dstFileName, args[0], autoFilename)
}
return err return err
}) })
return nil
}, },
} }

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http"
"path" "path"
"path/filepath" "path/filepath"
"sort" "sort"
@ -1616,26 +1617,48 @@ func RcatSize(ctx context.Context, fdst fs.Fs, dstFileName string, in io.ReadClo
return obj, nil return obj, nil
} }
// CopyURL copies the data from the url to (fdst, dstFileName) // copyURLFunc is called from CopyURLFn
func CopyURL(ctx context.Context, fdst fs.Fs, dstFileName string, url string, dstFileNameFromURL bool) (dst fs.Object, err error) { type copyURLFunc func(ctx context.Context, dstFileName string, in io.ReadCloser, size int64, modTime time.Time) (err error)
// copyURLFn copies the data from the url to the function supplied
func copyURLFn(ctx context.Context, dstFileName string, url string, dstFileNameFromURL bool, fn copyURLFunc) (err error) {
client := fshttp.NewClient(fs.Config) client := fshttp.NewClient(fs.Config)
resp, err := client.Get(url) resp, err := client.Get(url)
if err != nil { if err != nil {
return nil, err return err
} }
defer fs.CheckClose(resp.Body, &err) defer fs.CheckClose(resp.Body, &err)
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, errors.Errorf("CopyURL failed: %s", resp.Status) return errors.Errorf("CopyURL failed: %s", resp.Status)
}
modTime, err := http.ParseTime(resp.Header.Get("Last-Modified"))
if err != nil {
modTime = time.Now()
} }
if dstFileNameFromURL { if dstFileNameFromURL {
dstFileName = path.Base(resp.Request.URL.Path) dstFileName = path.Base(resp.Request.URL.Path)
if dstFileName == "." || dstFileName == "/" { if dstFileName == "." || dstFileName == "/" {
return nil, errors.Errorf("CopyURL failed: file name wasn't found in url") return errors.Errorf("CopyURL failed: file name wasn't found in url")
} }
} }
return fn(ctx, dstFileName, resp.Body, resp.ContentLength, modTime)
}
return RcatSize(ctx, fdst, dstFileName, resp.Body, resp.ContentLength, time.Now()) // CopyURL copies the data from the url to (fdst, dstFileName)
func CopyURL(ctx context.Context, fdst fs.Fs, dstFileName string, url string, dstFileNameFromURL bool) (dst fs.Object, err error) {
err = copyURLFn(ctx, dstFileName, url, dstFileNameFromURL, func(ctx context.Context, dstFileName string, in io.ReadCloser, size int64, modTime time.Time) (err error) {
dst, err = RcatSize(ctx, fdst, dstFileName, in, size, modTime)
return err
})
return dst, err
}
// CopyURLToWriter copies the data from the url to the io.Writer supplied
func CopyURLToWriter(ctx context.Context, url string, out io.Writer) (err error) {
return copyURLFn(ctx, "", url, false, func(ctx context.Context, dstFileName string, in io.ReadCloser, size int64, modTime time.Time) (err error) {
_, err = io.Copy(out, in)
return err
})
} }
// BackupDir returns the correctly configured --backup-dir // BackupDir returns the correctly configured --backup-dir

View File

@ -665,6 +665,37 @@ func TestCopyURL(t *testing.T) {
fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{file1, file2, fstest.NewItem(urlFileName, contents, t1)}, nil, fs.ModTimeNotSupported) fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{file1, file2, fstest.NewItem(urlFileName, contents, t1)}, nil, fs.ModTimeNotSupported)
} }
func TestCopyURLToWriter(t *testing.T) {
contents := "file contents\n"
// check when reading from regular HTTP server
status := 0
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if status != 0 {
http.Error(w, "an error ocurred", status)
return
}
_, err := w.Write([]byte(contents))
assert.NoError(t, err)
})
ts := httptest.NewServer(handler)
defer ts.Close()
// test normal fetch
var buf bytes.Buffer
err := operations.CopyURLToWriter(context.Background(), ts.URL, &buf)
require.NoError(t, err)
assert.Equal(t, contents, buf.String())
// test fetch with error
status = http.StatusNotFound
buf.Reset()
err = operations.CopyURLToWriter(context.Background(), ts.URL, &buf)
require.Error(t, err)
assert.Contains(t, err.Error(), "Not Found")
assert.Equal(t, 0, len(buf.String()))
}
func TestMoveFile(t *testing.T) { func TestMoveFile(t *testing.T) {
r := fstest.NewRun(t) r := fstest.NewRun(t)
defer r.Finalise() defer r.Finalise()