diff --git a/README.md b/README.md index 333280b..9f29398 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ All `--sort`, `--reverse`, `--time`, and `--time-format` flags work with both `l ```sh $ dbxcli share-link create /file.txt # create or return an existing shared link +$ dbxcli share-link download [target] # download a shared-link file $ dbxcli share-link info # display shared link information $ dbxcli share-link list # list existing shared links $ dbxcli share-link list /file.txt # list direct shared links for a path @@ -238,6 +239,8 @@ $ dbxcli share list link # deprecated compatibility command $ dbxcli share list folder # list shared folders ``` +`share-link download` writes to the metadata filename when `target` is omitted. Use `-` as the target to write file bytes to stdout, and `--password` for password-protected shared links. + New and changed commands should write command results to stdout. Status, progress, warnings, diagnostics, and verbose logs should go to stderr. ### Team management diff --git a/cmd/share_create_link_test.go b/cmd/share_create_link_test.go index 47a7c26..4753763 100644 --- a/cmd/share_create_link_test.go +++ b/cmd/share_create_link_test.go @@ -17,6 +17,7 @@ package cmd import ( "bytes" "fmt" + "io" "path" "strings" "testing" @@ -29,6 +30,7 @@ import ( type mockSharedLinkClient struct { createSharedLinkWithSettingsFn func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) + getSharedLinkFileFn func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) getSharedLinkMetadataFn func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error) listSharedLinksFn func(arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error) modifySharedLinkSettingsFn func(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) @@ -42,6 +44,13 @@ func (m *mockSharedLinkClient) CreateSharedLinkWithSettings(arg *sharing.CreateS return nil, nil } +func (m *mockSharedLinkClient) GetSharedLinkFile(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + if m.getSharedLinkFileFn != nil { + return m.getSharedLinkFileFn(arg) + } + return nil, nil, nil +} + func (m *mockSharedLinkClient) GetSharedLinkMetadata(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error) { if m.getSharedLinkMetadataFn != nil { return m.getSharedLinkMetadataFn(arg) diff --git a/cmd/share_link.go b/cmd/share_link.go index 6ce220f..34158e2 100644 --- a/cmd/share_link.go +++ b/cmd/share_link.go @@ -15,6 +15,8 @@ package cmd import ( + "io" + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/sharing" "github.com/spf13/cobra" @@ -22,6 +24,7 @@ import ( type sharedLinkClient interface { CreateSharedLinkWithSettings(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) + GetSharedLinkFile(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) GetSharedLinkMetadata(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error) ListSharedLinks(arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error) ModifySharedLinkSettings(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) diff --git a/cmd/share_link_download.go b/cmd/share_link_download.go new file mode 100644 index 0000000..a767d17 --- /dev/null +++ b/cmd/share_link_download.go @@ -0,0 +1,233 @@ +// Copyright © 2016 Dropbox, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/sharing" + "github.com/dustin/go-humanize" + "github.com/mitchellh/ioprogress" + "github.com/spf13/cobra" +) + +func shareLinkDownload(cmd *cobra.Command, args []string) error { + if len(args) == 0 || len(args) > 2 { + return errors.New("`share-link download` requires a `url` and optional `target` argument") + } + + url := args[0] + if url == "" { + return errors.New("`share-link download` requires a non-empty URL") + } + + target := "" + if len(args) == 2 { + target = args[1] + if target == "" { + return errors.New("`share-link download` requires a non-empty target") + } + } + + arg := sharing.NewGetSharedLinkMetadataArg(url) + password, err := cmd.Flags().GetString("password") + if err != nil { + return err + } + arg.LinkPassword = password + + dbx := newSharedLinkClient(config) + if target == "-" { + if err := downloadSharedLinkToStdout(dbx, arg, cmd.OutOrStdout()); err != nil { + return err + } + commandVerboseStatus(cmd, "Downloaded shared link to stdout") + return nil + } + + dst, err := downloadSharedLinkToFile(dbx, arg, target, cmd.ErrOrStderr()) + if err != nil { + return err + } + commandVerboseStatus(cmd, "Downloaded shared link to %s", dst) + return nil +} + +func downloadSharedLinkToFile(dbx sharedLinkClient, arg *sharing.GetSharedLinkMetadataArg, target string, errOut io.Writer) (string, error) { + var dst string + err := retryWithBackoff(func() error { + link, contents, err := dbx.GetSharedLinkFile(arg) + if err != nil { + return err + } + if contents == nil { + return errors.New("shared link download response did not include file content") + } + defer func() { _ = contents.Close() }() + + dst, err = sharedLinkDownloadTarget(target, link) + if err != nil { + return err + } + + return copySharedLinkContentToFile(contents, sharedLinkDownloadSize(link), dst, errOut) + }) + return dst, err +} + +func downloadSharedLinkToStdout(dbx sharedLinkClient, arg *sharing.GetSharedLinkMetadataArg, w io.Writer) error { + ignoreBrokenPipeSignal() + + var bytesWritten int64 + return retryWithBackoff(func() error { + if bytesWritten > 0 { + return partialStdoutError(bytesWritten) + } + + _, contents, err := dbx.GetSharedLinkFile(arg) + if err != nil { + return err + } + if contents == nil { + return errors.New("shared link download response did not include file content") + } + defer func() { _ = contents.Close() }() + + n, copyErr := io.Copy(stdoutBrokenPipeWriter{w: w}, contents) + bytesWritten += n + + if errors.Is(copyErr, errStdoutBrokenPipe) { + return nil + } + if copyErr != nil && bytesWritten > 0 { + return partialStdoutError(bytesWritten) + } + return copyErr + }) +} + +func copySharedLinkContentToFile(contents io.Reader, size uint64, dst string, errOut io.Writer) error { + if errOut == nil { + errOut = io.Discard + } + + finalDst, err := downloadDestinationPath(dst) + if err != nil { + return err + } + + f, tmp, err := createDownloadTemp(finalDst) + if err != nil { + return err + } + removeTemp := true + defer func() { + if removeTemp { + _ = os.Remove(tmp) + } + }() + + progressbar := &ioprogress.Reader{ + Reader: contents, + DrawFunc: ioprogress.DrawTerminalf(errOut, func(progress, total int64) string { + return fmt.Sprintf("Downloading %s/%s", + humanize.IBytes(uint64(progress)), humanize.IBytes(uint64(total))) + }), + Size: int64(size), + } + + _, copyErr := io.Copy(f, progressbar) + closeErr := f.Close() + if copyErr != nil { + return copyErr + } + if closeErr != nil { + return closeErr + } + if err := os.Rename(tmp, finalDst); err != nil { + return err + } + removeTemp = false + return nil +} + +func sharedLinkDownloadTarget(target string, link sharing.IsSharedLinkMetadata) (string, error) { + name, err := sharedLinkDownloadName(link) + if err != nil { + return "", err + } + + if target == "" { + return name, nil + } + + if info, err := os.Stat(target); err == nil && info.IsDir() { + return filepath.Join(target, name), nil + } else if err != nil && !os.IsNotExist(err) { + return "", err + } + + return target, nil +} + +func sharedLinkDownloadName(link sharing.IsSharedLinkMetadata) (string, error) { + file, ok := link.(*sharing.FileLinkMetadata) + if !ok { + return "", errors.New("shared link is not a downloadable file") + } + + name := file.Name + if name == "" && file.PathLower != "" { + name = path.Base(file.PathLower) + } + name = filepath.Base(filepath.FromSlash(name)) + if name == "" || name == "." || name == ".." || name == string(filepath.Separator) { + return "", errors.New("shared link file metadata did not include a name") + } + return name, nil +} + +func sharedLinkDownloadSize(link sharing.IsSharedLinkMetadata) uint64 { + file, ok := link.(*sharing.FileLinkMetadata) + if !ok { + return 0 + } + return file.Size +} + +var shareLinkDownloadCmd = &cobra.Command{ + Use: "download [target]", + Short: "Download a shared link file", + Long: `Download a file from a Dropbox shared link. + - If target is omitted, the local filename comes from shared-link metadata. + - Use - as target to write file bytes to stdout. + Stdout is byte-clean: all progress and errors go to stderr. + - Folder-link recursive download is not supported by this command. +`, + Example: ` dbxcli share-link download https://www.dropbox.com/s/example/file.txt + dbxcli share-link download https://www.dropbox.com/s/example/file.txt ./local-file.txt + dbxcli share-link download https://www.dropbox.com/s/example/file.txt - | tar tz`, + RunE: shareLinkDownload, +} + +func init() { + shareLinkDownloadCmd.Flags().String("password", "", "Password for password-protected shared links") + shareLinkCmd.AddCommand(shareLinkDownloadCmd) +} diff --git a/cmd/share_link_download_test.go b/cmd/share_link_download_test.go new file mode 100644 index 0000000..4989c6e --- /dev/null +++ b/cmd/share_link_download_test.go @@ -0,0 +1,384 @@ +// Copyright © 2016 Dropbox, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/sharing" + "github.com/spf13/cobra" +) + +func TestShareLinkDownloadRequiresURLAndOptionalTarget(t *testing.T) { + tests := []struct { + name string + args []string + }{ + {name: "missing URL", args: nil}, + {name: "too many args", args: []string{"https://example.com/one", "target", "extra"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + called := false + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + called = true + return nil, nil, nil + }, + }) + + err := shareLinkDownload(newShareLinkDownloadTestCommand(nil, nil), tt.args) + if err == nil || !strings.Contains(err.Error(), "requires a `url` and optional `target` argument") { + t.Fatalf("error = %v, want url/target argument error", err) + } + if called { + t.Fatal("GetSharedLinkFile should not be called") + } + }) + } +} + +func TestShareLinkDownloadRejectsEmptyURL(t *testing.T) { + called := false + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + called = true + return nil, nil, nil + }, + }) + + err := shareLinkDownload(newShareLinkDownloadTestCommand(nil, nil), []string{""}) + if err == nil || !strings.Contains(err.Error(), "requires a non-empty URL") { + t.Fatalf("error = %v, want non-empty URL error", err) + } + if called { + t.Fatal("GetSharedLinkFile should not be called") + } +} + +func TestShareLinkDownloadRejectsEmptyTarget(t *testing.T) { + called := false + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + called = true + return nil, nil, nil + }, + }) + + err := shareLinkDownload(newShareLinkDownloadTestCommand(nil, nil), []string{"https://example.com/link", ""}) + if err == nil || !strings.Contains(err.Error(), "requires a non-empty target") { + t.Fatalf("error = %v, want non-empty target error", err) + } + if called { + t.Fatal("GetSharedLinkFile should not be called") + } +} + +func TestShareLinkDownloadUsesMetadataNameAndPassword(t *testing.T) { + tmp := t.TempDir() + t.Chdir(tmp) + + content := "shared content" + var requested *sharing.GetSharedLinkMetadataArg + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + requested = arg + return downloadableSharedLinkFile("report.txt", "/docs/report.txt", "https://example.com/link", uint64(len(content))), + io.NopCloser(strings.NewReader(content)), nil + }, + }) + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd := newShareLinkDownloadTestCommand(&stdout, &stderr) + if err := cmd.Flags().Set("password", "secret"); err != nil { + t.Fatalf("set password: %v", err) + } + + if err := shareLinkDownload(cmd, []string{"https://example.com/link"}); err != nil { + t.Fatalf("shareLinkDownload error: %v", err) + } + + if requested == nil { + t.Fatal("GetSharedLinkFile was not called") + } + if requested.Url != "https://example.com/link" { + t.Fatalf("url = %q, want https://example.com/link", requested.Url) + } + if requested.LinkPassword != "secret" { + t.Fatalf("password = %q, want secret", requested.LinkPassword) + } + assertFileContent(t, filepath.Join(tmp, "report.txt"), content) + if stdout.String() != "" { + t.Fatalf("stdout = %q, want empty", stdout.String()) + } + if got := stderr.String(); !strings.Contains(got, "Downloading ") { + t.Fatalf("stderr = %q, want progress", got) + } +} + +func TestShareLinkDownloadUsesExplicitTarget(t *testing.T) { + tmp := t.TempDir() + content := "shared content" + target := filepath.Join(tmp, "local.txt") + + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return downloadableSharedLinkFile("report.txt", "/docs/report.txt", "https://example.com/link", uint64(len(content))), + io.NopCloser(strings.NewReader(content)), nil + }, + }) + + if err := shareLinkDownload(newShareLinkDownloadTestCommand(nil, nil), []string{"https://example.com/link", target}); err != nil { + t.Fatalf("shareLinkDownload error: %v", err) + } + assertFileContent(t, target, content) +} + +func TestShareLinkDownloadUsesTargetDirectory(t *testing.T) { + tmp := t.TempDir() + targetDir := filepath.Join(tmp, "downloads") + if err := os.Mkdir(targetDir, 0755); err != nil { + t.Fatalf("mkdir: %v", err) + } + + content := "shared content" + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return downloadableSharedLinkFile("report.txt", "/docs/report.txt", "https://example.com/link", uint64(len(content))), + io.NopCloser(strings.NewReader(content)), nil + }, + }) + + if err := shareLinkDownload(newShareLinkDownloadTestCommand(nil, nil), []string{"https://example.com/link", targetDir}); err != nil { + t.Fatalf("shareLinkDownload error: %v", err) + } + assertFileContent(t, filepath.Join(targetDir, "report.txt"), content) +} + +func TestShareLinkDownloadToStdoutIsByteClean(t *testing.T) { + content := "shared stdout content" + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return downloadableSharedLinkFile("report.txt", "/docs/report.txt", "https://example.com/link", uint64(len(content))), + io.NopCloser(strings.NewReader(content)), nil + }, + }) + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd := newShareLinkDownloadTestCommand(&stdout, &stderr) + + if err := shareLinkDownload(cmd, []string{"https://example.com/link", "-"}); err != nil { + t.Fatalf("shareLinkDownload error: %v", err) + } + if stdout.String() != content { + t.Fatalf("stdout = %q, want file bytes", stdout.String()) + } + if stderr.String() != "" { + t.Fatalf("stderr = %q, want empty without verbose", stderr.String()) + } +} + +func TestShareLinkDownloadToStdoutDoesNotRetryAfterPartialOutput(t *testing.T) { + retryDelays := stubRetrySleep(t) + calls := 0 + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + calls++ + return downloadableSharedLinkFile("report.txt", "/docs/report.txt", "https://example.com/link", 100), + &failingReadCloser{data: []byte("partial")}, nil + }, + }) + + var stdout bytes.Buffer + err := shareLinkDownload(newShareLinkDownloadTestCommand(&stdout, nil), []string{"https://example.com/link", "-"}) + if err == nil { + t.Fatal("expected error for partial stdout failure") + } + if !strings.Contains(err.Error(), "cannot retry") { + t.Fatalf("error = %q, want cannot retry", err.Error()) + } + if calls != 1 { + t.Fatalf("GetSharedLinkFile calls = %d, want 1", calls) + } + if len(*retryDelays) != 0 { + t.Fatalf("retry delays = %v, want none", *retryDelays) + } + if stdout.String() != "partial" { + t.Fatalf("stdout = %q, want partial bytes", stdout.String()) + } +} + +func TestShareLinkDownloadToStdoutBrokenPipeReturnsNil(t *testing.T) { + mock := &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return downloadableSharedLinkFile("report.txt", "/docs/report.txt", "https://example.com/link", 100), + io.NopCloser(strings.NewReader("some data")), nil + }, + } + + err := downloadSharedLinkToStdout(mock, sharing.NewGetSharedLinkMetadataArg("https://example.com/link"), epipeWriter{}) + if err != nil { + t.Fatalf("downloadSharedLinkToStdout error: %v", err) + } +} + +func TestShareLinkDownloadReturnsAPIErrors(t *testing.T) { + wantErr := fmt.Errorf("shared_link_not_found") + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return nil, nil, wantErr + }, + }) + + err := shareLinkDownload(newShareLinkDownloadTestCommand(nil, nil), []string{"https://example.com/link", "-"}) + if err != wantErr { + t.Fatalf("error = %v, want original API error", err) + } +} + +func TestShareLinkDownloadRejectsMissingContent(t *testing.T) { + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return downloadableSharedLinkFile("report.txt", "/docs/report.txt", "https://example.com/link", 0), nil, nil + }, + }) + + err := shareLinkDownload(newShareLinkDownloadTestCommand(nil, nil), []string{"https://example.com/link", filepath.Join(t.TempDir(), "target")}) + if err == nil || !strings.Contains(err.Error(), "did not include file content") { + t.Fatalf("error = %v, want missing content error", err) + } +} + +func TestShareLinkDownloadToStdoutRejectsMissingContent(t *testing.T) { + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return downloadableSharedLinkFile("report.txt", "/docs/report.txt", "https://example.com/link", 0), nil, nil + }, + }) + + var stdout bytes.Buffer + err := shareLinkDownload(newShareLinkDownloadTestCommand(&stdout, nil), []string{"https://example.com/link", "-"}) + if err == nil || !strings.Contains(err.Error(), "did not include file content") { + t.Fatalf("error = %v, want missing content error", err) + } + if stdout.String() != "" { + t.Fatalf("stdout = %q, want empty", stdout.String()) + } +} + +func TestShareLinkDownloadRejectsNonFileMetadata(t *testing.T) { + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return sharedLinkFolder("/docs", "https://example.com/link"), io.NopCloser(strings.NewReader("content")), nil + }, + }) + + err := shareLinkDownload(newShareLinkDownloadTestCommand(nil, nil), []string{"https://example.com/link", filepath.Join(t.TempDir(), "target")}) + if err == nil || !strings.Contains(err.Error(), "not a downloadable file") { + t.Fatalf("error = %v, want non-file error", err) + } +} + +func TestShareLinkDownloadRejectsUnsafeMetadataName(t *testing.T) { + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return downloadableSharedLinkFile("..", "/docs/..", "https://example.com/link", 7), + io.NopCloser(strings.NewReader("content")), nil + }, + }) + + err := shareLinkDownload(newShareLinkDownloadTestCommand(nil, nil), []string{"https://example.com/link", t.TempDir()}) + if err == nil || !strings.Contains(err.Error(), "did not include a name") { + t.Fatalf("error = %v, want invalid name error", err) + } +} + +func TestShareLinkDownloadVerboseWritesStatusToStderr(t *testing.T) { + content := "shared stdout content" + stubSharedLinkClient(t, &mockSharedLinkClient{ + getSharedLinkFileFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return downloadableSharedLinkFile("report.txt", "/docs/report.txt", "https://example.com/link", uint64(len(content))), + io.NopCloser(strings.NewReader(content)), nil + }, + }) + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd := newShareLinkDownloadTestCommand(&stdout, &stderr) + if err := cmd.Flags().Set("verbose", "true"); err != nil { + t.Fatalf("set verbose: %v", err) + } + + if err := shareLinkDownload(cmd, []string{"https://example.com/link", "-"}); err != nil { + t.Fatalf("shareLinkDownload error: %v", err) + } + if stdout.String() != content { + t.Fatalf("stdout = %q, want file bytes", stdout.String()) + } + if got := stderr.String(); got != "Downloaded shared link to stdout\n" { + t.Fatalf("stderr = %q, want verbose status", got) + } +} + +func TestShareLinkDownloadCommandIsRegistered(t *testing.T) { + cmd, _, err := RootCmd.Find([]string{"share-link", "download", "https://example.com/link"}) + if err != nil { + t.Fatalf("find share-link download: %v", err) + } + if cmd != shareLinkDownloadCmd { + t.Fatalf("share-link download resolved to %q", cmd.CommandPath()) + } +} + +func newShareLinkDownloadTestCommand(stdout, stderr *bytes.Buffer) *cobra.Command { + cmd := &cobra.Command{} + cmd.Flags().String("password", "", "") + cmd.Flags().Bool("verbose", false, "") + if stdout != nil { + cmd.SetOut(stdout) + } + if stderr != nil { + cmd.SetErr(stderr) + } + return cmd +} + +func downloadableSharedLinkFile(name string, pathLower string, url string, size uint64) *sharing.FileLinkMetadata { + link := sharing.NewFileLinkMetadata(url, name, nil, time.Time{}, time.Time{}, "rev", size) + link.PathLower = strings.ToLower(pathLower) + return link +} + +func assertFileContent(t *testing.T, path string, want string) { + t.Helper() + + got, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + if string(got) != want { + t.Fatalf("%s = %q, want %q", path, string(got), want) + } +} diff --git a/cmd/share_link_info_test.go b/cmd/share_link_info_test.go index 1e7c807..27a0f4f 100644 --- a/cmd/share_link_info_test.go +++ b/cmd/share_link_info_test.go @@ -193,4 +193,12 @@ func TestShareLinkInfoDoesNotBreakOtherCommands(t *testing.T) { if cmd != shareLinkRevokeCmd { t.Fatalf("share-link revoke resolved to %q", cmd.CommandPath()) } + + cmd, _, err = RootCmd.Find([]string{"share-link", "download"}) + if err != nil { + t.Fatalf("find share-link download: %v", err) + } + if cmd != shareLinkDownloadCmd { + t.Fatalf("share-link download resolved to %q", cmd.CommandPath()) + } }