diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f148d36..98f02b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Avoid type assert and type convert where possible. (https://github.com/C2FO/vfs/pull/303) - Remove unnecessary S3 `mockClient` type. (https://github.com/C2FO/vfs/pull/305) +### Added +- Ability to optionally specify context when creating file and location objects. ## [v7.12.0] - 2025-01-24 ### Security diff --git a/backend/azure/client.go b/backend/azure/client.go index 15fbe3dc..6367dfd5 100644 --- a/backend/azure/client.go +++ b/backend/azure/client.go @@ -66,7 +66,7 @@ func (a *DefaultClient) newContainerClient(containerURL string) (*container.Clie } // Properties fetches the properties for the blob specified by the parameters containerURI and filePath -func (a *DefaultClient) Properties(containerURI, filePath string) (*BlobProperties, error) { +func (a *DefaultClient) Properties(ctx context.Context, containerURI, filePath string) (*BlobProperties, error) { cli, err := a.newContainerClient(containerURI) if err != nil { return nil, err @@ -75,7 +75,7 @@ func (a *DefaultClient) Properties(containerURI, filePath string) (*BlobProperti if filePath == "" { // this is only used to check for the existence of a container so we don't care about anything but the // error - _, err := cli.GetProperties(context.Background(), nil) + _, err := cli.GetProperties(ctx, nil) if err != nil { return nil, err } @@ -83,7 +83,7 @@ func (a *DefaultClient) Properties(containerURI, filePath string) (*BlobProperti } blobURL := cli.NewBlockBlobClient(utils.RemoveLeadingSlash(filePath)) - resp, err := blobURL.GetProperties(context.Background(), nil) + resp, err := blobURL.GetProperties(ctx, nil) if err != nil { return nil, err } @@ -91,7 +91,7 @@ func (a *DefaultClient) Properties(containerURI, filePath string) (*BlobProperti } // Upload uploads a new file to Azure Blob Storage -func (a *DefaultClient) Upload(file vfs.File, content io.ReadSeeker, contentType string) error { +func (a *DefaultClient) Upload(ctx context.Context, file vfs.File, content io.ReadSeeker, contentType string) error { cli, err := a.newContainerClient(file.Location().Authority().String()) if err != nil { return err @@ -107,29 +107,29 @@ func (a *DefaultClient) Upload(file vfs.File, content io.ReadSeeker, contentType HTTPHeaders: &blob.HTTPHeaders{BlobContentType: &contentType}, } } - _, err = blobURL.Upload(context.Background(), body, opts) + _, err = blobURL.Upload(ctx, body, opts) return err } // SetMetadata sets the given metadata for the blob -func (a *DefaultClient) SetMetadata(file vfs.File, metadata map[string]*string) error { +func (a *DefaultClient) SetMetadata(ctx context.Context, file vfs.File, metadata map[string]*string) error { cli, err := a.newContainerClient(file.Location().Authority().String()) if err != nil { return err } blobURL := cli.NewBlockBlobClient(utils.RemoveLeadingSlash(file.Path())) - _, err = blobURL.SetMetadata(context.Background(), metadata, nil) + _, err = blobURL.SetMetadata(ctx, metadata, nil) return err } // Download returns an io.ReadCloser for the given vfs.File -func (a *DefaultClient) Download(file vfs.File) (io.ReadCloser, error) { +func (a *DefaultClient) Download(ctx context.Context, file vfs.File) (io.ReadCloser, error) { cli, err := a.newContainerClient(file.Location().Authority().String()) if err != nil { return nil, err } blobURL := cli.NewBlockBlobClient(utils.RemoveLeadingSlash(file.Path())) - get, err := blobURL.DownloadStream(context.Background(), nil) + get, err := blobURL.DownloadStream(ctx, nil) if err != nil { return nil, err } @@ -139,7 +139,7 @@ func (a *DefaultClient) Download(file vfs.File) (io.ReadCloser, error) { // Copy copies srcFile to the destination tgtFile within Azure Blob Storage. Note that in the case where we get // encoded spaces in the file name (i.e. %20) the '%' must be encoded or the copy command will return a not found // error. -func (a *DefaultClient) Copy(srcFile, tgtFile vfs.File) error { +func (a *DefaultClient) Copy(ctx context.Context, srcFile, tgtFile vfs.File) error { // Can't use url.PathEscape here since that will escape everything (even the directory separators) srcURL := strings.ReplaceAll(srcFile.Path(), "%", "%25") srcURL = a.serviceURL.JoinPath(srcFile.Location().Authority().String(), srcURL).String() @@ -151,7 +151,6 @@ func (a *DefaultClient) Copy(srcFile, tgtFile vfs.File) error { return err } blobURL := cli.NewBlockBlobClient(utils.RemoveLeadingSlash(tgtFile.Path())) - ctx := context.Background() resp, err := blobURL.StartCopyFromURL(ctx, srcURL, nil) if err != nil { return err @@ -170,7 +169,7 @@ func (a *DefaultClient) Copy(srcFile, tgtFile vfs.File) error { // List will return a listing of the contents of the given location. Each item in the list will contain the full key // as specified by the azure blob (including the virtual 'path'). -func (a *DefaultClient) List(l vfs.Location) ([]string, error) { +func (a *DefaultClient) List(ctx context.Context, l vfs.Location) ([]string, error) { cli, err := a.newContainerClient(l.Authority().String()) if err != nil { return []string{}, err @@ -180,7 +179,6 @@ func (a *DefaultClient) List(l vfs.Location) ([]string, error) { Prefix: to.Ptr(utils.RemoveLeadingSlash(l.Path())), Include: container.ListBlobsInclude{Metadata: true, Tags: true}, }) - ctx := context.Background() var list []string for pager.More() { listBlob, err := pager.NextPage(ctx) @@ -196,13 +194,13 @@ func (a *DefaultClient) List(l vfs.Location) ([]string, error) { } // Delete deletes the given file from Azure Blob Storage. -func (a *DefaultClient) Delete(file vfs.File) error { +func (a *DefaultClient) Delete(ctx context.Context, file vfs.File) error { cli, err := a.newContainerClient(file.Location().Authority().String()) if err != nil { return err } blobURL := cli.NewBlockBlobClient(utils.RemoveLeadingSlash(file.Path())) - _, err = blobURL.Delete(context.Background(), nil) + _, err = blobURL.Delete(ctx, nil) return err } @@ -210,14 +208,14 @@ func (a *DefaultClient) Delete(file vfs.File) error { // First the file blob is deleted, then each version of the blob is deleted. // If soft deletion is enabled for blobs in the storage account, each version will be marked for deletion and will be // permanently deleted by Azure as per the soft deletion policy. -func (a *DefaultClient) DeleteAllVersions(file vfs.File) error { +func (a *DefaultClient) DeleteAllVersions(ctx context.Context, file vfs.File) error { cli, err := a.newContainerClient(file.Location().Authority().String()) if err != nil { return err } blobURL := cli.NewBlockBlobClient(utils.RemoveLeadingSlash(file.Path())) - versions, err := a.getBlobVersions(cli, utils.RemoveLeadingSlash(file.Path())) + versions, err := a.getBlobVersions(ctx, cli, utils.RemoveLeadingSlash(file.Path())) if err != nil { return err } @@ -228,7 +226,7 @@ func (a *DefaultClient) DeleteAllVersions(file vfs.File) error { if err != nil { return err } - _, err = cli.Delete(context.Background(), nil) + _, err = cli.Delete(ctx, nil) if err != nil { return err } @@ -237,8 +235,7 @@ func (a *DefaultClient) DeleteAllVersions(file vfs.File) error { return err } -func (a *DefaultClient) getBlobVersions(cli *container.Client, blobName string) ([]*string, error) { - ctx := context.Background() +func (a *DefaultClient) getBlobVersions(ctx context.Context, cli *container.Client, blobName string) ([]*string, error) { pager := cli.NewListBlobsFlatPager(&container.ListBlobsFlatOptions{ Prefix: &blobName, Include: container.ListBlobsInclude{Versions: true}, diff --git a/backend/azure/client_integration_test.go b/backend/azure/client_integration_test.go index 52a3b08a..95e5786e 100644 --- a/backend/azure/client_integration_test.go +++ b/backend/azure/client_integration_test.go @@ -26,6 +26,7 @@ type ClientIntegrationTestSuite struct { func (s *ClientIntegrationTestSuite) SetupSuite() { s.accountName, s.accountKey = os.Getenv("VFS_AZURE_STORAGE_ACCOUNT"), os.Getenv("VFS_AZURE_STORAGE_ACCESS_KEY") + ctx := s.T().Context() credential, err := azblob.NewSharedKeyCredential(s.accountName, s.accountKey) if err != nil { @@ -36,8 +37,6 @@ func (s *ClientIntegrationTestSuite) SetupSuite() { s.Require().NoError(err) s.containerClient = cli - ctx := s.T().Context() - _, err = s.containerClient.Create(ctx, nil) s.Require().NoError(err) @@ -59,6 +58,8 @@ func (s *ClientIntegrationTestSuite) TearDownSuite() { } func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithNoPath() { + ctx := s.T().Context() + fs := NewFileSystem() f, err := fs.NewFile("test-container", "/test.txt") s.Require().NoError(err) @@ -66,15 +67,15 @@ func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithNoPath() { s.Require().NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials") // Create the new file - err = client.Upload(f, strings.NewReader("Hello world!"), "") + err = client.Upload(ctx, f, strings.NewReader("Hello world!"), "") s.Require().NoError(err, "The file should be successfully uploaded to azure") // make sure it exists - _, err = client.Properties(f.Location().URI(), f.Name()) + _, err = client.Properties(ctx, f.Location().URI(), f.Name()) s.Require().NoError(err, "If the file exists no error should be returned") // download it - reader, err := client.Download(f) + reader, err := client.Download(ctx, f) s.Require().NoError(err) dlContent, err := io.ReadAll(reader) s.Require().NoError(err) @@ -85,28 +86,30 @@ func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithNoPath() { // copy it copyOf, err := fs.NewFile("test-container", "/copy_of_test.txt") s.Require().NoError(err) - err = client.Copy(f, copyOf) + err = client.Copy(ctx, f, copyOf) s.Require().NoError(err, "Copy should succeed so there should be no error") - _, err = client.Properties(copyOf.Location().URI(), copyOf.Name()) + _, err = client.Properties(ctx, copyOf.Location().URI(), copyOf.Name()) s.Require().NoError(err, "The copy should succeed so we should not get an error on the properties call") // list the location - list, err := client.List(f.Location()) + list, err := client.List(ctx, f.Location()) s.Require().NoError(err) s.Len(list, 2) s.Equal("copy_of_test.txt", list[0]) s.Equal("test.txt", list[1]) // delete it - err = client.Delete(f) + err = client.Delete(ctx, f) s.Require().NoError(err, "if the file was deleted no error should be returned") // make sure it got deleted - _, err = client.Properties(f.Location().URI(), f.Name()) + _, err = client.Properties(ctx, f.Location().URI(), f.Name()) s.Require().Error(err, "File should have been deleted so we should get an error") } func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithPath() { + ctx := s.T().Context() + fs := NewFileSystem() f, err := fs.NewFile("test-container", "/foo/bar/test.txt") s.Require().NoError(err) @@ -114,15 +117,15 @@ func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithPath() { s.Require().NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials") // create a new file - err = client.Upload(f, strings.NewReader("Hello world!"), "") + err = client.Upload(ctx, f, strings.NewReader("Hello world!"), "") s.Require().NoError(err, "The file should be successfully uploaded to azure") // check to see if it exists - _, err = client.Properties(f.Location().(*Location).Authority().String(), f.Path()) + _, err = client.Properties(ctx, f.Location().(*Location).Authority().String(), f.Path()) s.Require().NoError(err, "If the file exists no error should be returned") // download it - reader, err := client.Download(f) + reader, err := client.Download(ctx, f) s.Require().NoError(err) dlContent, err := io.ReadAll(reader) s.Require().NoError(err) @@ -132,13 +135,15 @@ func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithPath() { s.Equal("Hello world!", string(dlContent)) // list the location - list, err := client.List(f.Location()) + list, err := client.List(ctx, f.Location()) s.Require().NoError(err) s.Len(list, 1) s.Equal("foo/bar/test.txt", list[0]) } func (s *ClientIntegrationTestSuite) TestDeleteAllVersions() { + ctx := s.T().Context() + fs := NewFileSystem() f, err := fs.NewFile("test-container", "/test.txt") s.Require().NoError(err) @@ -146,19 +151,19 @@ func (s *ClientIntegrationTestSuite) TestDeleteAllVersions() { s.Require().NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials") // Create the new file - err = client.Upload(f, strings.NewReader("Hello!"), "") + err = client.Upload(ctx, f, strings.NewReader("Hello!"), "") s.Require().NoError(err, "The file should be successfully uploaded to azure") // Recreate the file - err = client.Upload(f, strings.NewReader("Hello world!"), "") + err = client.Upload(ctx, f, strings.NewReader("Hello world!"), "") s.Require().NoError(err, "The file should be successfully uploaded to azure") // make sure it exists - _, err = client.Properties(f.Location().URI(), f.Name()) + _, err = client.Properties(ctx, f.Location().URI(), f.Name()) s.Require().NoError(err, "If the file exists no error should be returned") // delete it - err = client.DeleteAllVersions(f) + err = client.DeleteAllVersions(ctx, f) s.Require().NoError(err, "if the file versions were deleted no error should be returned") // make sure the file doesn't exist @@ -168,15 +173,17 @@ func (s *ClientIntegrationTestSuite) TestDeleteAllVersions() { } func (s *ClientIntegrationTestSuite) TestProperties() { + ctx := s.T().Context() + fs := NewFileSystem() f, err := fs.NewFile("test-container", "/foo/bar/test.txt") s.Require().NoError(err) client, err := fs.Client() s.Require().NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials") - err = client.Upload(f, strings.NewReader("Hello world!"), "") + err = client.Upload(ctx, f, strings.NewReader("Hello world!"), "") s.Require().NoError(err, "The file should be successfully uploaded to azure so we shouldn't get an error") - props, err := client.Properties(f.Location().(*Location).Authority().String(), f.Path()) + props, err := client.Properties(ctx, f.Location().(*Location).Authority().String(), f.Path()) s.Require().NoError(err, "The file exists so we shouldn't get an error") s.NotNil(props, "We should get a non-nil BlobProperties pointer back") s.Positive(props.Size, "The size should be greater than zero") @@ -184,70 +191,80 @@ func (s *ClientIntegrationTestSuite) TestProperties() { } func (s *ClientIntegrationTestSuite) TestProperties_Location() { + ctx := s.T().Context() + fs := NewFileSystem() f, err := fs.NewFile("test-container", "/foo/bar/test.txt") s.Require().NoError(err) l, _ := fs.NewLocation("test-container", "/") client, _ := fs.Client() - err = client.Upload(f, strings.NewReader("Hello world!"), "") + err = client.Upload(ctx, f, strings.NewReader("Hello world!"), "") s.Require().NoError(err, "The file should be successfully uploaded to azure so we shouldn't get an error") - props, err := client.Properties(l.URI(), "") + props, err := client.Properties(ctx, l.URI(), "") s.Require().NoError(err) s.Nil(props, "no props returned when calling properties on a location") } func (s *ClientIntegrationTestSuite) TestProperties_NonExistentFile() { + ctx := s.T().Context() + fs := NewFileSystem() f, err := fs.NewFile("test-container", "/nosuchfile.txt") s.Require().NoError(err) client, err := fs.Client() s.Require().NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials") - _, err = client.Properties(f.Location().URI(), f.Path()) + _, err = client.Properties(ctx, f.Location().URI(), f.Path()) var rerr *azcore.ResponseError s.Require().ErrorAs(err, &rerr, "The file does not exist so we expect an error") s.Equal(404, rerr.StatusCode) } func (s *ClientIntegrationTestSuite) TestDelete_NonExistentFile() { + ctx := s.T().Context() + fs := NewFileSystem() f, err := fs.NewFile("test-container", "/nosuchfile.txt") s.Require().NoError(err) client, err := fs.Client() s.Require().NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials") - err = client.Delete(f) + err = client.Delete(ctx, f) s.Require().Error(err, "The file does not exist so we expect an error") } func (s *ClientIntegrationTestSuite) TestTouch_NonExistentContainer() { + ctx := s.T().Context() + fs := NewFileSystem() f, err := fs.NewFile("nosuchcontainer", "/file.txt") s.Require().NoError(err) client, err := fs.Client() s.Require().NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials") - err = client.Upload(f, strings.NewReader(""), "") + err = client.Upload(ctx, f, strings.NewReader(""), "") s.Require().Error(err, "The container doesn't exist so we should get an error") } func (s *ClientIntegrationTestSuite) TestTouch_FileAlreadyExists() { + ctx := s.T().Context() + fs := NewFileSystem() f, err := fs.NewFile("test-container", "/touch-test.txt") s.Require().NoError(err) client, err := fs.Client() s.Require().NoError(err) - err = client.Upload(f, strings.NewReader("One fish, two fish, red fish, blue fish."), "") + err = client.Upload(ctx, f, strings.NewReader("One fish, two fish, red fish, blue fish."), "") s.Require().NoError(err) - originalProps, err := client.Properties(f.Location().(*Location).Authority().String(), f.Path()) + originalProps, err := client.Properties(ctx, f.Location().(*Location).Authority().String(), f.Path()) s.Require().NoError(err, "Should get properties back from azure with no error") err = f.Touch() s.Require().NoError(err, "Should not receive an error when touching an existing file") - newProps, err := client.Properties(f.Location().(*Location).Authority().String(), f.Path()) + newProps, err := client.Properties(ctx, f.Location().(*Location).Authority().String(), f.Path()) s.Require().NoError(err) s.NotNil(newProps, "New props should be non-nil") s.Greater(*newProps.LastModified, *originalProps.LastModified, "newProps.LastModified should be after originalProps.LastModified") diff --git a/backend/azure/client_test.go b/backend/azure/client_test.go index 21307909..fc6159e7 100644 --- a/backend/azure/client_test.go +++ b/backend/azure/client_test.go @@ -49,7 +49,7 @@ func TestDefaultClient_Properties(t *testing.T) { } // Test the Properties method - props, err := client.Properties(mockServer.URL, "test.txt") + props, err := client.Properties(t.Context(), mockServer.URL, "test.txt") require.NoError(t, err) require.NotNil(t, props) require.NotNil(t, props.Size) @@ -80,7 +80,7 @@ func TestDefaultClient_Upload(t *testing.T) { require.NoError(t, err) // Test the Upload method - err = client.Upload(f, strings.NewReader("Hello world!"), "text/plain") + err = client.Upload(t.Context(), f, strings.NewReader("Hello world!"), "text/plain") require.NoError(t, err) } @@ -108,7 +108,7 @@ func TestDefaultClient_Download(t *testing.T) { require.NoError(t, err) // Test the Download method - reader, err := client.Download(f) + reader, err := client.Download(t.Context(), f) require.NoError(t, err) defer func() { _ = reader.Close() }() @@ -149,7 +149,7 @@ func TestDefaultClient_SetMetadata(t *testing.T) { // Test the SetMetadata method metadata := map[string]*string{"key": utils.Ptr("value")} - err = client.SetMetadata(f, metadata) + err = client.SetMetadata(t.Context(), f, metadata) require.NoError(t, err) } @@ -180,7 +180,7 @@ func TestDefaultClient_Copy(t *testing.T) { require.NoError(t, err) // Test the Copy method - err = client.Copy(srcFile, tgtFile) + err = client.Copy(t.Context(), srcFile, tgtFile) require.NoError(t, err) } @@ -225,7 +225,7 @@ func TestDefaultClient_List(t *testing.T) { require.NoError(t, err) // Test the List method - list, err := client.List(l) + list, err := client.List(t.Context(), l) require.NoError(t, err) assert.Equal(t, []string{"file1.txt", "file2.txt"}, list) } @@ -253,7 +253,7 @@ func TestDefaultClient_Delete(t *testing.T) { require.NoError(t, err) // Test the Delete method - err = client.Delete(f) + err = client.Delete(t.Context(), f) require.NoError(t, err) } @@ -294,6 +294,6 @@ func TestDefaultClient_DeleteAllVersions(t *testing.T) { require.NoError(t, err) // Test the DeleteAllVersions method - err = client.DeleteAllVersions(f) + err = client.DeleteAllVersions(t.Context(), f) require.NoError(t, err) } diff --git a/backend/azure/file.go b/backend/azure/file.go index a5764942..c39d4b7c 100644 --- a/backend/azure/file.go +++ b/backend/azure/file.go @@ -1,6 +1,7 @@ package azure import ( + "context" "errors" "fmt" "io" @@ -25,6 +26,7 @@ type File struct { location *Location name string opts []options.NewFileOption + ctx context.Context tempFile *os.File isDirty bool } @@ -58,7 +60,7 @@ func (f *File) Close() error { } } - if err := client.Upload(f, f.tempFile, contentType); err != nil { + if err := client.Upload(f.ctx, f, f.tempFile, contentType); err != nil { return utils.WrapCloseError(err) } } @@ -129,7 +131,7 @@ func (f *File) Exists() (bool, error) { if err != nil { return false, utils.WrapExistsError(err) } - _, err = client.Properties(f.Location().Authority().String(), f.Path()) + _, err = client.Properties(f.ctx, f.Location().Authority().String(), f.Path()) if err != nil { if !bloberror.HasCode(err, bloberror.BlobNotFound) { return false, utils.WrapExistsError(err) @@ -190,7 +192,7 @@ func (f *File) CopyToFile(file vfs.File) (err error) { if err != nil { return utils.WrapCopyToFileError(err) } - return client.Copy(f, file) + return client.Copy(f.ctx, f, file) } } @@ -254,12 +256,12 @@ func (f *File) Delete(opts ...options.DeleteOption) error { } } - if err := client.Delete(f); err != nil { + if err := client.Delete(f.ctx, f); err != nil { return utils.WrapDeleteError(err) } if allVersions { - return client.DeleteAllVersions(f) + return client.DeleteAllVersions(f.ctx, f) } return nil @@ -271,7 +273,7 @@ func (f *File) LastModified() (*time.Time, error) { if err != nil { return nil, utils.WrapLastModifiedError(err) } - props, err := client.Properties(f.Location().Authority().String(), f.Path()) + props, err := client.Properties(f.ctx, f.Location().Authority().String(), f.Path()) if err != nil { return nil, utils.WrapLastModifiedError(err) } @@ -284,7 +286,7 @@ func (f *File) Size() (uint64, error) { if err != nil { return 0, utils.WrapSizeError(err) } - props, err := client.Properties(f.Location().Authority().String(), f.Path()) + props, err := client.Properties(f.ctx, f.Location().Authority().String(), f.Path()) if err != nil { return 0, utils.WrapSizeError(err) } @@ -324,21 +326,21 @@ func (f *File) Touch() error { } } - return client.Upload(f, strings.NewReader(""), contentType) + return client.Upload(f.ctx, f, strings.NewReader(""), contentType) } - props, err := client.Properties(f.Location().Authority().String(), f.Path()) + props, err := client.Properties(f.ctx, f.Location().Authority().String(), f.Path()) if err != nil { return utils.WrapTouchError(err) } newMetadata := make(map[string]*string) newMetadata["updated"] = to.Ptr("true") - if err := client.SetMetadata(f, newMetadata); err != nil { + if err := client.SetMetadata(f.ctx, f, newMetadata); err != nil { return utils.WrapTouchError(err) } - if err := client.SetMetadata(f, props.Metadata); err != nil { + if err := client.SetMetadata(f.ctx, f, props.Metadata); err != nil { return utils.WrapTouchError(err) } @@ -368,7 +370,7 @@ func (f *File) checkTempFile() error { } f.tempFile = tf } else { - reader, dlErr := client.Download(f) + reader, dlErr := client.Download(f.ctx, f) if dlErr != nil { return dlErr } diff --git a/backend/azure/fileSystem.go b/backend/azure/fileSystem.go index d873e94f..fee71f8d 100644 --- a/backend/azure/fileSystem.go +++ b/backend/azure/fileSystem.go @@ -1,12 +1,14 @@ package azure import ( + "context" "errors" "path" "github.com/c2fo/vfs/v7" "github.com/c2fo/vfs/v7/backend" "github.com/c2fo/vfs/v7/options" + "github.com/c2fo/vfs/v7/options/newlocation" "github.com/c2fo/vfs/v7/utils" "github.com/c2fo/vfs/v7/utils/authority" ) @@ -26,6 +28,7 @@ var ( type FileSystem struct { options *Options client Client + ctx context.Context } // NewFileSystem creates a new default FileSystem. This will set the options options.AccountName and @@ -33,6 +36,7 @@ type FileSystem struct { func NewFileSystem(opts ...options.NewFileSystemOption[FileSystem]) *FileSystem { fs := &FileSystem{ options: NewOptions(), + ctx: context.Background(), } // apply options @@ -107,7 +111,7 @@ func (fs *FileSystem) NewFile(container, absFilePath string, opts ...options.New } // NewLocation returns the azure implementation of vfs.Location -func (fs *FileSystem) NewLocation(container, absLocPath string) (vfs.Location, error) { +func (fs *FileSystem) NewLocation(container, absLocPath string, opts ...options.NewLocationOption) (vfs.Location, error) { if fs == nil { return nil, errFileSystemRequired } @@ -125,10 +129,20 @@ func (fs *FileSystem) NewLocation(container, absLocPath string) (vfs.Location, e return nil, err } + ctx := fs.ctx + for _, o := range opts { + switch o := o.(type) { + case *newlocation.Context: + ctx = context.Context(o) + default: + } + } + return &Location{ fileSystem: fs, path: path.Clean(absLocPath), authority: auth, + ctx: ctx, }, nil } diff --git a/backend/azure/file_test.go b/backend/azure/file_test.go index 1997cb94..1fdd2280 100644 --- a/backend/azure/file_test.go +++ b/backend/azure/file_test.go @@ -43,8 +43,8 @@ func (s *FileTestSuite) TestClose_FlushTempFile() { fs := NewFileSystem(WithClient(client)) f, _ := fs.NewFile("test-container", "/foo.txt") - client.EXPECT().Properties("test-container", "/foo.txt").Return(nil, errBlobNotFound) - client.EXPECT().Upload(mock.Anything, mock.Anything, "").Return(nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(nil, errBlobNotFound) + client.EXPECT().Upload(mock.Anything, mock.Anything, mock.Anything, "").Return(nil) _, err := f.Write([]byte("Hello, World!")) s.Require().NoError(err) s.Require().NoError(f.Close()) @@ -56,8 +56,8 @@ func (s *FileTestSuite) TestRead() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The file should exist so no error should be returned") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) contents := make([]byte, 12) n, err := f.Read(contents) s.Require().NoError(err) @@ -71,8 +71,8 @@ func (s *FileTestSuite) TestSeek() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The file should exist so no error should be returned") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) newOffset, err := f.Seek(6, io.SeekStart) s.Require().NoError(err) s.Equal(int64(6), newOffset) @@ -90,8 +90,8 @@ func (s *FileTestSuite) TestWrite() { f, err := fs.NewFile("test-container", "/foo.txt") s.NotNil(f) s.Require().NoError(err) - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) n, err := f.Write([]byte(" Aaaaand, Goodbye!")) s.Require().NoError(err) s.Equal(18, n) @@ -115,7 +115,7 @@ func (s *FileTestSuite) TestExists() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The file should exist so no error should be returned") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) exists, err := f.Exists() s.Require().NoError(err) s.True(exists) @@ -127,7 +127,7 @@ func (s *FileTestSuite) TestExists_NonExistentFile() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Properties("test-container", "/foo.txt").Return(nil, errBlobNotFound) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(nil, errBlobNotFound) exists, err := f.Exists() s.Require().NoError(err, "no error is returned when the file does not exist") s.False(exists) @@ -137,8 +137,8 @@ func (s *FileTestSuite) TestCloseWithContentType() { client := mocks.NewClient(s.T()) fs := NewFileSystem(WithClient(client)) f, _ := fs.NewFile("test-container", "/foo.txt", newfile.WithContentType("text/plain")) - client.EXPECT().Properties("test-container", "/foo.txt").Return(nil, errBlobNotFound) - client.EXPECT().Upload(mock.Anything, mock.Anything, "text/plain").Return(nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(nil, errBlobNotFound) + client.EXPECT().Upload(mock.Anything, mock.Anything, mock.Anything, "text/plain").Return(nil) _, _ = f.Write([]byte("Hello, World!")) s.Require().NoError(f.Close()) } @@ -156,9 +156,9 @@ func (s *FileTestSuite) TestCopyToLocation() { fs := NewFileSystem(WithClient(client)) source, _ := fs.NewFile("test-container", "/foo.txt") targetLoc, _ := fs.NewLocation("test-container", "/new/folder/") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) - client.EXPECT().Copy(mock.Anything, mock.Anything).Return(nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) + client.EXPECT().Copy(mock.Anything, mock.Anything, mock.Anything).Return(nil) copiedFile, err := source.CopyToLocation(targetLoc) s.Require().NoError(err) s.NotNil(copiedFile) @@ -171,9 +171,9 @@ func (s *FileTestSuite) TestCopyToFile() { source, _ := fs.NewFile("test-container", "/foo.txt") target, _ := fs.NewFile("test-container", "/bar.txt") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) - client.EXPECT().Copy(mock.Anything, mock.Anything).Return(nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) + client.EXPECT().Copy(mock.Anything, mock.Anything, mock.Anything).Return(nil) err := source.CopyToFile(target) s.Require().NoError(err) } @@ -185,9 +185,9 @@ func (s *FileTestSuite) TestCopyToFileBuffered() { source, _ := fs.NewFile("test-container", "/foo.txt") target, _ := fs.NewFile("test-container", "/bar.txt") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) - client.EXPECT().Copy(mock.Anything, mock.Anything).Return(nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) + client.EXPECT().Copy(mock.Anything, mock.Anything, mock.Anything).Return(nil) err := source.CopyToFile(target) s.Require().NoError(err) } @@ -198,10 +198,10 @@ func (s *FileTestSuite) TestMoveToLocation() { source, _ := fs.NewFile("test-container", "/foo.txt") target, _ := fs.NewLocation("test-container", "/new/folder/") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) - client.EXPECT().Copy(mock.Anything, mock.Anything).Return(nil) - client.EXPECT().Delete(mock.Anything).Return(nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) + client.EXPECT().Copy(mock.Anything, mock.Anything, mock.Anything).Return(nil) + client.EXPECT().Delete(mock.Anything, mock.Anything).Return(nil) movedFile, err := source.MoveToLocation(target) s.Require().NoError(err) s.NotNil(movedFile) @@ -213,10 +213,10 @@ func (s *FileTestSuite) TestMoveToFile() { fs := NewFileSystem(WithClient(client)) source, _ := fs.NewFile("test-container", "/foo.txt") target, _ := fs.NewFile("test-container", "/bar.txt") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) - client.EXPECT().Copy(mock.Anything, mock.Anything).Return(nil) - client.EXPECT().Delete(mock.Anything).Return(nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("blah")), nil) + client.EXPECT().Copy(mock.Anything, mock.Anything, mock.Anything).Return(nil) + client.EXPECT().Delete(mock.Anything, mock.Anything).Return(nil) err := source.MoveToFile(target) s.Require().NoError(err) } @@ -227,7 +227,7 @@ func (s *FileTestSuite) TestDelete() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Delete(mock.Anything).Return(nil) + client.EXPECT().Delete(mock.Anything, mock.Anything).Return(nil) s.Require().NoError(f.Delete(), "The delete should succeed so there should be no error") } @@ -237,8 +237,8 @@ func (s *FileTestSuite) TestDeleteWithAllVersionsOption() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Delete(mock.Anything).Return(nil) - client.EXPECT().DeleteAllVersions(mock.Anything).Return(nil) + client.EXPECT().Delete(mock.Anything, mock.Anything).Return(nil) + client.EXPECT().DeleteAllVersions(mock.Anything, mock.Anything).Return(nil) s.Require().NoError(f.Delete(delete.WithAllVersions()), "The delete should succeed so there should be no error") } @@ -248,7 +248,7 @@ func (s *FileTestSuite) TestDeleteWithAllVersionsOption_Error() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Delete(mock.Anything).Return(errors.New("i always error")) + client.EXPECT().Delete(mock.Anything, mock.Anything).Return(errors.New("i always error")) err = f.Delete(delete.WithAllVersions()) s.Require().Error(err, "If the file does not exist we get an error") } @@ -259,7 +259,7 @@ func (s *FileTestSuite) TestDelete_NonExistentFile() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Delete(mock.Anything).Return(errors.New("i always error")) + client.EXPECT().Delete(mock.Anything, mock.Anything).Return(errors.New("i always error")) err = f.Delete() s.Require().Error(err, "If the file does not exist we get an error") } @@ -271,7 +271,7 @@ func (s *FileTestSuite) TestLastModified() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{LastModified: &now}, nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{LastModified: &now}, nil) t, err := f.LastModified() s.Require().NoError(err) s.NotNil(t) @@ -283,7 +283,7 @@ func (s *FileTestSuite) TestSize() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{Size: to.Ptr[int64](5)}, nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{Size: to.Ptr[int64](5)}, nil) size, err := f.Size() s.Require().NoError(err) s.Equal(uint64(5), size, "The size should be 5") @@ -295,7 +295,7 @@ func (s *FileTestSuite) TestSize_NonExistentFile() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Properties("test-container", "/foo.txt").Return(nil, errors.New("i always error")) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(nil, errors.New("i always error")) size, err := f.Size() s.Require().Error(err, "If the file does not exist we get an error") s.Zero(size, "the file does not exist so the size is 0") @@ -324,8 +324,8 @@ func (s *FileTestSuite) TestTouch() { f, err := fs.NewFile("test-container", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().SetMetadata(mock.Anything, mock.Anything).Return(nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().SetMetadata(mock.Anything, mock.Anything, mock.Anything).Return(nil) s.Require().NoError(f.Touch()) } @@ -335,8 +335,8 @@ func (s *FileTestSuite) TestTouch_NonExistentContainer() { f, err := fs.NewFile("nosuchcontainer", "/foo.txt") s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Properties("nosuchcontainer", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().SetMetadata(mock.Anything, mock.Anything).Return(errors.New("i always error")) + client.EXPECT().Properties(mock.Anything, "nosuchcontainer", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().SetMetadata(mock.Anything, mock.Anything, mock.Anything).Return(errors.New("i always error")) s.Require().Error(f.Touch(), "The container does not exist so creating the new file should error") } @@ -346,8 +346,8 @@ func (s *FileTestSuite) TestTouchWithContentType() { f, err := fs.NewFile("test-container", "/foo.txt", newfile.WithContentType("text/plain")) s.Require().NoError(err, "The path is valid so no error should be returned") - client.EXPECT().Properties("test-container", "/foo.txt").Return(nil, errBlobNotFound) - client.EXPECT().Upload(mock.Anything, mock.Anything, "text/plain").Return(nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(nil, errBlobNotFound) + client.EXPECT().Upload(mock.Anything, mock.Anything, mock.Anything, "text/plain").Return(nil) s.Require().NoError(f.Touch()) } @@ -373,8 +373,8 @@ func (s *FileTestSuite) TestCheckTempFile() { s.NotNil(azureFile) s.Nil(azureFile.tempFile, "No calls to checkTempFile have occurred so we expect tempFile to be nil") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("Hello World!")), nil) err = azureFile.checkTempFile() s.Require().NoError(err, "Check temp file should create a local temp file so no error is expected") s.NotNil(azureFile.tempFile, "After the call to checkTempFile we should have a non-nil tempFile") @@ -396,7 +396,7 @@ func (s *FileTestSuite) TestCheckTempFile_FileDoesNotExist() { s.NotNil(azureFile) s.Nil(azureFile.tempFile, "No calls to checkTempFile have occurred so we expect tempFile to be nil") - client.EXPECT().Properties("test-container", "/foo.txt").Return(nil, errBlobNotFound) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(nil, errBlobNotFound) err = azureFile.checkTempFile() s.Require().NoError(err, "Check temp file should create a local temp file so no error is expected") s.NotNil(azureFile.tempFile, "After the call to checkTempFile we should have a non-nil tempFile") @@ -418,8 +418,8 @@ func (s *FileTestSuite) TestCheckTempFile_DownloadError() { s.NotNil(azureFile) s.Nil(azureFile.tempFile, "No calls to checkTempFile have occurred so we expect tempFile to be nil") - client.EXPECT().Properties("test-container", "/foo.txt").Return(&BlobProperties{}, nil) - client.EXPECT().Download(mock.Anything).Return(nil, errors.New("i always error")) + client.EXPECT().Properties(mock.Anything, "test-container", "/foo.txt").Return(&BlobProperties{}, nil) + client.EXPECT().Download(mock.Anything, mock.Anything).Return(nil, errors.New("i always error")) err = azureFile.checkTempFile() s.Require().Error(err, "The call to client.Download() errors so we expect to get an error") } diff --git a/backend/azure/location.go b/backend/azure/location.go index 38ec6ff4..2cdc4b9d 100644 --- a/backend/azure/location.go +++ b/backend/azure/location.go @@ -1,6 +1,7 @@ package azure import ( + "context" "errors" "path" "regexp" @@ -8,6 +9,8 @@ import ( "github.com/c2fo/vfs/v7" "github.com/c2fo/vfs/v7/options" + "github.com/c2fo/vfs/v7/options/newfile" + "github.com/c2fo/vfs/v7/options/newlocation" "github.com/c2fo/vfs/v7/utils" "github.com/c2fo/vfs/v7/utils/authority" ) @@ -19,6 +22,7 @@ type Location struct { authority authority.Authority path string fileSystem *FileSystem + ctx context.Context } // String returns the URI @@ -32,7 +36,7 @@ func (l *Location) List() ([]string, error) { if err != nil { return nil, err } - list, err := client.List(l) + list, err := client.List(l.ctx, l) if err != nil { return nil, err } @@ -130,7 +134,7 @@ func (l *Location) Exists() (bool, error) { if err != nil { return false, err } - _, err = client.Properties(l.Authority().String(), "") + _, err = client.Properties(l.ctx, l.Authority().String(), "") if err != nil { return false, nil } @@ -138,7 +142,7 @@ func (l *Location) Exists() (bool, error) { } // NewLocation creates a new location instance relative to the current location's path. -func (l *Location) NewLocation(relLocPath string) (vfs.Location, error) { +func (l *Location) NewLocation(relLocPath string, opts ...options.NewLocationOption) (vfs.Location, error) { if l == nil { return nil, errLocationRequired } @@ -147,10 +151,20 @@ func (l *Location) NewLocation(relLocPath string) (vfs.Location, error) { return nil, err } + ctx := l.ctx + for _, o := range opts { + switch o := o.(type) { + case *newlocation.Context: + ctx = context.Context(o) + default: + } + } + return &Location{ fileSystem: l.fileSystem, path: path.Join(l.path, relLocPath), authority: l.Authority(), + ctx: ctx, }, nil } @@ -198,10 +212,20 @@ func (l *Location) NewFile(relFilePath string, opts ...options.NewFileOption) (v return nil, err } + ctx := l.ctx + for _, o := range opts { + switch o := o.(type) { + case *newfile.Context: + ctx = context.Context(o) + default: + } + } + return &File{ location: newLocation.(*Location), name: path.Join(l.Path(), relFilePath), opts: opts, + ctx: ctx, }, nil } diff --git a/backend/azure/location_test.go b/backend/azure/location_test.go index 63080d50..1f7067f3 100644 --- a/backend/azure/location_test.go +++ b/backend/azure/location_test.go @@ -37,7 +37,7 @@ func (s *LocationTestSuite) TestString() { func (s *LocationTestSuite) TestList() { client := mocks.NewClient(s.T()) - client.EXPECT().List(mock.Anything).Return([]string{"file1.txt", "file2.txt"}, nil) + client.EXPECT().List(mock.Anything, mock.Anything).Return([]string{"file1.txt", "file2.txt"}, nil) fs := NewFileSystem(WithClient(client)) l, _ := fs.NewLocation("test-container", "/some/folder/") listing, err := l.List() @@ -47,7 +47,7 @@ func (s *LocationTestSuite) TestList() { func (s *LocationTestSuite) TestListByPrefix() { client := mocks.NewClient(s.T()) - client.EXPECT().List(mock.Anything).Return([]string{"file1.txt", "file2.txt", "foo.txt"}, nil) + client.EXPECT().List(mock.Anything, mock.Anything).Return([]string{"file1.txt", "file2.txt", "foo.txt"}, nil) fs := NewFileSystem(WithClient(client)) l, _ := fs.NewLocation("test-container", "/some/folder/") listing, err := l.ListByPrefix("file") @@ -59,7 +59,7 @@ func (s *LocationTestSuite) TestListByPrefix() { func (s *LocationTestSuite) TestListByRegex() { client := mocks.NewClient(s.T()) - client.EXPECT().List(mock.Anything).Return([]string{"file1.txt", "file2.txt", "foo.txt"}, nil) + client.EXPECT().List(mock.Anything, mock.Anything).Return([]string{"file1.txt", "file2.txt", "foo.txt"}, nil) fs := NewFileSystem(WithClient(client)) l, _ := fs.NewLocation("test-container", "/some/folder/") regex := regexp.MustCompile("file") @@ -104,7 +104,7 @@ func (s *LocationTestSuite) TestPath() { func (s *LocationTestSuite) TestExists() { client := mocks.NewClient(s.T()) - client.EXPECT().Properties("test-container", "").Return(&BlobProperties{}, nil) + client.EXPECT().Properties(mock.Anything, "test-container", "").Return(&BlobProperties{}, nil) fs := NewFileSystem(WithClient(client)) l, _ := fs.NewLocation("test-container", "/some/folder/") exists, err := l.Exists() @@ -114,7 +114,7 @@ func (s *LocationTestSuite) TestExists() { func (s *LocationTestSuite) TestExists_NonExistentFile() { client := mocks.NewClient(s.T()) - client.EXPECT().Properties("test-container", "").Return(nil, errors.New("no such file")) + client.EXPECT().Properties(mock.Anything, "test-container", "").Return(nil, errors.New("no such file")) fs := NewFileSystem(WithClient(client)) l, _ := fs.NewLocation("test-container", "/some/folder/") exists, err := l.Exists() @@ -257,7 +257,7 @@ func (s *LocationTestSuite) TestNewFile_NilReceiver() { func (s *LocationTestSuite) TestDeleteFile() { client := mocks.NewClient(s.T()) - client.EXPECT().Delete(mock.Anything).Return(nil) + client.EXPECT().Delete(mock.Anything, mock.Anything).Return(nil) fs := NewFileSystem(WithClient(client)) l, _ := fs.NewLocation("test-container", "/some/folder/") s.Require().NoError(l.DeleteFile("clever_file.txt"), "the file exists so we do not expect an error") @@ -265,7 +265,7 @@ func (s *LocationTestSuite) TestDeleteFile() { func (s *LocationTestSuite) TestDeleteFile_DoesNotExist() { client := mocks.NewClient(s.T()) - client.EXPECT().Delete(mock.Anything).Return(errors.New("no such file")) + client.EXPECT().Delete(mock.Anything, mock.Anything).Return(errors.New("no such file")) fs := NewFileSystem(WithClient(client)) l, _ := fs.NewLocation("test-container", "/some/folder/") s.Require().Error(l.DeleteFile("nosuchfile.txt"), "the file does not exist so we expect an error") diff --git a/backend/azure/mocks/Client.go b/backend/azure/mocks/Client.go index d399e068..e5fd40c3 100644 --- a/backend/azure/mocks/Client.go +++ b/backend/azure/mocks/Client.go @@ -5,6 +5,7 @@ package mocks import ( + "context" "io" "github.com/c2fo/vfs/v7" @@ -40,16 +41,16 @@ func (_m *Client) EXPECT() *Client_Expecter { } // Copy provides a mock function for the type Client -func (_mock *Client) Copy(srcFile vfs.File, tgtFile vfs.File) error { - ret := _mock.Called(srcFile, tgtFile) +func (_mock *Client) Copy(ctx context.Context, srcFile vfs.File, tgtFile vfs.File) error { + ret := _mock.Called(ctx, srcFile, tgtFile) if len(ret) == 0 { panic("no return value specified for Copy") } var r0 error - if returnFunc, ok := ret.Get(0).(func(vfs.File, vfs.File) error); ok { - r0 = returnFunc(srcFile, tgtFile) + if returnFunc, ok := ret.Get(0).(func(context.Context, vfs.File, vfs.File) error); ok { + r0 = returnFunc(ctx, srcFile, tgtFile) } else { r0 = ret.Error(0) } @@ -62,25 +63,31 @@ type Client_Copy_Call struct { } // Copy is a helper method to define mock.On call +// - ctx context.Context // - srcFile vfs.File // - tgtFile vfs.File -func (_e *Client_Expecter) Copy(srcFile interface{}, tgtFile interface{}) *Client_Copy_Call { - return &Client_Copy_Call{Call: _e.mock.On("Copy", srcFile, tgtFile)} +func (_e *Client_Expecter) Copy(ctx interface{}, srcFile interface{}, tgtFile interface{}) *Client_Copy_Call { + return &Client_Copy_Call{Call: _e.mock.On("Copy", ctx, srcFile, tgtFile)} } -func (_c *Client_Copy_Call) Run(run func(srcFile vfs.File, tgtFile vfs.File)) *Client_Copy_Call { +func (_c *Client_Copy_Call) Run(run func(ctx context.Context, srcFile vfs.File, tgtFile vfs.File)) *Client_Copy_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 vfs.File + var arg0 context.Context if args[0] != nil { - arg0 = args[0].(vfs.File) + arg0 = args[0].(context.Context) } var arg1 vfs.File if args[1] != nil { arg1 = args[1].(vfs.File) } + var arg2 vfs.File + if args[2] != nil { + arg2 = args[2].(vfs.File) + } run( arg0, arg1, + arg2, ) }) return _c @@ -91,22 +98,22 @@ func (_c *Client_Copy_Call) Return(err error) *Client_Copy_Call { return _c } -func (_c *Client_Copy_Call) RunAndReturn(run func(srcFile vfs.File, tgtFile vfs.File) error) *Client_Copy_Call { +func (_c *Client_Copy_Call) RunAndReturn(run func(ctx context.Context, srcFile vfs.File, tgtFile vfs.File) error) *Client_Copy_Call { _c.Call.Return(run) return _c } // Delete provides a mock function for the type Client -func (_mock *Client) Delete(file vfs.File) error { - ret := _mock.Called(file) +func (_mock *Client) Delete(ctx context.Context, file vfs.File) error { + ret := _mock.Called(ctx, file) if len(ret) == 0 { panic("no return value specified for Delete") } var r0 error - if returnFunc, ok := ret.Get(0).(func(vfs.File) error); ok { - r0 = returnFunc(file) + if returnFunc, ok := ret.Get(0).(func(context.Context, vfs.File) error); ok { + r0 = returnFunc(ctx, file) } else { r0 = ret.Error(0) } @@ -119,19 +126,25 @@ type Client_Delete_Call struct { } // Delete is a helper method to define mock.On call +// - ctx context.Context // - file vfs.File -func (_e *Client_Expecter) Delete(file interface{}) *Client_Delete_Call { - return &Client_Delete_Call{Call: _e.mock.On("Delete", file)} +func (_e *Client_Expecter) Delete(ctx interface{}, file interface{}) *Client_Delete_Call { + return &Client_Delete_Call{Call: _e.mock.On("Delete", ctx, file)} } -func (_c *Client_Delete_Call) Run(run func(file vfs.File)) *Client_Delete_Call { +func (_c *Client_Delete_Call) Run(run func(ctx context.Context, file vfs.File)) *Client_Delete_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 vfs.File + var arg0 context.Context if args[0] != nil { - arg0 = args[0].(vfs.File) + arg0 = args[0].(context.Context) + } + var arg1 vfs.File + if args[1] != nil { + arg1 = args[1].(vfs.File) } run( arg0, + arg1, ) }) return _c @@ -142,22 +155,22 @@ func (_c *Client_Delete_Call) Return(err error) *Client_Delete_Call { return _c } -func (_c *Client_Delete_Call) RunAndReturn(run func(file vfs.File) error) *Client_Delete_Call { +func (_c *Client_Delete_Call) RunAndReturn(run func(ctx context.Context, file vfs.File) error) *Client_Delete_Call { _c.Call.Return(run) return _c } // DeleteAllVersions provides a mock function for the type Client -func (_mock *Client) DeleteAllVersions(file vfs.File) error { - ret := _mock.Called(file) +func (_mock *Client) DeleteAllVersions(ctx context.Context, file vfs.File) error { + ret := _mock.Called(ctx, file) if len(ret) == 0 { panic("no return value specified for DeleteAllVersions") } var r0 error - if returnFunc, ok := ret.Get(0).(func(vfs.File) error); ok { - r0 = returnFunc(file) + if returnFunc, ok := ret.Get(0).(func(context.Context, vfs.File) error); ok { + r0 = returnFunc(ctx, file) } else { r0 = ret.Error(0) } @@ -170,19 +183,25 @@ type Client_DeleteAllVersions_Call struct { } // DeleteAllVersions is a helper method to define mock.On call +// - ctx context.Context // - file vfs.File -func (_e *Client_Expecter) DeleteAllVersions(file interface{}) *Client_DeleteAllVersions_Call { - return &Client_DeleteAllVersions_Call{Call: _e.mock.On("DeleteAllVersions", file)} +func (_e *Client_Expecter) DeleteAllVersions(ctx interface{}, file interface{}) *Client_DeleteAllVersions_Call { + return &Client_DeleteAllVersions_Call{Call: _e.mock.On("DeleteAllVersions", ctx, file)} } -func (_c *Client_DeleteAllVersions_Call) Run(run func(file vfs.File)) *Client_DeleteAllVersions_Call { +func (_c *Client_DeleteAllVersions_Call) Run(run func(ctx context.Context, file vfs.File)) *Client_DeleteAllVersions_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 vfs.File + var arg0 context.Context if args[0] != nil { - arg0 = args[0].(vfs.File) + arg0 = args[0].(context.Context) + } + var arg1 vfs.File + if args[1] != nil { + arg1 = args[1].(vfs.File) } run( arg0, + arg1, ) }) return _c @@ -193,14 +212,14 @@ func (_c *Client_DeleteAllVersions_Call) Return(err error) *Client_DeleteAllVers return _c } -func (_c *Client_DeleteAllVersions_Call) RunAndReturn(run func(file vfs.File) error) *Client_DeleteAllVersions_Call { +func (_c *Client_DeleteAllVersions_Call) RunAndReturn(run func(ctx context.Context, file vfs.File) error) *Client_DeleteAllVersions_Call { _c.Call.Return(run) return _c } // Download provides a mock function for the type Client -func (_mock *Client) Download(file vfs.File) (io.ReadCloser, error) { - ret := _mock.Called(file) +func (_mock *Client) Download(ctx context.Context, file vfs.File) (io.ReadCloser, error) { + ret := _mock.Called(ctx, file) if len(ret) == 0 { panic("no return value specified for Download") @@ -208,18 +227,18 @@ func (_mock *Client) Download(file vfs.File) (io.ReadCloser, error) { var r0 io.ReadCloser var r1 error - if returnFunc, ok := ret.Get(0).(func(vfs.File) (io.ReadCloser, error)); ok { - return returnFunc(file) + if returnFunc, ok := ret.Get(0).(func(context.Context, vfs.File) (io.ReadCloser, error)); ok { + return returnFunc(ctx, file) } - if returnFunc, ok := ret.Get(0).(func(vfs.File) io.ReadCloser); ok { - r0 = returnFunc(file) + if returnFunc, ok := ret.Get(0).(func(context.Context, vfs.File) io.ReadCloser); ok { + r0 = returnFunc(ctx, file) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(io.ReadCloser) } } - if returnFunc, ok := ret.Get(1).(func(vfs.File) error); ok { - r1 = returnFunc(file) + if returnFunc, ok := ret.Get(1).(func(context.Context, vfs.File) error); ok { + r1 = returnFunc(ctx, file) } else { r1 = ret.Error(1) } @@ -232,19 +251,25 @@ type Client_Download_Call struct { } // Download is a helper method to define mock.On call +// - ctx context.Context // - file vfs.File -func (_e *Client_Expecter) Download(file interface{}) *Client_Download_Call { - return &Client_Download_Call{Call: _e.mock.On("Download", file)} +func (_e *Client_Expecter) Download(ctx interface{}, file interface{}) *Client_Download_Call { + return &Client_Download_Call{Call: _e.mock.On("Download", ctx, file)} } -func (_c *Client_Download_Call) Run(run func(file vfs.File)) *Client_Download_Call { +func (_c *Client_Download_Call) Run(run func(ctx context.Context, file vfs.File)) *Client_Download_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 vfs.File + var arg0 context.Context if args[0] != nil { - arg0 = args[0].(vfs.File) + arg0 = args[0].(context.Context) + } + var arg1 vfs.File + if args[1] != nil { + arg1 = args[1].(vfs.File) } run( arg0, + arg1, ) }) return _c @@ -255,14 +280,14 @@ func (_c *Client_Download_Call) Return(readCloser io.ReadCloser, err error) *Cli return _c } -func (_c *Client_Download_Call) RunAndReturn(run func(file vfs.File) (io.ReadCloser, error)) *Client_Download_Call { +func (_c *Client_Download_Call) RunAndReturn(run func(ctx context.Context, file vfs.File) (io.ReadCloser, error)) *Client_Download_Call { _c.Call.Return(run) return _c } // List provides a mock function for the type Client -func (_mock *Client) List(l vfs.Location) ([]string, error) { - ret := _mock.Called(l) +func (_mock *Client) List(ctx context.Context, l vfs.Location) ([]string, error) { + ret := _mock.Called(ctx, l) if len(ret) == 0 { panic("no return value specified for List") @@ -270,18 +295,18 @@ func (_mock *Client) List(l vfs.Location) ([]string, error) { var r0 []string var r1 error - if returnFunc, ok := ret.Get(0).(func(vfs.Location) ([]string, error)); ok { - return returnFunc(l) + if returnFunc, ok := ret.Get(0).(func(context.Context, vfs.Location) ([]string, error)); ok { + return returnFunc(ctx, l) } - if returnFunc, ok := ret.Get(0).(func(vfs.Location) []string); ok { - r0 = returnFunc(l) + if returnFunc, ok := ret.Get(0).(func(context.Context, vfs.Location) []string); ok { + r0 = returnFunc(ctx, l) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } - if returnFunc, ok := ret.Get(1).(func(vfs.Location) error); ok { - r1 = returnFunc(l) + if returnFunc, ok := ret.Get(1).(func(context.Context, vfs.Location) error); ok { + r1 = returnFunc(ctx, l) } else { r1 = ret.Error(1) } @@ -294,19 +319,25 @@ type Client_List_Call struct { } // List is a helper method to define mock.On call +// - ctx context.Context // - l vfs.Location -func (_e *Client_Expecter) List(l interface{}) *Client_List_Call { - return &Client_List_Call{Call: _e.mock.On("List", l)} +func (_e *Client_Expecter) List(ctx interface{}, l interface{}) *Client_List_Call { + return &Client_List_Call{Call: _e.mock.On("List", ctx, l)} } -func (_c *Client_List_Call) Run(run func(l vfs.Location)) *Client_List_Call { +func (_c *Client_List_Call) Run(run func(ctx context.Context, l vfs.Location)) *Client_List_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 vfs.Location + var arg0 context.Context if args[0] != nil { - arg0 = args[0].(vfs.Location) + arg0 = args[0].(context.Context) + } + var arg1 vfs.Location + if args[1] != nil { + arg1 = args[1].(vfs.Location) } run( arg0, + arg1, ) }) return _c @@ -317,14 +348,14 @@ func (_c *Client_List_Call) Return(strings []string, err error) *Client_List_Cal return _c } -func (_c *Client_List_Call) RunAndReturn(run func(l vfs.Location) ([]string, error)) *Client_List_Call { +func (_c *Client_List_Call) RunAndReturn(run func(ctx context.Context, l vfs.Location) ([]string, error)) *Client_List_Call { _c.Call.Return(run) return _c } // Properties provides a mock function for the type Client -func (_mock *Client) Properties(locationURI string, filePath string) (*types.BlobProperties, error) { - ret := _mock.Called(locationURI, filePath) +func (_mock *Client) Properties(ctx context.Context, locationURI string, filePath string) (*types.BlobProperties, error) { + ret := _mock.Called(ctx, locationURI, filePath) if len(ret) == 0 { panic("no return value specified for Properties") @@ -332,18 +363,18 @@ func (_mock *Client) Properties(locationURI string, filePath string) (*types.Blo var r0 *types.BlobProperties var r1 error - if returnFunc, ok := ret.Get(0).(func(string, string) (*types.BlobProperties, error)); ok { - return returnFunc(locationURI, filePath) + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (*types.BlobProperties, error)); ok { + return returnFunc(ctx, locationURI, filePath) } - if returnFunc, ok := ret.Get(0).(func(string, string) *types.BlobProperties); ok { - r0 = returnFunc(locationURI, filePath) + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) *types.BlobProperties); ok { + r0 = returnFunc(ctx, locationURI, filePath) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*types.BlobProperties) } } - if returnFunc, ok := ret.Get(1).(func(string, string) error); ok { - r1 = returnFunc(locationURI, filePath) + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = returnFunc(ctx, locationURI, filePath) } else { r1 = ret.Error(1) } @@ -356,25 +387,31 @@ type Client_Properties_Call struct { } // Properties is a helper method to define mock.On call +// - ctx context.Context // - locationURI string // - filePath string -func (_e *Client_Expecter) Properties(locationURI interface{}, filePath interface{}) *Client_Properties_Call { - return &Client_Properties_Call{Call: _e.mock.On("Properties", locationURI, filePath)} +func (_e *Client_Expecter) Properties(ctx interface{}, locationURI interface{}, filePath interface{}) *Client_Properties_Call { + return &Client_Properties_Call{Call: _e.mock.On("Properties", ctx, locationURI, filePath)} } -func (_c *Client_Properties_Call) Run(run func(locationURI string, filePath string)) *Client_Properties_Call { +func (_c *Client_Properties_Call) Run(run func(ctx context.Context, locationURI string, filePath string)) *Client_Properties_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 string + var arg0 context.Context if args[0] != nil { - arg0 = args[0].(string) + arg0 = args[0].(context.Context) } var arg1 string if args[1] != nil { arg1 = args[1].(string) } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } run( arg0, arg1, + arg2, ) }) return _c @@ -385,22 +422,22 @@ func (_c *Client_Properties_Call) Return(blobProperties *types.BlobProperties, e return _c } -func (_c *Client_Properties_Call) RunAndReturn(run func(locationURI string, filePath string) (*types.BlobProperties, error)) *Client_Properties_Call { +func (_c *Client_Properties_Call) RunAndReturn(run func(ctx context.Context, locationURI string, filePath string) (*types.BlobProperties, error)) *Client_Properties_Call { _c.Call.Return(run) return _c } // SetMetadata provides a mock function for the type Client -func (_mock *Client) SetMetadata(file vfs.File, metadata map[string]*string) error { - ret := _mock.Called(file, metadata) +func (_mock *Client) SetMetadata(ctx context.Context, file vfs.File, metadata map[string]*string) error { + ret := _mock.Called(ctx, file, metadata) if len(ret) == 0 { panic("no return value specified for SetMetadata") } var r0 error - if returnFunc, ok := ret.Get(0).(func(vfs.File, map[string]*string) error); ok { - r0 = returnFunc(file, metadata) + if returnFunc, ok := ret.Get(0).(func(context.Context, vfs.File, map[string]*string) error); ok { + r0 = returnFunc(ctx, file, metadata) } else { r0 = ret.Error(0) } @@ -413,25 +450,31 @@ type Client_SetMetadata_Call struct { } // SetMetadata is a helper method to define mock.On call +// - ctx context.Context // - file vfs.File // - metadata map[string]*string -func (_e *Client_Expecter) SetMetadata(file interface{}, metadata interface{}) *Client_SetMetadata_Call { - return &Client_SetMetadata_Call{Call: _e.mock.On("SetMetadata", file, metadata)} +func (_e *Client_Expecter) SetMetadata(ctx interface{}, file interface{}, metadata interface{}) *Client_SetMetadata_Call { + return &Client_SetMetadata_Call{Call: _e.mock.On("SetMetadata", ctx, file, metadata)} } -func (_c *Client_SetMetadata_Call) Run(run func(file vfs.File, metadata map[string]*string)) *Client_SetMetadata_Call { +func (_c *Client_SetMetadata_Call) Run(run func(ctx context.Context, file vfs.File, metadata map[string]*string)) *Client_SetMetadata_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 vfs.File + var arg0 context.Context if args[0] != nil { - arg0 = args[0].(vfs.File) + arg0 = args[0].(context.Context) } - var arg1 map[string]*string + var arg1 vfs.File if args[1] != nil { - arg1 = args[1].(map[string]*string) + arg1 = args[1].(vfs.File) + } + var arg2 map[string]*string + if args[2] != nil { + arg2 = args[2].(map[string]*string) } run( arg0, arg1, + arg2, ) }) return _c @@ -442,22 +485,22 @@ func (_c *Client_SetMetadata_Call) Return(err error) *Client_SetMetadata_Call { return _c } -func (_c *Client_SetMetadata_Call) RunAndReturn(run func(file vfs.File, metadata map[string]*string) error) *Client_SetMetadata_Call { +func (_c *Client_SetMetadata_Call) RunAndReturn(run func(ctx context.Context, file vfs.File, metadata map[string]*string) error) *Client_SetMetadata_Call { _c.Call.Return(run) return _c } // Upload provides a mock function for the type Client -func (_mock *Client) Upload(file vfs.File, content io.ReadSeeker, contentType string) error { - ret := _mock.Called(file, content, contentType) +func (_mock *Client) Upload(ctx context.Context, file vfs.File, content io.ReadSeeker, contentType string) error { + ret := _mock.Called(ctx, file, content, contentType) if len(ret) == 0 { panic("no return value specified for Upload") } var r0 error - if returnFunc, ok := ret.Get(0).(func(vfs.File, io.ReadSeeker, string) error); ok { - r0 = returnFunc(file, content, contentType) + if returnFunc, ok := ret.Get(0).(func(context.Context, vfs.File, io.ReadSeeker, string) error); ok { + r0 = returnFunc(ctx, file, content, contentType) } else { r0 = ret.Error(0) } @@ -470,31 +513,37 @@ type Client_Upload_Call struct { } // Upload is a helper method to define mock.On call +// - ctx context.Context // - file vfs.File // - content io.ReadSeeker // - contentType string -func (_e *Client_Expecter) Upload(file interface{}, content interface{}, contentType interface{}) *Client_Upload_Call { - return &Client_Upload_Call{Call: _e.mock.On("Upload", file, content, contentType)} +func (_e *Client_Expecter) Upload(ctx interface{}, file interface{}, content interface{}, contentType interface{}) *Client_Upload_Call { + return &Client_Upload_Call{Call: _e.mock.On("Upload", ctx, file, content, contentType)} } -func (_c *Client_Upload_Call) Run(run func(file vfs.File, content io.ReadSeeker, contentType string)) *Client_Upload_Call { +func (_c *Client_Upload_Call) Run(run func(ctx context.Context, file vfs.File, content io.ReadSeeker, contentType string)) *Client_Upload_Call { _c.Call.Run(func(args mock.Arguments) { - var arg0 vfs.File + var arg0 context.Context if args[0] != nil { - arg0 = args[0].(vfs.File) + arg0 = args[0].(context.Context) } - var arg1 io.ReadSeeker + var arg1 vfs.File if args[1] != nil { - arg1 = args[1].(io.ReadSeeker) + arg1 = args[1].(vfs.File) } - var arg2 string + var arg2 io.ReadSeeker if args[2] != nil { - arg2 = args[2].(string) + arg2 = args[2].(io.ReadSeeker) + } + var arg3 string + if args[3] != nil { + arg3 = args[3].(string) } run( arg0, arg1, arg2, + arg3, ) }) return _c @@ -505,7 +554,7 @@ func (_c *Client_Upload_Call) Return(err error) *Client_Upload_Call { return _c } -func (_c *Client_Upload_Call) RunAndReturn(run func(file vfs.File, content io.ReadSeeker, contentType string) error) *Client_Upload_Call { +func (_c *Client_Upload_Call) RunAndReturn(run func(ctx context.Context, file vfs.File, content io.ReadSeeker, contentType string) error) *Client_Upload_Call { _c.Call.Return(run) return _c } diff --git a/backend/azure/newFileSystemOption.go b/backend/azure/newFileSystemOption.go index 69c9396f..2ca712aa 100644 --- a/backend/azure/newFileSystemOption.go +++ b/backend/azure/newFileSystemOption.go @@ -1,10 +1,15 @@ package azure -import "github.com/c2fo/vfs/v7/options" +import ( + "context" + + "github.com/c2fo/vfs/v7/options" +) const ( optionNameClient = "client" optionNameOptions = "options" + optionNameContext = "context" ) // WithClient returns clientSetter implementation of NewFileOption @@ -54,3 +59,27 @@ func (o *optionsOpt) Apply(fs *FileSystem) { func (o *optionsOpt) NewFileSystemOptionName() string { return optionNameOptions } + +// WithContext returns a context option implementation of NewFileOption +// +// WithContext is used to specify a context for the filesystem. +// The context is used to control the lifecycle of the filesystem. +func WithContext(ctx context.Context) options.NewFileSystemOption[FileSystem] { + return &contextOpt{ + ctx: ctx, + } +} + +type contextOpt struct { + ctx context.Context +} + +// Apply applies the context to the filesystem +func (c *contextOpt) Apply(fs *FileSystem) { + fs.ctx = c.ctx +} + +// NewFileSystemOptionName returns the name of the option +func (c *contextOpt) NewFileSystemOptionName() string { + return optionNameContext +} diff --git a/backend/azure/newFileSystemOption_test.go b/backend/azure/newFileSystemOption_test.go new file mode 100644 index 00000000..16ffe1ca --- /dev/null +++ b/backend/azure/newFileSystemOption_test.go @@ -0,0 +1,38 @@ +package azure + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWithClient(t *testing.T) { + client := &DefaultClient{} + fs := &FileSystem{} + + opt := WithClient(client) + opt.Apply(fs) + + assert.Equal(t, client, fs.client, "Client should be set correctly") +} + +func TestWithOptions(t *testing.T) { + options := Options{} + fs := &FileSystem{} + + opt := WithOptions(options) + opt.Apply(fs) + + assert.Equal(t, options, *fs.options, "Options should be set correctly") +} + +func TestWithContext(t *testing.T) { + ctx := context.Background() + fs := &FileSystem{} + + opt := WithContext(ctx) + opt.Apply(fs) + + assert.Equal(t, ctx, fs.ctx, "Context should be set correctly") +} diff --git a/backend/azure/types/types.go b/backend/azure/types/types.go index 1b966815..e891035b 100644 --- a/backend/azure/types/types.go +++ b/backend/azure/types/types.go @@ -2,6 +2,7 @@ package types import ( + "context" "io" "github.com/c2fo/vfs/v7" @@ -12,28 +13,28 @@ import ( type Client interface { // Properties should return a BlobProperties struct for the blob specified by locationURI, and filePath. If the // blob is not found an error should be returned. - Properties(locationURI, filePath string) (*BlobProperties, error) + Properties(ctx context.Context, locationURI, filePath string) (*BlobProperties, error) // SetMetadata should add the metadata specified by the parameter metadata for the blob specified by the parameter // file. - SetMetadata(file vfs.File, metadata map[string]*string) error + SetMetadata(ctx context.Context, file vfs.File, metadata map[string]*string) error // Upload should create or update the blob specified by the file parameter with the contents of the content // parameter - Upload(file vfs.File, content io.ReadSeeker, contentType string) error + Upload(ctx context.Context, file vfs.File, content io.ReadSeeker, contentType string) error // Download should return a reader for the blob specified by the file parameter - Download(file vfs.File) (io.ReadCloser, error) + Download(ctx context.Context, file vfs.File) (io.ReadCloser, error) // Copy should copy the file specified by srcFile to the file specified by tgtFile - Copy(srcFile vfs.File, tgtFile vfs.File) error + Copy(ctx context.Context, srcFile vfs.File, tgtFile vfs.File) error // List should return a listing for the specified location. Listings should include the full path for the file. - List(l vfs.Location) ([]string, error) + List(ctx context.Context, l vfs.Location) ([]string, error) // Delete should delete the file specified by the parameter file. - Delete(file vfs.File) error + Delete(ctx context.Context, file vfs.File) error // DeleteAllVersions should delete all versions of the file specified by the parameter file. - DeleteAllVersions(file vfs.File) error + DeleteAllVersions(ctx context.Context, file vfs.File) error } diff --git a/backend/ftp/file.go b/backend/ftp/file.go index 53d9fe2a..8ff10c9a 100644 --- a/backend/ftp/file.go +++ b/backend/ftp/file.go @@ -33,6 +33,7 @@ type File struct { location *Location path string opts []options.NewFileOption + ctx context.Context offset int64 } @@ -40,7 +41,7 @@ type File struct { // LastModified returns the LastModified property of ftp file. func (f *File) LastModified() (*time.Time, error) { - entry, err := f.stat(context.TODO()) + entry, err := f.stat(f.ctx) if err != nil { return nil, utils.WrapLastModifiedError(err) } @@ -90,7 +91,7 @@ func (f *File) Path() string { // Exists returns a boolean of whether or not the file exists on the ftp server func (f *File) Exists() (bool, error) { - _, err := f.stat(context.TODO()) + _, err := f.stat(f.ctx) if err != nil { if errors.Is(err, os.ErrNotExist) { // file does not exist @@ -121,7 +122,7 @@ func (f *File) Touch() error { } // if a set time function is available use that to set last modified to now - dc, err := f.location.fileSystem.DataConn(context.TODO(), f.Location().Authority(), types.SingleOp, f) + dc, err := f.location.fileSystem.DataConn(f.ctx, f.Location().Authority(), types.SingleOp, f) if err != nil { return utils.WrapTouchError(err) } @@ -149,7 +150,7 @@ func getTempFilename(origName string) string { // Size returns the size of the remote file. func (f *File) Size() (uint64, error) { - entry, err := f.stat(context.TODO()) + entry, err := f.stat(f.ctx) if err != nil { return 0, utils.WrapSizeError(err) } @@ -179,7 +180,7 @@ func (f *File) MoveToFile(t vfs.File) error { if err != nil { return utils.WrapMoveToFileError(err) } - dc, err := f.location.fileSystem.DataConn(context.TODO(), f.Location().Authority(), types.SingleOp, f) + dc, err := f.location.fileSystem.DataConn(f.ctx, f.Location().Authority(), types.SingleOp, f) if err != nil { return utils.WrapMoveToFileError(err) } @@ -296,7 +297,7 @@ func (f *File) CopyToLocation(location vfs.Location) (vfs.File, error) { // Delete removes the remote file. Error is returned, if any. func (f *File) Delete(_ ...options.DeleteOption) error { - dc, err := f.location.fileSystem.DataConn(context.TODO(), f.Location().Authority(), types.SingleOp, f) + dc, err := f.location.fileSystem.DataConn(f.ctx, f.Location().Authority(), types.SingleOp, f) if err != nil { return utils.WrapDeleteError(err) } @@ -319,7 +320,7 @@ func (f *File) Close() error { // Read calls the underlying ftp.File Read. func (f *File) Read(p []byte) (n int, err error) { - dc, err := f.location.fileSystem.DataConn(context.TODO(), f.Location().Authority(), types.OpenRead, f) + dc, err := f.location.fileSystem.DataConn(f.ctx, f.Location().Authority(), types.OpenRead, f) if err != nil { return 0, utils.WrapReadError(err) } @@ -398,7 +399,7 @@ func (f *File) Seek(offset int64, whence int) (int64, error) { } // now that f.offset has been adjusted and mode was captured, reinitialize file - _, err = f.location.fileSystem.DataConn(context.TODO(), f.Location().Authority(), mode, f) + _, err = f.location.fileSystem.DataConn(f.ctx, f.Location().Authority(), mode, f) if err != nil { return 0, utils.WrapSeekError(err) } @@ -409,7 +410,7 @@ func (f *File) Seek(offset int64, whence int) (int64, error) { // Write calls the underlying ftp.File Write. func (f *File) Write(data []byte) (res int, err error) { - dc, err := f.location.fileSystem.DataConn(context.TODO(), f.Location().Authority(), types.OpenWrite, f) + dc, err := f.location.fileSystem.DataConn(f.ctx, f.Location().Authority(), types.OpenWrite, f) if err != nil { return 0, utils.WrapWriteError(err) } diff --git a/backend/ftp/fileSystem.go b/backend/ftp/fileSystem.go index 4bc97dd4..65a3cf0d 100644 --- a/backend/ftp/fileSystem.go +++ b/backend/ftp/fileSystem.go @@ -9,6 +9,7 @@ import ( "github.com/c2fo/vfs/v7/backend" "github.com/c2fo/vfs/v7/backend/ftp/types" "github.com/c2fo/vfs/v7/options" + "github.com/c2fo/vfs/v7/options/newlocation" "github.com/c2fo/vfs/v7/utils" "github.com/c2fo/vfs/v7/utils/authority" ) @@ -28,6 +29,7 @@ var ( // FileSystem implements vfs.FileSystem for the FTP filesystem. type FileSystem struct { options Options + ctx context.Context ftpclient types.Client dataconn types.DataConn resetConn bool @@ -37,6 +39,7 @@ type FileSystem struct { func NewFileSystem(opts ...options.NewFileSystemOption[FileSystem]) *FileSystem { fs := &FileSystem{ options: Options{}, + ctx: context.Background(), } // apply options @@ -78,7 +81,7 @@ func (fs *FileSystem) NewFile(authorityStr, filePath string, opts ...options.New } // NewLocation function returns the FTP implementation of vfs.Location. -func (fs *FileSystem) NewLocation(authorityStr, locPath string) (vfs.Location, error) { +func (fs *FileSystem) NewLocation(authorityStr, locPath string, opts ...options.NewLocationOption) (vfs.Location, error) { if fs == nil { return nil, errFileSystemRequired } @@ -96,10 +99,20 @@ func (fs *FileSystem) NewLocation(authorityStr, locPath string) (vfs.Location, e return nil, err } + ctx := fs.ctx + for _, o := range opts { + switch o := o.(type) { + case *newlocation.Context: + ctx = context.Context(o) + default: + } + } + return &Location{ fileSystem: fs, path: utils.EnsureTrailingSlash(path.Clean(locPath)), authority: auth, + ctx: ctx, }, nil } diff --git a/backend/ftp/location.go b/backend/ftp/location.go index 2db13605..b58b0ba6 100644 --- a/backend/ftp/location.go +++ b/backend/ftp/location.go @@ -13,6 +13,8 @@ import ( "github.com/c2fo/vfs/v7" "github.com/c2fo/vfs/v7/backend/ftp/types" "github.com/c2fo/vfs/v7/options" + "github.com/c2fo/vfs/v7/options/newfile" + "github.com/c2fo/vfs/v7/options/newlocation" "github.com/c2fo/vfs/v7/utils" "github.com/c2fo/vfs/v7/utils/authority" ) @@ -24,13 +26,14 @@ type Location struct { fileSystem *FileSystem path string authority authority.Authority + ctx context.Context } // List calls FTP ReadDir to list all files in the location's path. // If you have many thousands of files at the given location, this could become quite expensive. func (l *Location) List() ([]string, error) { var filenames []string - dc, err := l.fileSystem.DataConn(context.TODO(), l.Authority(), types.SingleOp, nil) + dc, err := l.fileSystem.DataConn(l.ctx, l.Authority(), types.SingleOp, nil) if err != nil { return filenames, err } @@ -88,7 +91,7 @@ func (l *Location) ListByPrefix(prefix string) ([]string, error) { } // get dataconn - dc, err := l.fileSystem.DataConn(context.TODO(), l.Authority(), types.SingleOp, nil) + dc, err := l.fileSystem.DataConn(l.ctx, l.Authority(), types.SingleOp, nil) if err != nil { return filenames, err } @@ -152,7 +155,7 @@ func (l *Location) Path() string { // Exists returns true if the remote FTP directory exists. func (l *Location) Exists() (bool, error) { - dc, err := l.fileSystem.DataConn(context.TODO(), l.Authority(), types.SingleOp, nil) + dc, err := l.fileSystem.DataConn(l.ctx, l.Authority(), types.SingleOp, nil) if err != nil { return false, err } @@ -182,7 +185,7 @@ func (l *Location) Exists() (bool, error) { // NewLocation makes a copy of the underlying Location, then modifies its path by calling ChangeDir with the // relativePath argument, returning the resulting location. The only possible errors come from the call to // ChangeDir, which, for the FTP implementation doesn't ever result in an error. -func (l *Location) NewLocation(relativePath string) (vfs.Location, error) { +func (l *Location) NewLocation(relativePath string, opts ...options.NewLocationOption) (vfs.Location, error) { if l == nil { return nil, errLocationRequired } @@ -191,10 +194,20 @@ func (l *Location) NewLocation(relativePath string) (vfs.Location, error) { return nil, err } + ctx := l.ctx + for _, o := range opts { + switch o := o.(type) { + case *newlocation.Context: + ctx = context.Context(o) + default: + } + } + return &Location{ fileSystem: l.fileSystem, path: path.Join(l.path, relativePath), authority: l.Authority(), + ctx: ctx, }, nil } @@ -235,10 +248,20 @@ func (l *Location) NewFile(relFilePath string, opts ...options.NewFileOption) (v return nil, err } + ctx := l.ctx + for _, o := range opts { + switch o := o.(type) { + case *newfile.Context: + ctx = context.Context(o) + default: + } + } + return &File{ location: newLocation.(*Location), path: path.Join(l.Path(), relFilePath), opts: opts, + ctx: ctx, }, nil } diff --git a/backend/ftp/newFileSystemOption.go b/backend/ftp/newFileSystemOption.go index b1cdd998..36b4a0e2 100644 --- a/backend/ftp/newFileSystemOption.go +++ b/backend/ftp/newFileSystemOption.go @@ -1,6 +1,8 @@ package ftp import ( + "context" + "github.com/c2fo/vfs/v7/backend/ftp/types" "github.com/c2fo/vfs/v7/options" ) @@ -8,6 +10,7 @@ import ( const ( optionNameFTPClient = "ftpclient" optionNameOptions = "options" + optionNameContext = "context" optionNameDataConn = "dataconn" ) @@ -59,6 +62,30 @@ func (o *optionsOpt) NewFileSystemOptionName() string { return optionNameOptions } +// WithContext returns a context option implementation of NewFileOption +// +// WithContext is used to specify a context for the filesystem. +// The context is used to control the lifecycle of the filesystem. +func WithContext(ctx context.Context) options.NewFileSystemOption[FileSystem] { + return &contextOpt{ + ctx: ctx, + } +} + +type contextOpt struct { + ctx context.Context +} + +// Apply applies the context to the filesystem +func (c *contextOpt) Apply(fs *FileSystem) { + fs.ctx = c.ctx +} + +// NewFileSystemOptionName returns the name of the option +func (c *contextOpt) NewFileSystemOptionName() string { + return optionNameContext +} + // WithDataConn returns dataconnOpt implementation of NewFileOption // // WithDataConn is used to specify a DataConn to use for the filesystem. diff --git a/backend/ftp/newFileSystemOption_test.go b/backend/ftp/newFileSystemOption_test.go new file mode 100644 index 00000000..08bce8a3 --- /dev/null +++ b/backend/ftp/newFileSystemOption_test.go @@ -0,0 +1,39 @@ +package ftp + +import ( + "context" + "testing" + + "github.com/jlaffaye/ftp" + "github.com/stretchr/testify/assert" +) + +func TestWithClient(t *testing.T) { + client := &ftp.ServerConn{} + fs := &FileSystem{} + + opt := WithClient(client) + opt.Apply(fs) + + assert.Equal(t, client, fs.ftpclient, "Client should be set correctly") +} + +func TestWithOptions(t *testing.T) { + options := Options{} + fs := &FileSystem{} + + opt := WithOptions(options) + opt.Apply(fs) + + assert.Equal(t, options, fs.options, "Options should be set correctly") +} + +func TestWithContext(t *testing.T) { + ctx := context.Background() + fs := &FileSystem{} + + opt := WithContext(ctx) + opt.Apply(fs) + + assert.Equal(t, ctx, fs.ctx, "Context should be set correctly") +} diff --git a/backend/gs/file.go b/backend/gs/file.go index 6cc3b807..cc08f269 100644 --- a/backend/gs/file.go +++ b/backend/gs/file.go @@ -27,6 +27,7 @@ type File struct { // bucket string key string opts []options.NewFileOption + ctx context.Context // seek-related fields cursorPos int64 @@ -102,7 +103,7 @@ func (f *File) tempToGCS() error { return err } - w := handle.NewWriter(f.location.fileSystem.ctx) + w := handle.NewWriter(f.ctx) defer func() { _ = w.Close() }() for _, o := range f.opts { @@ -183,7 +184,7 @@ func (f *File) getReader() (io.ReadCloser, error) { } // get range reader (from current cursor position to end of file) - reader, err := h.NewRangeReader(f.location.fileSystem.ctx, f.cursorPos, -1) + reader, err := h.NewRangeReader(f.ctx, f.cursorPos, -1) if err != nil { return nil, err } @@ -319,7 +320,7 @@ func (f *File) initWriters() error { if f.gcsWriter == nil { if !f.seekCalled && !f.readCalled { // setup cancelable context - ctx, cancel := context.WithCancel(f.location.fileSystem.ctx) + ctx, cancel := context.WithCancel(f.ctx) f.cancelFunc = cancel // get object handle @@ -495,7 +496,7 @@ func (f *File) Delete(opts ...options.DeleteOption) error { if err != nil { return utils.WrapDeleteError(err) } - err = handle.Delete(f.location.fileSystem.ctx) + err = handle.Delete(f.ctx) if err != nil { return utils.WrapDeleteError(err) } @@ -506,7 +507,7 @@ func (f *File) Delete(opts ...options.DeleteOption) error { return utils.WrapDeleteError(err) } for _, handle := range handles { - err := handle.Delete(f.location.fileSystem.ctx) + err := handle.Delete(f.ctx) if err != nil { return utils.WrapDeleteError(err) } @@ -568,7 +569,7 @@ func (f *File) updateLastModifiedByAttrUpdate() error { return err } - cctx, cancel := context.WithCancel(f.location.fileSystem.ctx) + cctx, cancel := context.WithCancel(f.ctx) defer cancel() _, err = obj.Update(cctx, updateAttrs) @@ -591,7 +592,7 @@ func (f *File) isBucketVersioningEnabled() (bool, error) { if err != nil { return false, err } - cctx, cancel := context.WithCancel(f.location.fileSystem.ctx) + cctx, cancel := context.WithCancel(f.ctx) defer cancel() attrs, err := client.Bucket(f.Location().Authority().String()).Attrs(cctx) if err != nil { @@ -607,7 +608,7 @@ func (f *File) createEmptyFile() error { } // write zero length file. - ctx, cancel := context.WithCancel(f.location.fileSystem.ctx) + ctx, cancel := context.WithCancel(f.ctx) defer cancel() w := handle.NewWriter(ctx) @@ -687,7 +688,7 @@ func (f *File) copyToLocalTempReader(tmpFile *os.File) error { return err } - outputReader, err := handle.NewReader(f.location.fileSystem.ctx) + outputReader, err := handle.NewReader(f.ctx) if err != nil { return err } @@ -731,7 +732,7 @@ func (f *File) getObjectGenerationHandles() ([]*storage.ObjectHandle, error) { return nil, err } it := client.Bucket(f.Location().Authority().String()). - Objects(f.location.fileSystem.ctx, &storage.Query{Versions: true, Prefix: utils.RemoveLeadingSlash(f.key)}) + Objects(f.ctx, &storage.Query{Versions: true, Prefix: utils.RemoveLeadingSlash(f.key)}) for { attrs, err := it.Next() @@ -753,7 +754,7 @@ func (f *File) getObjectAttrs() (*storage.ObjectAttrs, error) { if err != nil { return nil, err } - return handle.Attrs(f.location.fileSystem.ctx) + return handle.Attrs(f.ctx) } func (f *File) copyWithinGCSToFile(targetFile *File) error { @@ -774,6 +775,6 @@ func (f *File) copyWithinGCSToFile(targetFile *File) error { copier.ContentType(attrs.ContentType) // Just copy content. - _, cerr := copier.Run(f.location.fileSystem.ctx) + _, cerr := copier.Run(f.ctx) return cerr } diff --git a/backend/gs/fileSystem.go b/backend/gs/fileSystem.go index 6f79c497..58c374c8 100644 --- a/backend/gs/fileSystem.go +++ b/backend/gs/fileSystem.go @@ -11,6 +11,7 @@ import ( "github.com/c2fo/vfs/v7" "github.com/c2fo/vfs/v7/backend" "github.com/c2fo/vfs/v7/options" + "github.com/c2fo/vfs/v7/options/newlocation" "github.com/c2fo/vfs/v7/utils" "github.com/c2fo/vfs/v7/utils/authority" ) @@ -84,7 +85,7 @@ func (fs *FileSystem) NewFile(authorityStr, filePath string, opts ...options.New } // NewLocation function returns the GCS implementation of vfs.Location. -func (fs *FileSystem) NewLocation(authorityStr, locPath string) (loc vfs.Location, err error) { +func (fs *FileSystem) NewLocation(authorityStr, locPath string, opts ...options.NewLocationOption) (loc vfs.Location, err error) { if fs == nil { return nil, errFileSystemRequired } @@ -102,10 +103,20 @@ func (fs *FileSystem) NewLocation(authorityStr, locPath string) (loc vfs.Locatio return nil, err } + ctx := fs.ctx + for _, o := range opts { + switch o := o.(type) { + case *newlocation.Context: + ctx = context.Context(o) + default: + } + } + return &Location{ fileSystem: fs, prefix: utils.EnsureTrailingSlash(path.Clean(locPath)), authority: auth, + ctx: ctx, }, nil } diff --git a/backend/gs/file_test.go b/backend/gs/file_test.go index 60da6066..bb508ab2 100644 --- a/backend/gs/file_test.go +++ b/backend/gs/file_test.go @@ -2,7 +2,6 @@ package gs import ( "bytes" - "context" "errors" "fmt" "io" @@ -21,9 +20,9 @@ type fileTestSuite struct { suite.Suite } -func objectExists(ctx context.Context, bucket *storage.BucketHandle, objectName string) bool { +func (ts *fileTestSuite) objectExists(bucket *storage.BucketHandle, objectName string) bool { objectHandle := bucket.Object(objectName) - _, err := objectHandle.Attrs(ctx) + _, err := objectHandle.Attrs(ts.T().Context()) if err != nil { if errors.Is(err, storage.ErrObjectNotExist) { return false @@ -33,9 +32,9 @@ func objectExists(ctx context.Context, bucket *storage.BucketHandle, objectName return true } -func mustReadObject(ctx context.Context, bucket *storage.BucketHandle, objectName string) []byte { +func (ts *fileTestSuite) mustReadObject(bucket *storage.BucketHandle, objectName string) []byte { objectHandle := bucket.Object(objectName) - reader, err := objectHandle.NewReader(ctx) + reader, err := objectHandle.NewReader(ts.T().Context()) if err != nil { panic(err) } @@ -138,7 +137,7 @@ func (ts *fileTestSuite) TestDelete() { ts.Require().NoError(err, "Shouldn't fail deleting the file") bucket := client.Bucket(bucketName) - ts.False(objectExists(ts.T().Context(), bucket, objectName)) + ts.False(ts.objectExists(bucket, objectName)) } func (ts *fileTestSuite) TestDeleteError() { @@ -202,7 +201,7 @@ func (ts *fileTestSuite) TestDeleteRemoveAllVersions() { ts.Require().NoError(err, "Shouldn't fail deleting the file") bucket := client.Bucket(bucketName) - ts.False(objectExists(ts.T().Context(), bucket, objectName)) + ts.False(ts.objectExists(bucket, objectName)) handles, err = f.getObjectGenerationHandles() ts.Require().NoError(err, "Shouldn't fail getting object generation handles") ts.Nil(handles) @@ -379,14 +378,12 @@ func (ts *fileTestSuite) TestMoveAndCopy() { sourceBucket := client.Bucket(sourceBucketName) targetBucket := client.Bucket(targetBucketName) - ctx := ts.T().Context() - - ts.True(objectExists(ctx, sourceBucket, sourceName), "source should exist") + ts.True(ts.objectExists(sourceBucket, sourceName), "source should exist") ts.True(fsFileNameExists(fs, sourceBucketName, sourceName), "source should exist") - ts.Equal(content, mustReadObject(ctx, sourceBucket, sourceName)) + ts.Equal(content, ts.mustReadObject(sourceBucket, sourceName)) ts.Equal(content, fsMustReadFileName(fs, sourceBucketName, sourceName)) - ts.False(objectExists(ctx, targetBucket, targetName), "target should not exist") + ts.False(ts.objectExists(targetBucket, targetName), "target should not exist") ts.False(fsFileNameExists(fs, sourceBucketName, targetName), "target should not exist") sourceFile, err := fs.NewFile(sourceBucketName, "/"+sourceName) @@ -411,18 +408,18 @@ func (ts *fileTestSuite) TestMoveAndCopy() { ts.Require().NoError(err, "Error shouldn't be returned from successful operation") if testCase.move { - ts.False(objectExists(ctx, sourceBucket, sourceName), "source should not exist") + ts.False(ts.objectExists(sourceBucket, sourceName), "source should not exist") ts.False(fsFileNameExists(fs, sourceBucketName, sourceName), "source should not exist") } else { - ts.True(objectExists(ctx, sourceBucket, sourceName), "source should exist") + ts.True(ts.objectExists(sourceBucket, sourceName), "source should exist") ts.True(fsFileNameExists(fs, sourceBucketName, sourceName), "source should exist") - ts.Equal(content, mustReadObject(ctx, sourceBucket, sourceName)) + ts.Equal(content, ts.mustReadObject(sourceBucket, sourceName)) ts.Equal(content, fsMustReadFileName(fs, sourceBucketName, sourceName)) } - ts.True(objectExists(ctx, targetBucket, targetName), "target should exist") + ts.True(ts.objectExists(targetBucket, targetName), "target should exist") ts.True(fsFileNameExists(fs, targetBucketName, targetName), "target should exist") - ts.Equal(content, mustReadObject(ctx, targetBucket, targetName)) + ts.Equal(content, ts.mustReadObject(targetBucket, targetName)) ts.Equal(content, fsMustReadFileName(fs, targetBucketName, targetName)) } }) @@ -482,14 +479,12 @@ func (ts *fileTestSuite) TestMoveAndCopyBuffered() { sourceBucket := client.Bucket(sourceBucketName) targetBucket := client.Bucket(targetBucketName) - ctx := ts.T().Context() - - ts.True(objectExists(ctx, sourceBucket, sourceName), "source should exist") + ts.True(ts.objectExists(sourceBucket, sourceName), "source should exist") ts.True(fsFileNameExists(fs, sourceBucketName, sourceName), "source should exist") - ts.Equal(content, mustReadObject(ctx, sourceBucket, sourceName)) + ts.Equal(content, ts.mustReadObject(sourceBucket, sourceName)) ts.Equal(content, fsMustReadFileName(fs, sourceBucketName, sourceName)) - ts.False(objectExists(ctx, targetBucket, targetName), "target should not exist") + ts.False(ts.objectExists(targetBucket, targetName), "target should not exist") ts.False(fsFileNameExists(fs, sourceBucketName, targetName), "target should not exist") sourceFile, err := fs.NewFile(sourceBucketName, "/"+sourceName) @@ -514,18 +509,18 @@ func (ts *fileTestSuite) TestMoveAndCopyBuffered() { ts.Require().NoError(err, "Error shouldn't be returned from successful operation") if testCase.move { - ts.False(objectExists(ctx, sourceBucket, sourceName), "source should not exist") + ts.False(ts.objectExists(sourceBucket, sourceName), "source should not exist") ts.False(fsFileNameExists(fs, sourceBucketName, sourceName), "source should not exist") } else { - ts.True(objectExists(ctx, sourceBucket, sourceName), "source should exist") + ts.True(ts.objectExists(sourceBucket, sourceName), "source should exist") ts.True(fsFileNameExists(fs, sourceBucketName, sourceName), "source should exist") - ts.Equal(content, mustReadObject(ctx, sourceBucket, sourceName)) + ts.Equal(content, ts.mustReadObject(sourceBucket, sourceName)) ts.Equal(content, fsMustReadFileName(fs, sourceBucketName, sourceName)) } - ts.True(objectExists(ctx, targetBucket, targetName), "target should exist") + ts.True(ts.objectExists(targetBucket, targetName), "target should exist") ts.True(fsFileNameExists(fs, targetBucketName, targetName), "target should exist") - ts.Equal(content, mustReadObject(ctx, targetBucket, targetName)) + ts.Equal(content, ts.mustReadObject(targetBucket, targetName)) ts.Equal(content, fsMustReadFileName(fs, targetBucketName, targetName)) } }) diff --git a/backend/gs/location.go b/backend/gs/location.go index 197ca0ad..700c3ad9 100644 --- a/backend/gs/location.go +++ b/backend/gs/location.go @@ -1,6 +1,7 @@ package gs import ( + "context" "errors" "path" "regexp" @@ -11,6 +12,8 @@ import ( "github.com/c2fo/vfs/v7" "github.com/c2fo/vfs/v7/options" + "github.com/c2fo/vfs/v7/options/newfile" + "github.com/c2fo/vfs/v7/options/newlocation" "github.com/c2fo/vfs/v7/utils" "github.com/c2fo/vfs/v7/utils/authority" ) @@ -26,6 +29,7 @@ type Location struct { prefix string bucketHandle BucketHandleWrapper authority authority.Authority + ctx context.Context } // String returns the full URI of the location. @@ -68,7 +72,7 @@ func (l *Location) ListByPrefix(filenamePrefix string) ([]string, error) { } var fileNames []string - it := handle.WrappedObjects(l.fileSystem.ctx, q) + it := handle.WrappedObjects(l.ctx, q) for { objAttrs, err := it.Next() if err != nil { @@ -135,7 +139,7 @@ func (l *Location) Exists() (bool, error) { } // NewLocation creates a new location instance relative to the current location's path. -func (l *Location) NewLocation(relativePath string) (vfs.Location, error) { +func (l *Location) NewLocation(relativePath string, opts ...options.NewLocationOption) (vfs.Location, error) { if l == nil { return nil, errLocationRequired } @@ -148,10 +152,20 @@ func (l *Location) NewLocation(relativePath string) (vfs.Location, error) { return nil, err } + ctx := l.ctx + for _, o := range opts { + switch o := o.(type) { + case *newlocation.Context: + ctx = context.Context(o) + default: + } + } + return &Location{ fileSystem: l.fileSystem, prefix: path.Join(l.prefix, relativePath), authority: l.Authority(), + ctx: ctx, }, nil } @@ -208,10 +222,20 @@ func (l *Location) NewFile(relFilePath string, opts ...options.NewFileOption) (v return nil, err } + ctx := l.ctx + for _, o := range opts { + switch o := o.(type) { + case *newfile.Context: + ctx = context.Context(o) + default: + } + } + return &File{ location: newLocation.(*Location), key: utils.EnsureLeadingSlash(path.Join(l.prefix, relFilePath)), opts: opts, + ctx: ctx, }, nil } @@ -252,5 +276,5 @@ func (l *Location) getBucketAttrs() (*storage.BucketAttrs, error) { return nil, err } - return handle.Attrs(l.fileSystem.ctx) + return handle.Attrs(l.ctx) } diff --git a/backend/mem/fileSystem.go b/backend/mem/fileSystem.go index b5520f86..8425d323 100644 --- a/backend/mem/fileSystem.go +++ b/backend/mem/fileSystem.go @@ -96,7 +96,7 @@ func (fs *FileSystem) NewFile(volume, absFilePath string, opts ...options.NewFil // NewLocation function returns the in-memory implementation of vfs.Location. // A location always exists. If a file is created on a location that has not yet // been made in the fsMap, then the location will be created with the file -func (fs *FileSystem) NewLocation(volume, absLocPath string) (vfs.Location, error) { +func (fs *FileSystem) NewLocation(volume, absLocPath string, _ ...options.NewLocationOption) (vfs.Location, error) { err := utils.ValidateAbsoluteLocationPath(absLocPath) if err != nil { return nil, err diff --git a/backend/mem/location.go b/backend/mem/location.go index c9e3e60d..7cafa5e1 100644 --- a/backend/mem/location.go +++ b/backend/mem/location.go @@ -115,7 +115,7 @@ func (l *Location) Exists() (bool, error) { // NewLocation creates a new location at the // given relative path, which is tagged onto the current locations absolute path -func (l *Location) NewLocation(relLocPath string) (vfs.Location, error) { +func (l *Location) NewLocation(relLocPath string, _ ...options.NewLocationOption) (vfs.Location, error) { err := utils.ValidateRelativeLocationPath(relLocPath) if err != nil { return nil, err diff --git a/backend/os/fileSystem.go b/backend/os/fileSystem.go index c3ab795c..038a3e46 100644 --- a/backend/os/fileSystem.go +++ b/backend/os/fileSystem.go @@ -64,7 +64,7 @@ func (fs *FileSystem) NewFile(authorityStr, filePath string, opts ...options.New } // NewLocation function returns the os implementation of vfs.Location. -func (fs *FileSystem) NewLocation(authorityStr, locPath string) (vfs.Location, error) { +func (fs *FileSystem) NewLocation(authorityStr, locPath string, _ ...options.NewLocationOption) (vfs.Location, error) { if runtime.GOOS == "windows" && filepath.IsAbs(locPath) { if v := filepath.VolumeName(locPath); v != "" { authorityStr = v diff --git a/backend/os/location.go b/backend/os/location.go index b286c76c..21720b0d 100644 --- a/backend/os/location.go +++ b/backend/os/location.go @@ -167,7 +167,7 @@ func (l *Location) String() string { // NewLocation makes a copy of the underlying Location, then modifies its path by calling ChangeDir with the // relativePath argument, returning the resulting location. The only possible errors come from the call to // ChangeDir. -func (l *Location) NewLocation(relativePath string) (vfs.Location, error) { +func (l *Location) NewLocation(relativePath string, _ ...options.NewLocationOption) (vfs.Location, error) { if l == nil { return nil, errLocationRequired } diff --git a/backend/s3/file.go b/backend/s3/file.go index 44860723..1e8d1838 100644 --- a/backend/s3/file.go +++ b/backend/s3/file.go @@ -30,6 +30,7 @@ type File struct { location *Location key string opts []options.NewFileOption + ctx context.Context // seek-related fields cursorPos int64 @@ -132,7 +133,7 @@ func (f *File) CopyToFile(file vfs.File) (err error) { if err != nil { return utils.WrapCopyToFileError(err) } - _, err = client.CopyObject(context.Background(), input) + _, err = client.CopyObject(f.ctx, input) if err != nil { return utils.WrapCopyToFileError(err) } @@ -217,7 +218,7 @@ func (f *File) Delete(opts ...options.DeleteOption) error { } bucket := f.Location().Authority().String() - _, err = client.DeleteObject(context.Background(), &s3.DeleteObjectInput{ + _, err = client.DeleteObject(f.ctx, &s3.DeleteObjectInput{ Key: &f.key, Bucket: &bucket, }) @@ -232,7 +233,7 @@ func (f *File) Delete(opts ...options.DeleteOption) error { } for idx := range objectVersions.Versions { - if _, err = client.DeleteObject(context.Background(), &s3.DeleteObjectInput{ + if _, err = client.DeleteObject(f.ctx, &s3.DeleteObjectInput{ Key: &f.key, Bucket: &bucket, VersionId: objectVersions.Versions[idx].VersionId, @@ -337,7 +338,7 @@ func (f *File) tempToS3() error { uploadInput := uploadInput(f) uploadInput.Body = f.tempFileWriter - _, err = uploader.Upload(context.Background(), uploadInput) + _, err = uploader.Upload(f.ctx, uploadInput) if err != nil { return err } @@ -533,7 +534,7 @@ Private helper functions func (f *File) getAllObjectVersions(client Client) (*s3.ListObjectVersionsOutput, error) { prefix := utils.RemoveLeadingSlash(f.key) bucket := f.Location().Authority().String() - objVers, err := client.ListObjectVersions(context.Background(), &s3.ListObjectVersionsInput{ + objVers, err := client.ListObjectVersions(f.ctx, &s3.ListObjectVersionsInput{ Bucket: &bucket, Prefix: &prefix, }) @@ -551,7 +552,7 @@ func (f *File) getHeadObject() (*s3.HeadObjectOutput, error) { return nil, err } - head, err := client.HeadObject(context.Background(), headObjectInput) + head, err := client.HeadObject(f.ctx, headObjectInput) return head, handleExistsError(err) } @@ -639,7 +640,7 @@ func (f *File) copyS3ToLocalTempReader(tmpFile *os.File) error { } opt := withDownloadPartitionSize(f.getDownloadPartitionSize()) _, err = manager.NewDownloader(client, opt). - Download(context.Background(), tmpFile, input) + Download(f.ctx, tmpFile, input) return err } @@ -736,7 +737,7 @@ func (f *File) getReader() (io.ReadCloser, error) { } // Request the object - result, err := client.GetObject(context.Background(), input) + result, err := client.GetObject(f.ctx, input) if err != nil { return nil, err } @@ -807,7 +808,7 @@ func (f *File) getS3Writer() (*io.PipeWriter, error) { return nil, err } uploader := manager.NewUploader(client, withUploadPartitionSize(f.getUploadPartitionSize())) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(f.ctx) f.cancelFunc = cancel uploadInput := uploadInput(f) uploadInput.Body = pr diff --git a/backend/s3/fileSystem.go b/backend/s3/fileSystem.go index 1818fd1e..64a27459 100644 --- a/backend/s3/fileSystem.go +++ b/backend/s3/fileSystem.go @@ -2,12 +2,14 @@ package s3 import ( + "context" "errors" "path" "github.com/c2fo/vfs/v7" "github.com/c2fo/vfs/v7/backend" "github.com/c2fo/vfs/v7/options" + "github.com/c2fo/vfs/v7/options/newlocation" "github.com/c2fo/vfs/v7/utils" "github.com/c2fo/vfs/v7/utils/authority" ) @@ -25,12 +27,14 @@ var ( type FileSystem struct { client Client options Options + ctx context.Context } // NewFileSystem initializer for FileSystem struct accepts aws-sdk client and returns Filesystem or error. func NewFileSystem(opts ...options.NewFileSystemOption[FileSystem]) *FileSystem { fs := &FileSystem{ options: Options{}, + ctx: context.Background(), } options.ApplyOptions(fs, opts...) @@ -72,7 +76,7 @@ func (fs *FileSystem) NewFile(authorityStr, name string, opts ...options.NewFile } // NewLocation function returns the s3 implementation of vfs.Location. -func (fs *FileSystem) NewLocation(authorityStr, name string) (vfs.Location, error) { +func (fs *FileSystem) NewLocation(authorityStr, name string, opts ...options.NewLocationOption) (vfs.Location, error) { if fs == nil { return nil, errFileSystemRequired } @@ -90,10 +94,20 @@ func (fs *FileSystem) NewLocation(authorityStr, name string) (vfs.Location, erro return nil, err } + ctx := fs.ctx + for _, o := range opts { + switch o := o.(type) { + case *newlocation.Context: + ctx = context.Context(o) + default: + } + } + return &Location{ fileSystem: fs, prefix: utils.EnsureTrailingSlash(path.Clean(name)), authority: auth, + ctx: ctx, }, nil } @@ -112,7 +126,7 @@ func (fs *FileSystem) Scheme() string { func (fs *FileSystem) Client() (Client, error) { if fs.client == nil { var err error - fs.client, err = GetClient(fs.options) + fs.client, err = GetClient(fs.ctx, fs.options) if err != nil { return nil, err } diff --git a/backend/s3/fileSystem_test.go b/backend/s3/fileSystem_test.go index ed41b767..d69b4357 100644 --- a/backend/s3/fileSystem_test.go +++ b/backend/s3/fileSystem_test.go @@ -101,7 +101,7 @@ func (ts *fileSystemTestSuite) TestClient() { ts.Require().NoError(err, "no error") ts.Equal(s3fs.client, client, "client was already set") - s3fs = &FileSystem{} + s3fs = &FileSystem{ctx: ts.T().Context()} client, err = s3fs.Client() ts.Require().NoError(err, "no error") ts.NotNil(client, "client was set") diff --git a/backend/s3/file_test.go b/backend/s3/file_test.go index f7fdd2a0..4b1d5382 100644 --- a/backend/s3/file_test.go +++ b/backend/s3/file_test.go @@ -44,7 +44,7 @@ func (ts *fileTestSuite) SetupTest() { var err error s3cliMock = mocks.NewClient(ts.T()) defaultOptions = Options{AccessKeyID: "abc"} - fs = FileSystem{client: s3cliMock, options: defaultOptions} + fs = FileSystem{client: s3cliMock, options: defaultOptions, ctx: ts.T().Context()} testFileName = "/some/path/to/file.txt" bucket = "bucket" testFile, err = fs.NewFile(bucket, testFileName) @@ -473,6 +473,7 @@ func (ts *fileTestSuite) TestTouch() { authority: auth, }, key: "/new/file/path/hello.txt", + ctx: ts.T().Context(), } terr := file.Touch() @@ -493,6 +494,7 @@ func (ts *fileTestSuite) TestTouch() { authority: auth, }, key: "/new/file/path/hello.txt", + ctx: ts.T().Context(), } s3Mock2.EXPECT().PutObject(matchContext, mock.IsType((*s3.PutObjectInput)(nil)), mock.Anything, mock.Anything). @@ -748,6 +750,7 @@ func (ts *fileTestSuite) TestCloseWithWrite() { authority: auth, }, key: "/new/file/path/hello.txt", + ctx: ts.T().Context(), } contents := []byte("Hello world!") _, err = file.Write(contents) @@ -797,6 +800,7 @@ func (ts *fileTestSuite) TestWriteOperations() { authority: auth, }, key: "/new/file/path/hello.txt", + ctx: ts.T().Context(), } }, actions: []func(*File) error{ @@ -826,6 +830,7 @@ func (ts *fileTestSuite) TestWriteOperations() { authority: auth, }, key: "/new/file/path/hello.txt", + ctx: ts.T().Context(), } }, actions: []func(*File) error{ @@ -859,6 +864,7 @@ func (ts *fileTestSuite) TestWriteOperations() { authority: auth, }, key: "/new/file/path/hello.txt", + ctx: ts.T().Context(), } }, actions: []func(*File) error{ diff --git a/backend/s3/location.go b/backend/s3/location.go index e11beebd..c59e4bc9 100644 --- a/backend/s3/location.go +++ b/backend/s3/location.go @@ -13,6 +13,8 @@ import ( "github.com/c2fo/vfs/v7" "github.com/c2fo/vfs/v7/options" + "github.com/c2fo/vfs/v7/options/newfile" + "github.com/c2fo/vfs/v7/options/newlocation" "github.com/c2fo/vfs/v7/utils" "github.com/c2fo/vfs/v7/utils/authority" ) @@ -27,6 +29,7 @@ type Location struct { fileSystem *FileSystem prefix string authority authority.Authority + ctx context.Context } // List calls the s3 API to list all objects in the location's bucket, with a prefix automatically @@ -94,7 +97,7 @@ func (l *Location) Exists() (bool, error) { if err != nil { return false, err } - _, err = client.HeadBucket(context.Background(), headBucketInput) + _, err = client.HeadBucket(l.ctx, headBucketInput) if err != nil { var terr *types.NotFound if errors.As(err, &terr) { @@ -109,7 +112,7 @@ func (l *Location) Exists() (bool, error) { // NewLocation makes a copy of the underlying Location, then modifies its path by calling ChangeDir with the // relativePath argument, returning the resulting location. The only possible errors come from the call to // ChangeDir, which, for the s3 implementation doesn't ever result in an error. -func (l *Location) NewLocation(relativePath string) (vfs.Location, error) { +func (l *Location) NewLocation(relativePath string, opts ...options.NewLocationOption) (vfs.Location, error) { if l == nil { return nil, errLocationRequired } @@ -122,10 +125,20 @@ func (l *Location) NewLocation(relativePath string) (vfs.Location, error) { return nil, err } + ctx := l.ctx + for _, o := range opts { + switch o := o.(type) { + case *newlocation.Context: + ctx = context.Context(o) + default: + } + } + return &Location{ fileSystem: l.fileSystem, prefix: path.Join(l.prefix, relativePath), authority: l.Authority(), + ctx: ctx, }, nil } @@ -179,10 +192,20 @@ func (l *Location) NewFile(relFilePath string, opts ...options.NewFileOption) (v return nil, err } + ctx := l.ctx + for _, o := range opts { + switch o := o.(type) { + case *newfile.Context: + ctx = context.Context(o) + default: + } + } + newFile := &File{ location: newLocation.(*Location), key: utils.RemoveLeadingSlash(path.Join(l.prefix, relFilePath)), opts: opts, + ctx: ctx, } return newFile, nil } @@ -223,7 +246,7 @@ func (l *Location) fullLocationList(input *s3.ListObjectsInput, prefix string) ( return keys, err } for { - listObjectsOutput, err := client.ListObjects(context.Background(), input) + listObjectsOutput, err := client.ListObjects(l.ctx, input) if err != nil { return []string{}, err } diff --git a/backend/s3/newFileSystemOption.go b/backend/s3/newFileSystemOption.go index 63d1d732..f47e93af 100644 --- a/backend/s3/newFileSystemOption.go +++ b/backend/s3/newFileSystemOption.go @@ -1,10 +1,15 @@ package s3 -import "github.com/c2fo/vfs/v7/options" +import ( + "context" + + "github.com/c2fo/vfs/v7/options" +) const ( optionNameClient = "client" optionNameOptions = "options" + optionNameContext = "context" ) // WithClient returns clientOpt implementation of NewFileOption @@ -54,3 +59,27 @@ func (o *optionsOpt) Apply(fs *FileSystem) { func (o *optionsOpt) NewFileSystemOptionName() string { return optionNameOptions } + +// WithContext returns a context option implementation of NewFileOption +// +// WithContext is used to specify a context for the filesystem. +// The context is used to control the lifecycle of the filesystem. +func WithContext(ctx context.Context) options.NewFileSystemOption[FileSystem] { + return &contextOpt{ + ctx: ctx, + } +} + +type contextOpt struct { + ctx context.Context +} + +// Apply applies the context to the filesystem +func (c *contextOpt) Apply(fs *FileSystem) { + fs.ctx = c.ctx +} + +// NewFileSystemOptionName returns the name of the option +func (c *contextOpt) NewFileSystemOptionName() string { + return optionNameContext +} diff --git a/backend/s3/newFileSystemOption_test.go b/backend/s3/newFileSystemOption_test.go new file mode 100644 index 00000000..952dfd8e --- /dev/null +++ b/backend/s3/newFileSystemOption_test.go @@ -0,0 +1,39 @@ +package s3 + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/assert" +) + +func TestWithClient(t *testing.T) { + client := &s3.Client{} + fs := &FileSystem{} + + opt := WithClient(client) + opt.Apply(fs) + + assert.Equal(t, client, fs.client, "Client should be set correctly") +} + +func TestWithOptions(t *testing.T) { + options := Options{} + fs := &FileSystem{} + + opt := WithOptions(options) + opt.Apply(fs) + + assert.Equal(t, options, fs.options, "Options should be set correctly") +} + +func TestWithContext(t *testing.T) { + ctx := context.Background() + fs := &FileSystem{} + + opt := WithContext(ctx) + opt.Apply(fs) + + assert.Equal(t, ctx, fs.ctx, "Context should be set correctly") +} diff --git a/backend/s3/options.go b/backend/s3/options.go index fc8f7579..c817ef85 100644 --- a/backend/s3/options.go +++ b/backend/s3/options.go @@ -34,9 +34,9 @@ type Options struct { } // GetClient setup S3 client -func GetClient(opt Options) (*s3.Client, error) { +func GetClient(ctx context.Context, opt Options) (*s3.Client, error) { // setup default config - awsConfig, err := config.LoadDefaultConfig(context.Background()) + awsConfig, err := config.LoadDefaultConfig(ctx) if err != nil { return nil, err } diff --git a/backend/s3/options_test.go b/backend/s3/options_test.go index 7cef0be2..aaf7fab8 100644 --- a/backend/s3/options_test.go +++ b/backend/s3/options_test.go @@ -103,7 +103,7 @@ func (o *optionsTestSuite) TestGetClient() { for k, v := range tt.envVar { o.T().Setenv(k, v) } - client, err := GetClient(tt.opts) + client, err := GetClient(o.T().Context(), tt.opts) tt.expected(o, client, err) for k := range tt.envVar { _ = os.Unsetenv(k) diff --git a/backend/sftp/fileSystem.go b/backend/sftp/fileSystem.go index 45528f92..2574c5d7 100644 --- a/backend/sftp/fileSystem.go +++ b/backend/sftp/fileSystem.go @@ -84,7 +84,7 @@ func (fs *FileSystem) NewFile(authorityStr, filePath string, opts ...options.New } // NewLocation function returns the SFTP implementation of vfs.Location. -func (fs *FileSystem) NewLocation(authorityStr, locPath string) (vfs.Location, error) { +func (fs *FileSystem) NewLocation(authorityStr, locPath string, _ ...options.NewLocationOption) (vfs.Location, error) { if fs == nil { return nil, errFileSystemRequired } diff --git a/backend/sftp/location.go b/backend/sftp/location.go index 7e4fadb6..7d8de40d 100644 --- a/backend/sftp/location.go +++ b/backend/sftp/location.go @@ -153,7 +153,7 @@ func (l *Location) Exists() (bool, error) { // NewLocation makes a copy of the underlying Location, then modifies its path by calling ChangeDir with the // relativePath argument, returning the resulting location. -func (l *Location) NewLocation(relativePath string) (vfs.Location, error) { +func (l *Location) NewLocation(relativePath string, _ ...options.NewLocationOption) (vfs.Location, error) { if l == nil { return nil, errLocationRequired } diff --git a/mocks/FileSystem.go b/mocks/FileSystem.go index a5a2898c..2cd1f9bb 100644 --- a/mocks/FileSystem.go +++ b/mocks/FileSystem.go @@ -165,8 +165,14 @@ func (_c *FileSystem_NewFile_Call) RunAndReturn(run func(authority string, absFi } // NewLocation provides a mock function for the type FileSystem -func (_mock *FileSystem) NewLocation(authority string, absLocPath string) (vfs.Location, error) { - ret := _mock.Called(authority, absLocPath) +func (_mock *FileSystem) NewLocation(authority string, absLocPath string, opts ...options.NewLocationOption) (vfs.Location, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(authority, absLocPath, opts) + } else { + tmpRet = _mock.Called(authority, absLocPath) + } + ret := tmpRet if len(ret) == 0 { panic("no return value specified for NewLocation") @@ -174,18 +180,18 @@ func (_mock *FileSystem) NewLocation(authority string, absLocPath string) (vfs.L var r0 vfs.Location var r1 error - if returnFunc, ok := ret.Get(0).(func(string, string) (vfs.Location, error)); ok { - return returnFunc(authority, absLocPath) + if returnFunc, ok := ret.Get(0).(func(string, string, ...options.NewLocationOption) (vfs.Location, error)); ok { + return returnFunc(authority, absLocPath, opts...) } - if returnFunc, ok := ret.Get(0).(func(string, string) vfs.Location); ok { - r0 = returnFunc(authority, absLocPath) + if returnFunc, ok := ret.Get(0).(func(string, string, ...options.NewLocationOption) vfs.Location); ok { + r0 = returnFunc(authority, absLocPath, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(vfs.Location) } } - if returnFunc, ok := ret.Get(1).(func(string, string) error); ok { - r1 = returnFunc(authority, absLocPath) + if returnFunc, ok := ret.Get(1).(func(string, string, ...options.NewLocationOption) error); ok { + r1 = returnFunc(authority, absLocPath, opts...) } else { r1 = ret.Error(1) } @@ -200,11 +206,13 @@ type FileSystem_NewLocation_Call struct { // NewLocation is a helper method to define mock.On call // - authority string // - absLocPath string -func (_e *FileSystem_Expecter) NewLocation(authority interface{}, absLocPath interface{}) *FileSystem_NewLocation_Call { - return &FileSystem_NewLocation_Call{Call: _e.mock.On("NewLocation", authority, absLocPath)} +// - opts ...options.NewLocationOption +func (_e *FileSystem_Expecter) NewLocation(authority interface{}, absLocPath interface{}, opts ...interface{}) *FileSystem_NewLocation_Call { + return &FileSystem_NewLocation_Call{Call: _e.mock.On("NewLocation", + append([]interface{}{authority, absLocPath}, opts...)...)} } -func (_c *FileSystem_NewLocation_Call) Run(run func(authority string, absLocPath string)) *FileSystem_NewLocation_Call { +func (_c *FileSystem_NewLocation_Call) Run(run func(authority string, absLocPath string, opts ...options.NewLocationOption)) *FileSystem_NewLocation_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { @@ -214,9 +222,16 @@ func (_c *FileSystem_NewLocation_Call) Run(run func(authority string, absLocPath if args[1] != nil { arg1 = args[1].(string) } + var arg2 []options.NewLocationOption + var variadicArgs []options.NewLocationOption + if len(args) > 2 { + variadicArgs = args[2].([]options.NewLocationOption) + } + arg2 = variadicArgs run( arg0, arg1, + arg2..., ) }) return _c @@ -227,7 +242,7 @@ func (_c *FileSystem_NewLocation_Call) Return(location vfs.Location, err error) return _c } -func (_c *FileSystem_NewLocation_Call) RunAndReturn(run func(authority string, absLocPath string) (vfs.Location, error)) *FileSystem_NewLocation_Call { +func (_c *FileSystem_NewLocation_Call) RunAndReturn(run func(authority string, absLocPath string, opts ...options.NewLocationOption) (vfs.Location, error)) *FileSystem_NewLocation_Call { _c.Call.Return(run) return _c } diff --git a/mocks/Location.go b/mocks/Location.go index da1b0e6b..bda2241c 100644 --- a/mocks/Location.go +++ b/mocks/Location.go @@ -557,8 +557,14 @@ func (_c *Location_NewFile_Call) RunAndReturn(run func(relFilePath string, opts } // NewLocation provides a mock function for the type Location -func (_mock *Location) NewLocation(relLocPath string) (vfs.Location, error) { - ret := _mock.Called(relLocPath) +func (_mock *Location) NewLocation(relLocPath string, opts ...options.NewLocationOption) (vfs.Location, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(relLocPath, opts) + } else { + tmpRet = _mock.Called(relLocPath) + } + ret := tmpRet if len(ret) == 0 { panic("no return value specified for NewLocation") @@ -566,18 +572,18 @@ func (_mock *Location) NewLocation(relLocPath string) (vfs.Location, error) { var r0 vfs.Location var r1 error - if returnFunc, ok := ret.Get(0).(func(string) (vfs.Location, error)); ok { - return returnFunc(relLocPath) + if returnFunc, ok := ret.Get(0).(func(string, ...options.NewLocationOption) (vfs.Location, error)); ok { + return returnFunc(relLocPath, opts...) } - if returnFunc, ok := ret.Get(0).(func(string) vfs.Location); ok { - r0 = returnFunc(relLocPath) + if returnFunc, ok := ret.Get(0).(func(string, ...options.NewLocationOption) vfs.Location); ok { + r0 = returnFunc(relLocPath, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(vfs.Location) } } - if returnFunc, ok := ret.Get(1).(func(string) error); ok { - r1 = returnFunc(relLocPath) + if returnFunc, ok := ret.Get(1).(func(string, ...options.NewLocationOption) error); ok { + r1 = returnFunc(relLocPath, opts...) } else { r1 = ret.Error(1) } @@ -591,18 +597,27 @@ type Location_NewLocation_Call struct { // NewLocation is a helper method to define mock.On call // - relLocPath string -func (_e *Location_Expecter) NewLocation(relLocPath interface{}) *Location_NewLocation_Call { - return &Location_NewLocation_Call{Call: _e.mock.On("NewLocation", relLocPath)} +// - opts ...options.NewLocationOption +func (_e *Location_Expecter) NewLocation(relLocPath interface{}, opts ...interface{}) *Location_NewLocation_Call { + return &Location_NewLocation_Call{Call: _e.mock.On("NewLocation", + append([]interface{}{relLocPath}, opts...)...)} } -func (_c *Location_NewLocation_Call) Run(run func(relLocPath string)) *Location_NewLocation_Call { +func (_c *Location_NewLocation_Call) Run(run func(relLocPath string, opts ...options.NewLocationOption)) *Location_NewLocation_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 string if args[0] != nil { arg0 = args[0].(string) } + var arg1 []options.NewLocationOption + var variadicArgs []options.NewLocationOption + if len(args) > 1 { + variadicArgs = args[1].([]options.NewLocationOption) + } + arg1 = variadicArgs run( arg0, + arg1..., ) }) return _c @@ -613,7 +628,7 @@ func (_c *Location_NewLocation_Call) Return(location vfs.Location, err error) *L return _c } -func (_c *Location_NewLocation_Call) RunAndReturn(run func(relLocPath string) (vfs.Location, error)) *Location_NewLocation_Call { +func (_c *Location_NewLocation_Call) RunAndReturn(run func(relLocPath string, opts ...options.NewLocationOption) (vfs.Location, error)) *Location_NewLocation_Call { _c.Call.Return(run) return _c } diff --git a/options/newfile/context.go b/options/newfile/context.go new file mode 100644 index 00000000..996d9acf --- /dev/null +++ b/options/newfile/context.go @@ -0,0 +1,23 @@ +// Package newfile provides options for creating new files in a virtual filesystem. +package newfile + +import ( + "context" + + "github.com/c2fo/vfs/v7/options" +) + +const optionNameNewFileContext = "newFileContext" + +// WithContext returns Context implementation of NewFileOption +func WithContext(ctx context.Context) options.NewFileOption { + return &Context{ctx} +} + +// Context represents the NewFileOption that is used to specify a context for created files. +type Context struct{ context.Context } + +// NewFileOptionName returns the name of Context option +func (ct *Context) NewFileOptionName() string { + return optionNameNewFileContext +} diff --git a/options/newfile/context_test.go b/options/newfile/context_test.go new file mode 100644 index 00000000..d2992dc8 --- /dev/null +++ b/options/newfile/context_test.go @@ -0,0 +1,22 @@ +package newfile_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/c2fo/vfs/v7/options/newfile" +) + +func TestWithContext(t *testing.T) { + is := require.New(t) + + ctx := context.WithoutCancel(t.Context()) + opt := newfile.WithContext(ctx) + + nfc, ok := opt.(*newfile.Context) + is.True(ok) + is.Equal(ctx, nfc.Context) + is.Equal("newFileContext", nfc.NewFileOptionName()) +} diff --git a/options/newlocation/context.go b/options/newlocation/context.go new file mode 100644 index 00000000..3e349678 --- /dev/null +++ b/options/newlocation/context.go @@ -0,0 +1,23 @@ +// Package newlocation provides options for creating new locations in a virtual filesystem. +package newlocation + +import ( + "context" + + "github.com/c2fo/vfs/v7/options" +) + +const optionNameNewLocationContext = "newLocationContext" + +// WithContext returns Context implementation of NewLocationOption +func WithContext(ctx context.Context) options.NewLocationOption { + return &Context{ctx} +} + +// Context represents the NewLocationOption that is used to specify a context for created locations. +type Context struct{ context.Context } + +// NewLocationOptionName returns the name of Context option +func (ct *Context) NewLocationOptionName() string { + return optionNameNewLocationContext +} diff --git a/options/newlocation/context_test.go b/options/newlocation/context_test.go new file mode 100644 index 00000000..7c58a9b5 --- /dev/null +++ b/options/newlocation/context_test.go @@ -0,0 +1,22 @@ +package newlocation_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/c2fo/vfs/v7/options/newlocation" +) + +func TestWithContext(t *testing.T) { + is := require.New(t) + + ctx := context.WithoutCancel(t.Context()) + opt := newlocation.WithContext(ctx) + + nfc, ok := opt.(*newlocation.Context) + is.True(ok) + is.Equal(ctx, nfc.Context) + is.Equal("newLocationContext", nfc.NewLocationOptionName()) +} diff --git a/options/options.go b/options/options.go index c8eca3f0..630171a2 100644 --- a/options/options.go +++ b/options/options.go @@ -35,6 +35,11 @@ type NewFileOption interface { NewFileOptionName() string } +// NewLocationOption interface contains function that should be implemented by any custom option to qualify as a new location option. +type NewLocationOption interface { + NewLocationOptionName() string +} + // NewFileSystemOption interface contains function that should be implemented by any custom option to qualify as a new // file system option. type NewFileSystemOption[T any] interface { diff --git a/vfs.go b/vfs.go index 8cb743a3..546e43d0 100644 --- a/vfs.go +++ b/vfs.go @@ -32,7 +32,7 @@ type FileSystem interface { // * On error, nil is returned for the location. // // See NewFile for note on authority. - NewLocation(authority string, absLocPath string) (Location, error) + NewLocation(authority string, absLocPath string, opts ...options.NewLocationOption) (Location, error) // Name returns the name of the FileSystem ie: Amazon S3, os, Google Cloud Storage, etc. Name() string @@ -112,7 +112,7 @@ type Location interface { // s3://mybucket/some/ // // * Accepts a relative location path. - NewLocation(relLocPath string) (Location, error) + NewLocation(relLocPath string, opts ...options.NewLocationOption) (Location, error) // ChangeDir updates the existing Location's path to the provided relative location path. //