github.com/Files-com/files-sdk-go/v3@v3.1.81/file/downloader_test.go (about)

     1  package file
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/fs"
     8  	"os"
     9  	"path/filepath"
    10  	"sync"
    11  	"testing"
    12  	"testing/fstest"
    13  	"time"
    14  
    15  	files_sdk "github.com/Files-com/files-sdk-go/v3"
    16  	"github.com/Files-com/files-sdk-go/v3/file/manager"
    17  	"github.com/Files-com/files-sdk-go/v3/file/status"
    18  	"github.com/Files-com/files-sdk-go/v3/lib"
    19  	"github.com/samber/lo"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  type ReporterCall struct {
    25  	JobFile
    26  	err error
    27  }
    28  
    29  type TestSetup struct {
    30  	files         []Entity
    31  	reporterCalls []ReporterCall
    32  	fstest.MapFS
    33  	DownloaderParams
    34  	rootDestination string
    35  	tempDir         string
    36  	files_sdk.Config
    37  }
    38  
    39  func NewTestSetup() *TestSetup {
    40  	t := &TestSetup{Config: files_sdk.Config{}.Init()}
    41  	t.MapFS = make(fstest.MapFS)
    42  	err := t.TempDir()
    43  	if err != nil {
    44  		panic(err)
    45  	}
    46  	return t
    47  }
    48  
    49  func (setup *TestSetup) Reporter() EventsReporter {
    50  	m := sync.Mutex{}
    51  
    52  	callback := func(status JobFile) {
    53  		m.Lock()
    54  		setup.reporterCalls = append(setup.reporterCalls, ReporterCall{JobFile: status})
    55  		m.Unlock()
    56  	}
    57  
    58  	return CreateFileEvents(callback, append(status.Excluded, status.Included...)...)
    59  }
    60  
    61  func (setup *TestSetup) TempDir() error {
    62  	var err error
    63  	setup.tempDir, err = os.MkdirTemp("", "test")
    64  
    65  	return err
    66  }
    67  
    68  func (setup *TestSetup) TearDown() error {
    69  	return os.RemoveAll(setup.tempDir)
    70  }
    71  
    72  func (setup *TestSetup) Call() *Job {
    73  	setup.DownloaderParams.config = setup.Config
    74  	job := downloader(
    75  		context.Background(),
    76  		setup.MapFS,
    77  		setup.DownloaderParams,
    78  	)
    79  
    80  	job.Start()
    81  	job.Wait()
    82  	return job
    83  }
    84  
    85  func (setup *TestSetup) RootDestination() string {
    86  	if setup.rootDestination != "" && setup.rootDestination[len(setup.rootDestination)-1:] == string(os.PathSeparator) {
    87  		return filepath.Join(setup.tempDir, setup.rootDestination) + string(os.PathSeparator)
    88  	}
    89  
    90  	return filepath.Join(setup.tempDir, setup.rootDestination)
    91  }
    92  
    93  func Test_downloadFolder_ending_in_slash(t *testing.T) {
    94  	setup := NewTestSetup()
    95  	setup.MapFS["some-path"] = &fstest.MapFile{
    96  		Data:    nil,
    97  		Mode:    fs.ModeDir,
    98  		ModTime: time.Time{},
    99  		Sys:     files_sdk.File{DisplayName: "some-path", Path: "some-path", Type: "directory"},
   100  	}
   101  
   102  	setup.MapFS["some-path/taco.png"] = &fstest.MapFile{
   103  		Data:    make([]byte, 100),
   104  		Mode:    fs.ModePerm,
   105  		ModTime: time.Time{},
   106  		Sys:     files_sdk.File{DisplayName: "taco.png", Path: "some-path/taco.png", Type: "file", Size: 100},
   107  	}
   108  
   109  	setup.DownloaderParams = DownloaderParams{RemotePath: "some-path", EventsReporter: setup.Reporter(), LocalPath: setup.RootDestination()}
   110  	setup.rootDestination = "some-path/"
   111  	setup.Call()
   112  
   113  	assert.Equal(t, 1, setup.reporterCalls[0].Job.Count())
   114  	assert.Equal(t, 3, len(setup.reporterCalls))
   115  	assert.Equal(t, status.Queued, setup.reporterCalls[0].Status)
   116  	assert.Equal(t, status.Downloading, setup.reporterCalls[1].Status)
   117  	assert.Equal(t, status.Complete, setup.reporterCalls[2].Status)
   118  	assert.NoError(t, setup.reporterCalls[2].err)
   119  	assert.Equal(t, "some-path/taco.png", setup.reporterCalls[0].File.Path)
   120  	assert.Equal(t, int64(0), setup.reporterCalls[0].TransferBytes)
   121  
   122  	assert.Equal(t, true, setup.reporterCalls[0].Job.All(status.Ended...))
   123  	assert.Equal(t, int64(100), setup.reporterCalls[0].Job.TransferBytes())
   124  	assert.Equal(t, int64(100), setup.reporterCalls[0].Job.TotalBytes())
   125  
   126  	assert.NoError(t, setup.TearDown())
   127  }
   128  
   129  func Test_downloader_RemoteStartingSlash(t *testing.T) {
   130  	setup := NewTestSetup()
   131  	setup.MapFS["some-path"] = &fstest.MapFile{
   132  		Data:    nil,
   133  		Mode:    fs.ModeDir,
   134  		ModTime: time.Time{},
   135  		Sys:     files_sdk.File{DisplayName: "some-path", Path: "some-path", Type: "directory"},
   136  	}
   137  
   138  	setup.MapFS["some-path/taco.png"] = &fstest.MapFile{
   139  		Data:    make([]byte, 100),
   140  		Mode:    fs.ModePerm,
   141  		ModTime: time.Time{},
   142  		Sys:     files_sdk.File{DisplayName: "taco.png", Path: "some-path/taco.png", Type: "file", Size: 100},
   143  	}
   144  
   145  	setup.DownloaderParams = DownloaderParams{RemotePath: "some-path", EventsReporter: setup.Reporter(), LocalPath: setup.RootDestination()}
   146  	setup.rootDestination = "some-path" + string(os.PathSeparator)
   147  	setup.Call()
   148  
   149  	fi, ok := setup.reporterCalls[0].Find(status.Errored)
   150  	if ok {
   151  		require.NoError(t, fi.Err())
   152  	}
   153  	assert.Equal(t, 1, setup.reporterCalls[0].Job.Count())
   154  	assert.Equal(t, 3, len(setup.reporterCalls))
   155  	assert.Equal(t, status.Queued, setup.reporterCalls[0].Status)
   156  	assert.Equal(t, status.Downloading, setup.reporterCalls[1].Status)
   157  	assert.Equal(t, status.Complete, setup.reporterCalls[2].Status)
   158  	assert.NoError(t, setup.reporterCalls[2].err)
   159  	assert.Equal(t, "some-path/taco.png", setup.reporterCalls[0].File.Path)
   160  	assert.Equal(t, int64(0), setup.reporterCalls[0].TransferBytes)
   161  
   162  	assert.Equal(t, true, setup.reporterCalls[0].Job.All(status.Ended...))
   163  	assert.Equal(t, int64(100), setup.reporterCalls[0].Job.TransferBytes())
   164  	assert.Equal(t, int64(100), setup.reporterCalls[0].Job.TotalBytes())
   165  
   166  	assert.NoError(t, setup.TearDown())
   167  }
   168  
   169  func TestClient_Downloader(t *testing.T) {
   170  	t.Run("small file with size", func(t *testing.T) {
   171  		root := t.TempDir()
   172  		server := (&MockAPIServer{T: t}).Do()
   173  		defer server.Shutdown()
   174  		client := server.Client()
   175  		server.MockFiles["small-file-with-size.txt"] = mockFile{
   176  			SizeTrust: TrustedSizeValue,
   177  			File:      files_sdk.File{Size: 1999},
   178  		}
   179  		job := client.Downloader(DownloaderParams{RemotePath: "small-file-with-size.txt", LocalPath: root + "/"})
   180  		job.Start()
   181  		job.Wait()
   182  		assert.Len(t, job.Statuses, 1)
   183  		require.NoError(t, job.Statuses[0].Err())
   184  		f, err := os.Open(filepath.Join(root, "small-file-with-size.txt"))
   185  		require.NoError(t, err)
   186  		stat, err := f.Stat()
   187  		require.NoError(t, err)
   188  		assert.Equal(t, int64(1999), stat.Size())
   189  	})
   190  
   191  	t.Run("large file with size", func(t *testing.T) {
   192  		root := t.TempDir()
   193  		server := (&MockAPIServer{T: t}).Do()
   194  		defer server.Shutdown()
   195  		client := server.Client()
   196  		server.MockFiles["large-file-with-size.txt"] = mockFile{
   197  			SizeTrust: TrustedSizeValue,
   198  			File:      files_sdk.File{Size: 19999999},
   199  		}
   200  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-size.txt", LocalPath: root + "/"})
   201  		job.Start()
   202  		job.Wait()
   203  		assert.Len(t, job.Statuses, 1)
   204  		require.NoError(t, job.Statuses[0].Err())
   205  		f, err := os.Open(filepath.Join(root, "large-file-with-size.txt"))
   206  		require.NoError(t, err)
   207  		stat, err := f.Stat()
   208  		require.NoError(t, err)
   209  		assert.Equal(t, int64(19999999), stat.Size())
   210  	})
   211  
   212  	t.Run("large file with size with max concurrent connections of 1", func(t *testing.T) {
   213  		root := t.TempDir()
   214  		server := (&MockAPIServer{T: t}).Do()
   215  		defer server.Shutdown()
   216  		client := server.Client()
   217  		server.MockFiles["large-file-with-size.txt"] = mockFile{
   218  			SizeTrust:      TrustedSizeValue,
   219  			File:           files_sdk.File{Size: 1024 * 1024 * 100},
   220  			MaxConnections: 1,
   221  		}
   222  		m := manager.Build(1, 1)
   223  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-size.txt", LocalPath: root + "/", Manager: m})
   224  		job.Start()
   225  		job.Wait()
   226  		assert.Len(t, job.Statuses, 1)
   227  		require.NoError(t, job.Statuses[0].Err())
   228  		f, err := os.Open(filepath.Join(root, "large-file-with-size.txt"))
   229  		require.NoError(t, err)
   230  		stat, err := f.Stat()
   231  		require.NoError(t, err)
   232  		assert.Equal(t, int64(1024*1024*100), stat.Size())
   233  		assert.Len(t, server.TrackRequest["/download/:download_id"], 1)
   234  	})
   235  
   236  	t.Run("large file with size with max concurrent connections of 1", func(t *testing.T) {
   237  		root := t.TempDir()
   238  		server := (&MockAPIServer{T: t}).Do()
   239  		defer server.Shutdown()
   240  		client := server.Client()
   241  		server.MockFiles["large-file-with-size.txt"] = mockFile{
   242  			SizeTrust:      TrustedSizeValue,
   243  			File:           files_sdk.File{Size: 1024 * 1024 * 50},
   244  			MaxConnections: 1,
   245  		}
   246  		m := manager.Build(1, 1)
   247  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-size.txt", LocalPath: root + "/", Manager: m})
   248  		job.Start()
   249  		job.Wait()
   250  		assert.Len(t, job.Statuses, 1)
   251  		require.NoError(t, job.Statuses[0].Err())
   252  		f, err := os.Open(filepath.Join(root, "large-file-with-size.txt"))
   253  		require.NoError(t, err)
   254  		stat, err := f.Stat()
   255  		require.NoError(t, err)
   256  		assert.Equal(t, int64(1024*1024*50), stat.Size())
   257  		assert.Len(t, server.TrackRequest["/download/:download_id"], 1)
   258  	})
   259  
   260  	t.Run("large file with size DownloadFilesAsSingleStream", func(t *testing.T) {
   261  		root := t.TempDir()
   262  		server := (&MockAPIServer{T: t}).Do()
   263  		defer server.Shutdown()
   264  		client := server.Client()
   265  		server.MockFiles["large-file-with-size.txt"] = mockFile{
   266  			SizeTrust: TrustedSizeValue,
   267  			File:      files_sdk.File{Size: 1024 * 1024 * 50},
   268  		}
   269  		m := manager.Build(10, 1, true)
   270  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-size.txt", LocalPath: root + "/", Manager: m})
   271  		job.Start()
   272  		job.Wait()
   273  		assert.Len(t, job.Statuses, 1)
   274  		require.NoError(t, job.Statuses[0].Err())
   275  		f, err := os.Open(filepath.Join(root, "large-file-with-size.txt"))
   276  		require.NoError(t, err)
   277  		stat, err := f.Stat()
   278  		require.NoError(t, err)
   279  		assert.Equal(t, int64(1024*1024*50), stat.Size())
   280  		assert.Len(t, server.TrackRequest["/download/:download_id"], 1)
   281  	})
   282  
   283  	t.Run("large file with no size", func(t *testing.T) {
   284  		root := t.TempDir()
   285  		server := (&MockAPIServer{T: t}).Do()
   286  		defer server.Shutdown()
   287  		client := server.Client()
   288  		server.MockFiles["large-file-with-no-size.txt"] = mockFile{
   289  			SizeTrust: UntrustedSizeValue,
   290  			File:      files_sdk.File{Size: 19999999},
   291  		}
   292  
   293  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-no-size.txt", LocalPath: root + "/"})
   294  		job.Start()
   295  		job.Wait()
   296  		assert.Len(t, job.Statuses, 1)
   297  		require.NoError(t, job.Statuses[0].Err())
   298  		f, err := os.Open(filepath.Join(root, "large-file-with-no-size.txt"))
   299  		require.NoError(t, err)
   300  		stat, err := f.Stat()
   301  		require.NoError(t, err)
   302  		assert.Equal(t, int64(19999999), stat.Size())
   303  	})
   304  
   305  	t.Run("large file with no size - extra parts are canceled", func(t *testing.T) {
   306  		root := t.TempDir()
   307  		server := (&MockAPIServer{T: t}).Do()
   308  		defer server.Shutdown()
   309  		client := server.Client()
   310  		realSize := int64((1024 * 1024 * 5) - 256)
   311  		server.MockFiles["large-file-with-no-size.txt"] = mockFile{
   312  			SizeTrust: UntrustedSizeValue,
   313  			File:      files_sdk.File{Size: 1024 * 1024 * 100},
   314  			RealSize:  &realSize,
   315  		}
   316  
   317  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-no-size.txt", LocalPath: root + "/"})
   318  		job.Start()
   319  		job.Wait()
   320  		assert.Len(t, job.Statuses, 1)
   321  		require.NoError(t, job.Statuses[0].Err())
   322  		f, err := os.Open(filepath.Join(root, "large-file-with-no-size.txt"))
   323  		require.NoError(t, err)
   324  		stat, err := f.Stat()
   325  		require.NoError(t, err)
   326  		assert.Equal(t, realSize, stat.Size())
   327  	})
   328  
   329  	t.Run("large file with no size - client does not receive all bytes server reported to send", func(t *testing.T) {
   330  		root := t.TempDir()
   331  		server := (&MockAPIServer{T: t}).Do()
   332  		defer server.Shutdown()
   333  		client := server.Client()
   334  		serverBytesSent := int64((1024 * 1024 * 5) + 256)
   335  		server.MockFiles["large-file-with-no-size.txt"] = mockFile{
   336  			SizeTrust:       UntrustedSizeValue,
   337  			File:            files_sdk.File{Size: 1024 * 1024 * 15},
   338  			ServerBytesSent: &serverBytesSent,
   339  		}
   340  
   341  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-no-size.txt", LocalPath: root + "/"})
   342  		job.Start()
   343  		job.Wait()
   344  		assert.Len(t, job.Statuses, 1)
   345  		require.EqualError(t, job.Statuses[0].Err(), `received size did not match server send size
   346  expected 5243136 bytes sent 5242880 received`)
   347  		_, err := os.Open(filepath.Join(root, "large-file-with-no-size.txt"))
   348  		require.Error(t, err)
   349  	})
   350  
   351  	t.Run("large file with no size - client received more bytes than server reported to send", func(t *testing.T) {
   352  		root := t.TempDir()
   353  		server := (&MockAPIServer{T: t}).Do()
   354  		defer server.Shutdown()
   355  		client := server.Client()
   356  		serverBytesSent := int64(1024 * 1024 * 4)
   357  		server.MockFiles["large-file-with-no-size.txt"] = mockFile{
   358  			SizeTrust:       UntrustedSizeValue,
   359  			File:            files_sdk.File{Size: 1024 * 1024 * 15},
   360  			ServerBytesSent: &serverBytesSent,
   361  		}
   362  
   363  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-no-size.txt", LocalPath: root + "/"})
   364  		job.Start()
   365  		job.Wait()
   366  		assert.Len(t, job.Statuses, 1)
   367  		require.EqualError(t, job.Statuses[0].Err(), `received size did not match server send size
   368  expected 4194304 bytes sent 5242880 received`)
   369  		_, err := os.Open(filepath.Join(root, "large-file-with-no-size.txt"))
   370  		require.Error(t, err)
   371  	})
   372  
   373  	t.Run("large file with no size - when sever has invalid request status", func(t *testing.T) {
   374  		root := t.TempDir()
   375  		server := (&MockAPIServer{T: t}).Do()
   376  		defer server.Shutdown()
   377  		client := server.Client()
   378  		serverBytesSent := int64(1024 * 1024 * 4)
   379  		server.MockFiles["large-file-with-no-size.txt"] = mockFile{
   380  			SizeTrust:          UntrustedSizeValue,
   381  			File:               files_sdk.File{Size: 1024 * 1024 * 15},
   382  			ServerBytesSent:    &serverBytesSent,
   383  			ForceRequestStatus: "started",
   384  		}
   385  
   386  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-no-size.txt", LocalPath: root + "/"})
   387  		job.Start()
   388  		job.Wait()
   389  		assert.Len(t, job.Statuses, 1)
   390  		require.NoError(t, job.Statuses[0].Err())
   391  		f, err := os.Open(filepath.Join(root, "large-file-with-no-size.txt"))
   392  		require.NoError(t, err)
   393  		stat, err := f.Stat()
   394  		require.NoError(t, err)
   395  		assert.Equal(t, int64(1024*1024*15), stat.Size())
   396  	})
   397  
   398  	t.Run("large file with no size - when sever has failed request status", func(t *testing.T) {
   399  		root := t.TempDir()
   400  		server := (&MockAPIServer{T: t}).Do()
   401  		defer server.Shutdown()
   402  		client := server.Client()
   403  		server.MockFiles["large-file-with-no-size.txt"] = mockFile{
   404  			SizeTrust:           UntrustedSizeValue,
   405  			File:                files_sdk.File{Size: 1024 * 1024 * 15},
   406  			ForceRequestStatus:  "failed",
   407  			ForceRequestMessage: "problem",
   408  		}
   409  		var events []JobFile
   410  		eventReporter := CreateFileEvents(
   411  			func(file JobFile) {
   412  				events = append(events, file)
   413  			},
   414  			status.Included...,
   415  		)
   416  
   417  		job := client.Downloader(DownloaderParams{RemotePath: "large-file-with-no-size.txt", LocalPath: root + "/", EventsReporter: eventReporter})
   418  		transferBytes := []string{"zero"}
   419  		wait := make(chan bool)
   420  		go func() {
   421  			for {
   422  				select {
   423  				case <-job.Finished.C:
   424  					wait <- true
   425  					return
   426  				default:
   427  					bytes := job.TransferBytes()
   428  					if bytes > 0 && transferBytes[len(transferBytes)-1] == "zero" {
   429  						transferBytes = append(transferBytes, "bytes")
   430  					}
   431  					if bytes == 0 && transferBytes[len(transferBytes)-1] != "zero" {
   432  						transferBytes = append(transferBytes, "zero")
   433  					}
   434  				}
   435  			}
   436  		}()
   437  
   438  		job.Start()
   439  		job.Wait()
   440  		assert.Len(t, job.Statuses, 1)
   441  		assert.Error(t, job.Statuses[0].Err(), `received size did not match server send size
   442  expected 4194304 bytes sent 5242880 received`)
   443  		assert.Equal(t, []int64{0, 32768, 0}, lo.Map[JobFile, int64](events, func(item JobFile, index int) int64 { return item.TransferBytes }))
   444  		assert.Equal(t, []string{"queued", "downloading", "errored"}, lo.Map[JobFile, string](events, func(item JobFile, index int) string { return item.StatusName }))
   445  		<-wait
   446  		assert.GreaterOrEqual(t, lo.Count[string](transferBytes, "zero"), 2, "After error transfer bytes are set to zero")
   447  		assert.GreaterOrEqual(t, lo.Count[string](transferBytes, "bytes"), 2, "After error transfer bytes are set to zero")
   448  	})
   449  
   450  	t.Run("large file with bad size info real size is bigger", func(t *testing.T) {
   451  		root := t.TempDir()
   452  		server := (&MockAPIServer{T: t}).Do()
   453  		defer server.Shutdown()
   454  		client := server.Client()
   455  		realSize := int64(20000000)
   456  		server.MockFiles["file-with-mismatch-size-bigger"] = mockFile{
   457  			SizeTrust: UntrustedSizeValue,
   458  			File:      files_sdk.File{Size: 19999999},
   459  			RealSize:  &realSize,
   460  		}
   461  
   462  		job := client.Downloader(DownloaderParams{RemotePath: "file-with-mismatch-size-bigger", LocalPath: root + "/"})
   463  		job.Start()
   464  		job.Wait()
   465  		require.Len(t, job.Statuses, 1)
   466  		require.NoError(t, job.Statuses[0].Err())
   467  		f, err := os.Open(filepath.Join(root, "file-with-mismatch-size-bigger"))
   468  		require.NoError(t, err)
   469  		stat, err := f.Stat()
   470  		require.NoError(t, err)
   471  		assert.Equal(t, int64(20000000), stat.Size())
   472  	})
   473  
   474  	t.Run("large file with bad size info real size is smaller", func(t *testing.T) {
   475  		root := t.TempDir()
   476  		server := (&MockAPIServer{T: t}).Do()
   477  		defer server.Shutdown()
   478  		client := server.Client()
   479  		realSize := int64(19999999)
   480  		server.MockFiles["file-with-mismatch-size-smaller"] = mockFile{
   481  			SizeTrust: UntrustedSizeValue,
   482  			File:      files_sdk.File{Size: 20000000},
   483  			RealSize:  &realSize,
   484  		}
   485  
   486  		job := client.Downloader(DownloaderParams{RemotePath: "file-with-mismatch-size-smaller", LocalPath: root + "/"})
   487  		job.Start()
   488  		job.Wait()
   489  		require.Len(t, job.Statuses, 1)
   490  		require.NoError(t, job.Statuses[0].Err())
   491  		f, err := os.Open(filepath.Join(root, "file-with-mismatch-size-smaller"))
   492  		require.NoError(t, err)
   493  		stat, err := f.Stat()
   494  		require.NoError(t, err)
   495  		assert.Equal(t, int64(19999999), stat.Size())
   496  	})
   497  
   498  	multipleFiles := func(relativeRoot string, t *testing.T) {
   499  		root := t.TempDir()
   500  		server := (&MockAPIServer{T: t}).Do()
   501  		defer server.Shutdown()
   502  		client := server.Client()
   503  		server.MockFiles[filepath.Join(relativeRoot, "file1")] = mockFile{
   504  			SizeTrust: TrustedSizeValue,
   505  			File:      files_sdk.File{Size: 6},
   506  		}
   507  		server.MockFiles[filepath.Join(relativeRoot, "file2")] = mockFile{
   508  			SizeTrust: TrustedSizeValue,
   509  			File:      files_sdk.File{Size: 1024 * 1024},
   510  		}
   511  		server.MockFiles[filepath.Join(relativeRoot, "file3")] = mockFile{
   512  			SizeTrust: TrustedSizeValue,
   513  			File:      files_sdk.File{Size: 1024 * 1024 * 2},
   514  		}
   515  		server.MockFiles[filepath.Join(relativeRoot, "file4")] = mockFile{
   516  			SizeTrust: TrustedSizeValue,
   517  			File:      files_sdk.File{Size: 1024 * 1024 * 10},
   518  		}
   519  		server.MockFiles[filepath.Join(relativeRoot, "file5")] = mockFile{
   520  			SizeTrust: TrustedSizeValue,
   521  			File:      files_sdk.File{Size: 100},
   522  		}
   523  		if relativeRoot != "" {
   524  			server.MockFiles[relativeRoot] = mockFile{
   525  				File: files_sdk.File{Type: "directory"},
   526  			}
   527  		}
   528  
   529  		job := client.Downloader(DownloaderParams{RemotePath: relativeRoot, LocalPath: root + "/"})
   530  		job.Start()
   531  		job.Wait()
   532  		assert.Len(t, job.Statuses, 5)
   533  		require.NoError(t, job.Statuses[0].Err())
   534  
   535  		for k, v := range server.MockFiles {
   536  			f, err := os.Open(filepath.Join(root, k))
   537  			require.NoError(t, err)
   538  			stat, err := f.Stat()
   539  			require.NoError(t, err)
   540  			if !stat.IsDir() {
   541  				assert.Equal(t, v.Size, stat.Size())
   542  			}
   543  		}
   544  	}
   545  
   546  	t.Run("list folder from a path", func(t *testing.T) {
   547  		multipleFiles("a-root", t)
   548  	})
   549  
   550  	t.Run("multiple files from root", func(t *testing.T) {
   551  		multipleFiles("", t)
   552  	})
   553  
   554  	t.Run("PreserveTimes with mtime", func(t *testing.T) {
   555  		root := t.TempDir()
   556  		server := (&MockAPIServer{T: t}).Do()
   557  		defer server.Shutdown()
   558  		client := server.Client()
   559  		mtime := time.Date(2010, 11, 17, 20, 34, 58, 651387237, time.UTC).Truncate(time.Millisecond)
   560  		server.MockFiles["small-file-with-size.txt"] = mockFile{
   561  			SizeTrust: TrustedSizeValue,
   562  			File:      files_sdk.File{Size: 1999, Mtime: &mtime},
   563  		}
   564  		job := client.Downloader(DownloaderParams{RemotePath: "small-file-with-size.txt", LocalPath: root + "/", PreserveTimes: true})
   565  		job.Start()
   566  		job.Wait()
   567  		assert.Len(t, job.Statuses, 1)
   568  		require.NoError(t, job.Statuses[0].Err())
   569  		f, err := os.Open(filepath.Join(root, "small-file-with-size.txt"))
   570  		require.NoError(t, err)
   571  		stat, err := f.Stat()
   572  		require.NoError(t, err)
   573  		assert.Equal(t, int64(1999), stat.Size())
   574  		assert.Equal(t, mtime, stat.ModTime().UTC())
   575  	})
   576  
   577  	t.Run("PreserveTimes with providedMtime", func(t *testing.T) {
   578  		root := t.TempDir()
   579  		server := (&MockAPIServer{T: t}).Do()
   580  		defer server.Shutdown()
   581  		client := server.Client()
   582  		providedMtime := time.Date(2010, 11, 17, 20, 34, 58, 651387237, time.UTC).Truncate(time.Millisecond)
   583  		server.MockFiles["small-file-with-size.txt"] = mockFile{
   584  			SizeTrust: TrustedSizeValue,
   585  			File:      files_sdk.File{Size: 1999, Mtime: lib.Time(time.Now()), ProvidedMtime: &providedMtime},
   586  		}
   587  		job := client.Downloader(DownloaderParams{RemotePath: "small-file-with-size.txt", LocalPath: root + "/", PreserveTimes: true})
   588  		job.Start()
   589  		job.Wait()
   590  		assert.Len(t, job.Statuses, 1)
   591  		require.NoError(t, job.Statuses[0].Err())
   592  		f, err := os.Open(filepath.Join(root, "small-file-with-size.txt"))
   593  		require.NoError(t, err)
   594  		stat, err := f.Stat()
   595  		require.NoError(t, err)
   596  		assert.Equal(t, int64(1999), stat.Size())
   597  		assert.Equal(t, providedMtime, stat.ModTime().UTC())
   598  	})
   599  
   600  	t.Run("sync already downloaded", func(t *testing.T) {
   601  		root := t.TempDir()
   602  		server := (&MockAPIServer{T: t}).Do()
   603  		defer server.Shutdown()
   604  		client := server.Client()
   605  		server.MockFiles["taco.png"] = mockFile{
   606  			SizeTrust: TrustedSizeValue,
   607  			File:      files_sdk.File{Size: 100},
   608  		}
   609  		taco, err := os.Create(filepath.Join(root, "taco.png"))
   610  		assert.NoError(t, err)
   611  		_, err = taco.Write(make([]byte, 100))
   612  		require.NoError(t, err)
   613  		require.NoError(t, taco.Close())
   614  		job := client.Downloader(DownloaderParams{Sync: true, RemotePath: "taco.png", LocalPath: root + "/"})
   615  		job.Start()
   616  		job.Wait()
   617  		assert.Len(t, job.Statuses, 1)
   618  		require.NoError(t, job.Statuses[0].Err())
   619  		assert.Equal(t, status.Skipped, job.Statuses[0].Status())
   620  	})
   621  
   622  	t.Run("sync does not exist locally", func(t *testing.T) {
   623  		root := t.TempDir()
   624  		server := (&MockAPIServer{T: t}).Do()
   625  		defer server.Shutdown()
   626  		client := server.Client()
   627  		server.MockFiles["taco.png"] = mockFile{
   628  			SizeTrust: TrustedSizeValue,
   629  			File:      files_sdk.File{Size: 100},
   630  		}
   631  		job := client.Downloader(DownloaderParams{Sync: true, RemotePath: "taco.png", LocalPath: root + "/"})
   632  		job.Start()
   633  		job.Wait()
   634  		assert.Len(t, job.Statuses, 1)
   635  		require.NoError(t, job.Statuses[0].Err())
   636  		assert.Equal(t, status.Complete, job.Statuses[0].Status())
   637  	})
   638  
   639  	t.Run("sync is out of date locally by size", func(t *testing.T) {
   640  		root := t.TempDir()
   641  		server := (&MockAPIServer{T: t}).Do()
   642  		defer server.Shutdown()
   643  		client := server.Client()
   644  		server.MockFiles["taco.png"] = mockFile{
   645  			SizeTrust: TrustedSizeValue,
   646  			File:      files_sdk.File{Size: 100},
   647  		}
   648  		taco, err := os.Create(filepath.Join(root, "taco.png"))
   649  		assert.NoError(t, err)
   650  		require.NoError(t, taco.Close())
   651  		job := client.Downloader(DownloaderParams{Sync: true, RemotePath: "taco.png", LocalPath: root + "/"})
   652  		job.Start()
   653  		job.Wait()
   654  		assert.Len(t, job.Statuses, 1)
   655  		require.NoError(t, job.Statuses[0].Err())
   656  		assert.Equal(t, status.Complete, job.Statuses[0].Status())
   657  	})
   658  
   659  	t.Run("local directory is privileged", func(t *testing.T) {
   660  		root := t.TempDir()
   661  		server := (&MockAPIServer{T: t}).Do()
   662  		defer server.Shutdown()
   663  		client := server.Client()
   664  		server.MockFiles["taco.png"] = mockFile{
   665  			SizeTrust: TrustedSizeValue,
   666  			File:      files_sdk.File{Size: 100},
   667  		}
   668  
   669  		require.NoError(t, os.Mkdir(filepath.Join(root, "restricted"), 0000))
   670  
   671  		t.Cleanup(func() {
   672  			require.NoError(t, os.Chmod(filepath.Join(root, "restricted"), 0777))
   673  		})
   674  
   675  		job := client.Downloader(DownloaderParams{Sync: true, RemotePath: "taco.png", LocalPath: filepath.Join(root, "restricted") + string(os.PathSeparator)})
   676  		job.Start()
   677  		job.Wait()
   678  		assert.Len(t, job.Statuses, 1)
   679  		require.True(t, os.IsPermission(job.Statuses[0].Err()))
   680  		assert.Equal(t, status.Errored, job.Statuses[0].Status())
   681  	})
   682  
   683  	t.Run("local path is invalid", func(t *testing.T) {
   684  		server := (&MockAPIServer{T: t}).Do()
   685  		defer server.Shutdown()
   686  		client := server.Client()
   687  		server.MockFiles["taco.png"] = mockFile{
   688  			SizeTrust: TrustedSizeValue,
   689  			File:      files_sdk.File{Size: 100},
   690  		}
   691  
   692  		job := client.Downloader(DownloaderParams{Sync: true, RemotePath: "taco.png", LocalPath: "invalid\000path"})
   693  		job.Start()
   694  		job.Wait()
   695  		assert.Len(t, job.Statuses, 1)
   696  		require.Error(t, job.Statuses[0].Err())
   697  		require.Contains(t, job.Statuses[0].Err().Error(), "invalid argument")
   698  		assert.Equal(t, status.Errored, job.Statuses[0].Status())
   699  	})
   700  }
   701  
   702  func TestDownload(t *testing.T) {
   703  	mutex := &sync.Mutex{}
   704  	t.Run("downloader", func(t *testing.T) {
   705  		sourceFs := &FS{Context: context.Background()}
   706  		destinationFs := lib.ReadWriteFs(lib.LocalFileSystem{})
   707  		for _, tt := range lib.PathSpec(sourceFs.PathSeparator(), destinationFs.PathSeparator()) {
   708  			t.Run(tt.Name, func(t *testing.T) {
   709  				client, r, err := CreateClient(t.Name())
   710  				if err != nil {
   711  					t.Fatal(err)
   712  				}
   713  				config := client.Config
   714  				sourceFs := (&FS{Context: context.Background()}).Init(config, false)
   715  				lib.BuildPathSpecTest(t, mutex, tt, sourceFs, destinationFs, func(args lib.PathSpecArgs) lib.Cmd {
   716  					return &CmdRunner{
   717  						run: func() *Job {
   718  							return downloader(context.Background(), sourceFs, DownloaderParams{config: config, RemotePath: args.Src, LocalPath: args.Dest, PreserveTimes: args.PreserveTimes})
   719  						},
   720  						args: []string{args.Src, args.Dest, "--times", fmt.Sprintf("%v", args.PreserveTimes)},
   721  					}
   722  				})
   723  				r.Stop()
   724  			})
   725  		}
   726  	})
   727  }
   728  
   729  type CmdRunner struct {
   730  	run    func() *Job
   731  	stderr io.Writer
   732  	stdout io.Writer
   733  	args   []string
   734  	*Job
   735  }
   736  
   737  func (c *CmdRunner) Run() error {
   738  	c.Job = c.run()
   739  	c.Job.Start()
   740  	c.Job.Wait()
   741  	for _, f := range c.Job.Sub(status.Errored).Statuses {
   742  		c.stderr.Write([]byte(f.Err().Error()))
   743  	}
   744  	return nil
   745  }
   746  
   747  func (c *CmdRunner) Args() []string {
   748  	return c.args
   749  }
   750  
   751  func (c *CmdRunner) SetOut(w io.Writer) {
   752  	c.stdout = w
   753  }
   754  
   755  func (c *CmdRunner) SetErr(stderr io.Writer) {
   756  	c.stderr = stderr
   757  }