diff --git a/backend/azure/client.go b/backend/azure/client.go index ae41f13e..8a016d9d 100644 --- a/backend/azure/client.go +++ b/backend/azure/client.go @@ -28,7 +28,7 @@ type Client interface { // 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) error + Upload(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) @@ -102,7 +102,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) error { +func (a *DefaultClient) Upload(file vfs.File, content io.ReadSeeker, contentType string) error { URL, err := url.Parse(file.Location().(*Location).ContainerURL()) if err != nil { return err @@ -110,7 +110,7 @@ func (a *DefaultClient) Upload(file vfs.File, content io.ReadSeeker) error { containerURL := azblob.NewContainerURL(*URL, a.pipeline) blobURL := containerURL.NewBlockBlobURL(utils.RemoveLeadingSlash(file.Path())) - _, err = blobURL.Upload(context.Background(), content, azblob.BlobHTTPHeaders{}, azblob.Metadata{}, + _, err = blobURL.Upload(context.Background(), content, azblob.BlobHTTPHeaders{ContentType: contentType}, azblob.Metadata{}, azblob.BlobAccessConditions{}, azblob.DefaultAccessTier, nil, azblob.ClientProvidedKeyOptions{}, azblob.ImmutabilityPolicyOptions{}) return err } diff --git a/backend/azure/client_integration_test.go b/backend/azure/client_integration_test.go index 22858ca4..93acda69 100644 --- a/backend/azure/client_integration_test.go +++ b/backend/azure/client_integration_test.go @@ -6,6 +6,7 @@ package azure import ( "context" "fmt" + "io" "net/url" "os" "strings" @@ -64,7 +65,7 @@ func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithNoPath() { s.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(f, strings.NewReader("Hello world!"), "") s.NoError(err, "The file should be successfully uploaded to azure") // make sure it exists @@ -112,7 +113,7 @@ func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithPath() { s.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(f, strings.NewReader("Hello world!"), "") s.NoError(err, "The file should be successfully uploaded to azure") // check to see if it exists @@ -144,11 +145,11 @@ func (s *ClientIntegrationTestSuite) TestDeleteAllVersions() { s.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(f, strings.NewReader("Hello!"), "") s.NoError(err, "The file should be successfully uploaded to azure") // Recreate the file - err = client.Upload(f, strings.NewReader("Hello world!")) + err = client.Upload(f, strings.NewReader("Hello world!"), "") s.NoError(err, "The file should be successfully uploaded to azure") // make sure it exists @@ -172,7 +173,7 @@ func (s *ClientIntegrationTestSuite) TestProperties() { client, err := fs.Client() s.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(f, strings.NewReader("Hello world!"), "") s.NoError(err, "The file should be successfully uploaded to azure so we shouldn't get an error") props, err := client.Properties(f.Location().(*Location).ContainerURL(), f.Path()) s.NoError(err, "Tne file exists so we shouldn't get an error") @@ -188,7 +189,7 @@ func (s *ClientIntegrationTestSuite) TestProperties_Location() { l, _ := fs.NewLocation("test-container", "/") client, _ := fs.Client() - err = client.Upload(f, strings.NewReader("Hello world!")) + err = client.Upload(f, strings.NewReader("Hello world!"), "") s.NoError(err, "The file should be successfully uploaded to azure so we shouldn't get an error") props, err := client.Properties(l.URI(), "") @@ -226,7 +227,7 @@ func (s *ClientIntegrationTestSuite) TestTouch_NonexistantContainer() { client, err := fs.Client() s.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(f, strings.NewReader(""), "") s.Error(err, "The container doesn't exist so we should get an error") } @@ -237,7 +238,7 @@ func (s *ClientIntegrationTestSuite) TestTouch_FileAlreadyExists() { client, err := fs.Client() s.NoError(err) - err = client.Upload(f, strings.NewReader("One fish, two fish, red fish, blue fish.")) + err = client.Upload(f, strings.NewReader("One fish, two fish, red fish, blue fish."), "") s.NoError(err) originalProps, err := client.Properties(f.Location().(*Location).ContainerURL(), f.Path()) s.NoError(err, "Should get properties back from azure with no error") diff --git a/backend/azure/file.go b/backend/azure/file.go index 47e23c8a..7ba8a29e 100644 --- a/backend/azure/file.go +++ b/backend/azure/file.go @@ -15,6 +15,7 @@ import ( "github.com/c2fo/vfs/v6/backend" "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/options/delete" + "github.com/c2fo/vfs/v6/options/newfile" "github.com/c2fo/vfs/v6/utils" ) @@ -23,6 +24,7 @@ type File struct { fileSystem *FileSystem container string name string + opts []options.NewFileOption tempFile *os.File isDirty bool } @@ -47,7 +49,16 @@ func (f *File) Close() error { } if f.isDirty { - if err := client.Upload(f, f.tempFile); err != nil { + var contentType string + for _, o := range f.opts { + switch o := o.(type) { + case *newfile.ContentType: + contentType = *(*string)(o) + default: + } + } + + if err := client.Upload(f, f.tempFile, contentType); err != nil { return utils.WrapCloseError(err) } } @@ -308,7 +319,16 @@ func (f *File) Touch() error { } if !exists { - return client.Upload(f, strings.NewReader("")) + var contentType string + for _, o := range f.opts { + switch o := o.(type) { + case *newfile.ContentType: + contentType = *(*string)(o) + default: + } + } + + return client.Upload(f, strings.NewReader(""), contentType) } props, err := client.Properties(f.Location().(*Location).ContainerURL(), f.Path()) diff --git a/backend/azure/fileSystem.go b/backend/azure/fileSystem.go index 23280208..dad1188d 100644 --- a/backend/azure/fileSystem.go +++ b/backend/azure/fileSystem.go @@ -11,6 +11,7 @@ import ( "github.com/c2fo/vfs/v6" "github.com/c2fo/vfs/v6/backend" + "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/utils" ) @@ -58,7 +59,7 @@ func (fs *FileSystem) Client() (Client, error) { } // NewFile returns the azure implementation of vfs.File -func (fs *FileSystem) NewFile(volume, absFilePath string) (vfs.File, error) { +func (fs *FileSystem) NewFile(volume, absFilePath string, opts ...options.NewFileOption) (vfs.File, error) { if fs == nil { return nil, errors.New(errNilFileSystemReceiver) } @@ -75,6 +76,7 @@ func (fs *FileSystem) NewFile(volume, absFilePath string) (vfs.File, error) { fileSystem: fs, container: volume, name: path.Clean(absFilePath), + opts: opts, }, nil } diff --git a/backend/azure/file_test.go b/backend/azure/file_test.go index 772ab146..e89c54a8 100644 --- a/backend/azure/file_test.go +++ b/backend/azure/file_test.go @@ -11,6 +11,7 @@ import ( "github.com/c2fo/vfs/v6" "github.com/c2fo/vfs/v6/options/delete" + "github.com/c2fo/vfs/v6/options/newfile" "github.com/c2fo/vfs/v6/utils" ) @@ -114,6 +115,15 @@ func (s *FileTestSuite) TestExists_NonExistantFile() { s.False(exists) } +func (s *FileTestSuite) TestCloseWithContentType() { + client := MockAzureClient{PropertiesError: MockStorageError{}} + fs := NewFileSystem().WithClient(&client) + f, _ := fs.NewFile("test-container", "/foo.txt", newfile.WithContentType("text/plain")) + _, _ = f.Write([]byte("Hello, World!")) + s.NoError(f.Close()) + s.Equal("text/plain", client.UploadContentType) +} + func (s *FileTestSuite) TestLocation() { fs := NewFileSystem().WithOptions(Options{AccountName: "test-account"}) f, _ := fs.NewFile("test-container", "/file.txt") @@ -287,6 +297,16 @@ func (s *FileTestSuite) TestTouch_NonexistantContainer() { s.Error(f.Touch(), "The container does not exist so creating the new file should error") } +func (s *FileTestSuite) TestTouchWithContentType() { + client := MockAzureClient{ExpectedResult: &BlobProperties{}, PropertiesError: MockStorageError{}} + fs := NewFileSystem().WithClient(&client) + + f, err := fs.NewFile("test-container", "/foo.txt", newfile.WithContentType("text/plain")) + s.NoError(err, "The path is valid so no error should be returned") + s.NoError(f.Touch()) + s.Equal("text/plain", client.UploadContentType) +} + func (s *FileTestSuite) TestURI() { fs := NewFileSystem().WithOptions(Options{AccountName: "test-container"}) f, _ := fs.NewFile("temp", "/foo/bar/blah.txt") diff --git a/backend/azure/location.go b/backend/azure/location.go index 967a64d2..0637bbab 100644 --- a/backend/azure/location.go +++ b/backend/azure/location.go @@ -167,7 +167,7 @@ func (l *Location) FileSystem() vfs.FileSystem { } // NewFile returns a new file instance at the given path, relative to the current location. -func (l *Location) NewFile(relFilePath string) (vfs.File, error) { +func (l *Location) NewFile(relFilePath string, opts ...options.NewFileOption) (vfs.File, error) { if l == nil { return nil, errors.New(errNilLocationReceiver) } @@ -180,6 +180,7 @@ func (l *Location) NewFile(relFilePath string) (vfs.File, error) { name: utils.EnsureLeadingSlash(path.Join(l.path, relFilePath)), container: l.container, fileSystem: l.fileSystem, + opts: opts, }, nil } diff --git a/backend/azure/mock_client.go b/backend/azure/mock_client.go index 55e1be82..13a8d3fa 100644 --- a/backend/azure/mock_client.go +++ b/backend/azure/mock_client.go @@ -11,10 +11,11 @@ import ( // MockAzureClient is a mock implementation of azure.Client. type MockAzureClient struct { - PropertiesError error - PropertiesResult *BlobProperties - ExpectedError error - ExpectedResult interface{} + PropertiesError error + PropertiesResult *BlobProperties + ExpectedError error + ExpectedResult interface{} + UploadContentType string } // Properties returns a PropertiesResult if it exists, otherwise it will return the value of PropertiesError @@ -31,7 +32,8 @@ func (a *MockAzureClient) SetMetadata(file vfs.File, metadata map[string]string) } // Upload returns the value of ExpectedError -func (a *MockAzureClient) Upload(file vfs.File, content io.ReadSeeker) error { +func (a *MockAzureClient) Upload(file vfs.File, content io.ReadSeeker, contentType string) error { + a.UploadContentType = contentType return a.ExpectedError } diff --git a/backend/ftp/file.go b/backend/ftp/file.go index 45b69377..10fdf0b1 100644 --- a/backend/ftp/file.go +++ b/backend/ftp/file.go @@ -33,6 +33,7 @@ type File struct { fileSystem *FileSystem authority utils.Authority path string + opts []options.NewFileOption offset int64 } diff --git a/backend/ftp/fileSystem.go b/backend/ftp/fileSystem.go index 2511725a..b4bab65c 100644 --- a/backend/ftp/fileSystem.go +++ b/backend/ftp/fileSystem.go @@ -9,6 +9,7 @@ import ( "github.com/c2fo/vfs/v6" "github.com/c2fo/vfs/v6/backend" "github.com/c2fo/vfs/v6/backend/ftp/types" + "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/utils" ) @@ -34,7 +35,7 @@ func (fs *FileSystem) Retry() vfs.Retry { } // NewFile function returns the FTP implementation of vfs.File. -func (fs *FileSystem) NewFile(authority, filePath string) (vfs.File, error) { +func (fs *FileSystem) NewFile(authority, filePath string, opts ...options.NewFileOption) (vfs.File, error) { if fs == nil { return nil, errors.New("non-nil ftp.FileSystem pointer is required") } @@ -54,6 +55,7 @@ func (fs *FileSystem) NewFile(authority, filePath string) (vfs.File, error) { fileSystem: fs, authority: auth, path: path.Clean(filePath), + opts: opts, }, nil } diff --git a/backend/ftp/location.go b/backend/ftp/location.go index 572714dc..ce95c1f4 100644 --- a/backend/ftp/location.go +++ b/backend/ftp/location.go @@ -193,7 +193,7 @@ func (l *Location) ChangeDir(relativePath string) error { // NewFile uses the properties of the calling location to generate a vfs.File (backed by an ftp.File). The filePath // argument is expected to be a relative path to the location's current path. -func (l *Location) NewFile(filePath string) (vfs.File, error) { +func (l *Location) NewFile(filePath string, opts ...options.NewFileOption) (vfs.File, error) { err := utils.ValidateRelativeFilePath(filePath) if err != nil { return nil, err @@ -202,6 +202,7 @@ func (l *Location) NewFile(filePath string) (vfs.File, error) { fileSystem: l.fileSystem, authority: l.Authority, path: utils.EnsureLeadingSlash(path.Join(l.path, filePath)), + opts: opts, } return newFile, nil } diff --git a/backend/gs/file.go b/backend/gs/file.go index cb1395da..6a114d4e 100644 --- a/backend/gs/file.go +++ b/backend/gs/file.go @@ -16,6 +16,7 @@ import ( "github.com/c2fo/vfs/v6/backend" "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/options/delete" + "github.com/c2fo/vfs/v6/options/newfile" "github.com/c2fo/vfs/v6/utils" ) @@ -28,6 +29,7 @@ type File struct { fileSystem *FileSystem bucket string key string + opts []options.NewFileOption // seek-related fields cursorPos int64 @@ -107,6 +109,14 @@ func (f *File) tempToGCS() error { w := handle.NewWriter(f.fileSystem.ctx) defer func() { _ = w.Close() }() + for _, o := range f.opts { + switch o := o.(type) { + case *newfile.ContentType: + w.ContentType = *(*string)(o) + default: + } + } + _, err = f.tempFileWriter.Seek(0, io.SeekStart) if err != nil { return err @@ -328,6 +338,14 @@ func (f *File) initWriters() error { return err } + for _, o := range f.opts { + switch o := o.(type) { + case *newfile.ContentType: + w.ContentType = *(*string)(o) + default: + } + } + // set gcsWriter f.gcsWriter = w } @@ -593,6 +611,15 @@ func (f *File) createEmptyFile() error { defer cancel() w := handle.NewWriter(ctx) + + for _, o := range f.opts { + switch o := o.(type) { + case *newfile.ContentType: + w.ContentType = *(*string)(o) + default: + } + } + defer func() { _ = w.Close() }() if _, err := w.Write(make([]byte, 0)); err != nil { return err diff --git a/backend/gs/fileSystem.go b/backend/gs/fileSystem.go index 35b78051..b8647c2b 100644 --- a/backend/gs/fileSystem.go +++ b/backend/gs/fileSystem.go @@ -10,6 +10,7 @@ import ( "github.com/c2fo/vfs/v6" "github.com/c2fo/vfs/v6/backend" + "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/utils" ) @@ -27,14 +28,14 @@ type FileSystem struct { // Retry will return a retrier provided via options, or a no-op if none is provided. func (fs *FileSystem) Retry() vfs.Retry { - if options, _ := fs.options.(Options); options.Retry != nil { - return options.Retry + if opts, _ := fs.options.(Options); opts.Retry != nil { + return opts.Retry } return vfs.DefaultRetryer() } // NewFile function returns the gcs implementation of vfs.File. -func (fs *FileSystem) NewFile(volume, name string) (vfs.File, error) { +func (fs *FileSystem) NewFile(volume, name string, opts ...options.NewFileOption) (vfs.File, error) { if fs == nil { return nil, errors.New("non-nil gs.FileSystem pointer is required") } @@ -48,6 +49,7 @@ func (fs *FileSystem) NewFile(volume, name string) (vfs.File, error) { fileSystem: fs, bucket: volume, key: path.Clean(name), + opts: opts, }, nil } diff --git a/backend/gs/file_test.go b/backend/gs/file_test.go index fa5e2d96..4e771bcc 100644 --- a/backend/gs/file_test.go +++ b/backend/gs/file_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/c2fo/vfs/v6/options/delete" + "github.com/c2fo/vfs/v6/options/newfile" "github.com/c2fo/vfs/v6/utils" ) @@ -242,6 +243,56 @@ func (ts *fileTestSuite) TestWrite() { ts.Nil(err, "Error should be nil when calling Write") } +func (ts *fileTestSuite) TestWriteWithContentType() { + contents := "hello world!" + bucketName := "bucki" + objectName := "some/path/file.txt" + server := fakestorage.NewServer(Objects{}) + defer server.Stop() + client := server.Client() + bucket := client.Bucket(bucketName) + ctx := context.Background() + err := bucket.Create(ctx, "", nil) + ts.Require().NoError(err) + fs := NewFileSystem().WithClient(client) + + file, err := fs.NewFile(bucketName, "/"+objectName, newfile.WithContentType("text/plain")) + ts.NoError(err, "Shouldn't fail creating new file") + + _, err = file.Write([]byte(contents)) + ts.NoError(err, "Error should be nil when calling Write") + + err = file.Close() + ts.NoError(err, "Error should be nil when calling Close") + + attrs, err := bucket.Object(objectName).Attrs(ctx) + ts.Require().NoError(err) + ts.Equal("text/plain", attrs.ContentType) +} + +func (ts *fileTestSuite) TestTouchWithContentType() { + bucketName := "bucki" + objectName := "some/path/file.txt" + server := fakestorage.NewServer(Objects{}) + defer server.Stop() + client := server.Client() + bucket := client.Bucket(bucketName) + ctx := context.Background() + err := bucket.Create(ctx, "", nil) + ts.Require().NoError(err) + fs := NewFileSystem().WithClient(client) + + file, err := fs.NewFile(bucketName, "/"+objectName, newfile.WithContentType("text/plain")) + ts.NoError(err, "Shouldn't fail creating new file") + + err = file.Touch() + ts.NoError(err, "Error should be nil when calling Touch") + + attrs, err := bucket.Object(objectName).Attrs(ctx) + ts.Require().NoError(err) + ts.Equal("text/plain", attrs.ContentType) +} + func (ts *fileTestSuite) TestGetLocation() { server := fakestorage.NewServer(Objects{}) defer server.Stop() diff --git a/backend/gs/location.go b/backend/gs/location.go index f4ad7d30..38a5a5e6 100644 --- a/backend/gs/location.go +++ b/backend/gs/location.go @@ -157,7 +157,7 @@ func (l *Location) FileSystem() vfs.FileSystem { } // NewFile returns a new file instance at the given path, relative to the current location. -func (l *Location) NewFile(filePath string) (vfs.File, error) { +func (l *Location) NewFile(filePath string, opts ...options.NewFileOption) (vfs.File, error) { if l == nil { return nil, errors.New("non-nil gs.Location pointer is required") } @@ -172,6 +172,7 @@ func (l *Location) NewFile(filePath string) (vfs.File, error) { fileSystem: l.fileSystem, bucket: l.bucket, key: utils.EnsureLeadingSlash(path.Join(l.prefix, filePath)), + opts: opts, } return newFile, nil } diff --git a/backend/mem/file.go b/backend/mem/file.go index bd6b9e87..9d6fd39e 100644 --- a/backend/mem/file.go +++ b/backend/mem/file.go @@ -40,6 +40,7 @@ type File struct { memFile *memFile readWriteSeeker *ReadWriteSeeker name string // the base name of the file + opts []options.NewFileOption cursor int writeMode mode isOpen bool diff --git a/backend/mem/fileSystem.go b/backend/mem/fileSystem.go index e39f8ddd..2ca5738c 100644 --- a/backend/mem/fileSystem.go +++ b/backend/mem/fileSystem.go @@ -6,6 +6,7 @@ import ( "github.com/c2fo/vfs/v6" "github.com/c2fo/vfs/v6/backend" + "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/utils" ) @@ -38,7 +39,7 @@ func (fs *FileSystem) Retry() vfs.Retry { // If a file is written to before a touch call, Write() will take care of that call. This is // true for other functions as well and existence only poses a problem in the context of deletion // or copying FROM a non-existent file. -func (fs *FileSystem) NewFile(volume, absFilePath string) (vfs.File, error) { +func (fs *FileSystem) NewFile(volume, absFilePath string, opts ...options.NewFileOption) (vfs.File, error) { err := utils.ValidateAbsoluteFilePath(absFilePath) if err != nil { @@ -60,6 +61,7 @@ func (fs *FileSystem) NewFile(volume, absFilePath string) (vfs.File, error) { name: obj.i.(*memFile).name, memFile: obj.i.(*memFile), readWriteSeeker: NewReadWriteSeekerWithData(obj.i.(*memFile).contents), + opts: opts, } return vfsFile, nil } @@ -69,6 +71,7 @@ func (fs *FileSystem) NewFile(volume, absFilePath string) (vfs.File, error) { // validateAbsFile path will throw an error if there was a trailing slash, hence not calling path.Clean() file := &File{ name: path.Base(absFilePath), + opts: opts, } memFile := newMemFile(file, location.(*Location)) diff --git a/backend/mem/location.go b/backend/mem/location.go index 551bed73..970a8d68 100644 --- a/backend/mem/location.go +++ b/backend/mem/location.go @@ -158,7 +158,7 @@ func (l *Location) FileSystem() vfs.FileSystem { } // NewFile creates a vfs.File given its relative path and tags it onto "l's" path -func (l *Location) NewFile(relFilePath string) (vfs.File, error) { +func (l *Location) NewFile(relFilePath string, opts ...options.NewFileOption) (vfs.File, error) { if relFilePath == "" { return nil, errors.New("cannot use empty name for file") @@ -190,6 +190,7 @@ func (l *Location) NewFile(relFilePath string) (vfs.File, error) { file := &File{ name: path.Base(nameStr), + opts: opts, } newLoc := *l newLoc.name = relativeLocationPath diff --git a/backend/os/file.go b/backend/os/file.go index 6cc98a46..9e826284 100644 --- a/backend/os/file.go +++ b/backend/os/file.go @@ -24,6 +24,7 @@ type File struct { file *os.File name string filesystem *FileSystem + opts []options.NewFileOption cursorPos int64 tempFile *os.File useTempFile bool diff --git a/backend/os/fileSystem.go b/backend/os/fileSystem.go index 415dd814..b2d3254a 100644 --- a/backend/os/fileSystem.go +++ b/backend/os/fileSystem.go @@ -5,6 +5,7 @@ import ( "github.com/c2fo/vfs/v6" "github.com/c2fo/vfs/v6/backend" + "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/utils" ) @@ -21,12 +22,12 @@ func (fs *FileSystem) Retry() vfs.Retry { } // NewFile function returns the os implementation of vfs.File. -func (fs *FileSystem) NewFile(volume, name string) (vfs.File, error) { +func (fs *FileSystem) NewFile(volume, name string, opts ...options.NewFileOption) (vfs.File, error) { err := utils.ValidateAbsoluteFilePath(name) if err != nil { return nil, err } - return &File{name: name, filesystem: fs}, nil + return &File{name: name, filesystem: fs, opts: opts}, nil } // NewLocation function returns the os implementation of vfs.Location. diff --git a/backend/os/location.go b/backend/os/location.go index 5c82eb44..4718e9ed 100644 --- a/backend/os/location.go +++ b/backend/os/location.go @@ -21,7 +21,7 @@ type Location struct { // NewFile uses the properties of the calling location to generate a vfs.File (backed by an os.File). A string // argument is expected to be a relative path to the location's current path. -func (l *Location) NewFile(fileName string) (vfs.File, error) { +func (l *Location) NewFile(fileName string, opts ...options.NewFileOption) (vfs.File, error) { if l == nil { return nil, errors.New("non-nil os.Location pointer is required") } @@ -33,7 +33,7 @@ func (l *Location) NewFile(fileName string) (vfs.File, error) { return nil, err } fileName = utils.EnsureLeadingSlash(path.Clean(path.Join(l.name, fileName))) - return l.fileSystem.NewFile(l.Volume(), fileName) + return l.fileSystem.NewFile(l.Volume(), fileName, opts...) } // DeleteFile deletes the file of the given name at the location. This is meant to be a short cut for instantiating a diff --git a/backend/s3/file.go b/backend/s3/file.go index 1b7e9dd3..22ebf820 100644 --- a/backend/s3/file.go +++ b/backend/s3/file.go @@ -21,6 +21,7 @@ import ( "github.com/c2fo/vfs/v6/mocks" "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/options/delete" + "github.com/c2fo/vfs/v6/options/newfile" "github.com/c2fo/vfs/v6/utils" ) @@ -31,6 +32,7 @@ type File struct { fileSystem *FileSystem bucket string key string + opts []options.NewFileOption // seek-related fields cursorPos int64 @@ -654,6 +656,14 @@ func uploadInput(f *File) *s3manager.UploadInput { } } + for _, o := range f.opts { + switch o := o.(type) { + case *newfile.ContentType: + input.ContentType = (*string)(o) + default: + } + } + return input } diff --git a/backend/s3/fileSystem.go b/backend/s3/fileSystem.go index da0120d5..bb78dae1 100644 --- a/backend/s3/fileSystem.go +++ b/backend/s3/fileSystem.go @@ -10,6 +10,7 @@ import ( "github.com/c2fo/vfs/v6" "github.com/c2fo/vfs/v6/backend" + "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/utils" ) @@ -30,7 +31,7 @@ func (fs *FileSystem) Retry() vfs.Retry { } // NewFile function returns the s3 implementation of vfs.File. -func (fs *FileSystem) NewFile(volume, name string) (vfs.File, error) { +func (fs *FileSystem) NewFile(volume, name string, opts ...options.NewFileOption) (vfs.File, error) { if fs == nil { return nil, errors.New("non-nil s3.FileSystem pointer is required") } @@ -45,6 +46,7 @@ func (fs *FileSystem) NewFile(volume, name string) (vfs.File, error) { fileSystem: fs, bucket: utils.RemoveTrailingSlash(volume), key: path.Clean(name), + opts: opts, }, nil } diff --git a/backend/s3/file_test.go b/backend/s3/file_test.go index e9992c3a..b4ba18b6 100644 --- a/backend/s3/file_test.go +++ b/backend/s3/file_test.go @@ -25,6 +25,7 @@ import ( "github.com/c2fo/vfs/v6/backend/s3/mocks" vfsmocks "github.com/c2fo/vfs/v6/mocks" "github.com/c2fo/vfs/v6/options/delete" + "github.com/c2fo/vfs/v6/options/newfile" "github.com/c2fo/vfs/v6/utils" ) @@ -696,6 +697,13 @@ func (ts *fileTestSuite) TestUploadInputDisableSSE() { ts.Equal("mybucket", *input.Bucket, "bucket was set") } +func (ts *fileTestSuite) TestUploadInputContentType() { + fs = FileSystem{client: &mocks.S3API{}} + file, _ := fs.NewFile("mybucket", "/some/file/test.txt", newfile.WithContentType("text/plain")) + input := uploadInput(file.(*File)) + ts.Equal("text/plain", *input.ContentType) +} + func (ts *fileTestSuite) TestNewFile() { fs := &FileSystem{} // fs is nil diff --git a/backend/s3/location.go b/backend/s3/location.go index 730eb56c..cbec6f19 100644 --- a/backend/s3/location.go +++ b/backend/s3/location.go @@ -123,7 +123,7 @@ func (l *Location) ChangeDir(relativePath string) error { // NewFile uses the properties of the calling location to generate a vfs.File (backed by an s3.File). The filePath // argument is expected to be a relative path to the location's current path. -func (l *Location) NewFile(filePath string) (vfs.File, error) { +func (l *Location) NewFile(filePath string, opts ...options.NewFileOption) (vfs.File, error) { if l == nil { return nil, errors.New("non-nil s3.Location pointer is required") } @@ -138,6 +138,7 @@ func (l *Location) NewFile(filePath string) (vfs.File, error) { fileSystem: l.fileSystem, bucket: l.bucket, key: utils.EnsureLeadingSlash(path.Join(l.prefix, filePath)), + opts: opts, } return newFile, nil } diff --git a/backend/sftp/file.go b/backend/sftp/file.go index 62762b58..1786eab3 100644 --- a/backend/sftp/file.go +++ b/backend/sftp/file.go @@ -18,6 +18,7 @@ type File struct { fileSystem *FileSystem Authority utils.Authority path string + opts []options.NewFileOption sftpfile ReadWriteSeekCloser opener fileOpener seekCalled bool diff --git a/backend/sftp/fileSystem.go b/backend/sftp/fileSystem.go index 3c2fdbee..3660fb1b 100644 --- a/backend/sftp/fileSystem.go +++ b/backend/sftp/fileSystem.go @@ -14,6 +14,7 @@ import ( "github.com/c2fo/vfs/v6" "github.com/c2fo/vfs/v6/backend" + "github.com/c2fo/vfs/v6/options" "github.com/c2fo/vfs/v6/utils" ) @@ -40,7 +41,7 @@ func (fs *FileSystem) Retry() vfs.Retry { } // NewFile function returns the SFTP implementation of vfs.File. -func (fs *FileSystem) NewFile(authority, filePath string) (vfs.File, error) { +func (fs *FileSystem) NewFile(authority, filePath string, opts ...options.NewFileOption) (vfs.File, error) { if fs == nil { return nil, errors.New("non-nil sftp.FileSystem pointer is required") } @@ -60,6 +61,7 @@ func (fs *FileSystem) NewFile(authority, filePath string) (vfs.File, error) { fileSystem: fs, Authority: auth, path: path.Clean(filePath), + opts: opts, }, nil } diff --git a/backend/sftp/location.go b/backend/sftp/location.go index f3c20603..08b84951 100644 --- a/backend/sftp/location.go +++ b/backend/sftp/location.go @@ -177,7 +177,7 @@ func (l *Location) ChangeDir(relativePath string) error { // NewFile uses the properties of the calling location to generate a vfs.File (backed by an sftp.File). The filePath // argument is expected to be a relative path to the location's current path. -func (l *Location) NewFile(filePath string) (vfs.File, error) { +func (l *Location) NewFile(filePath string, opts ...options.NewFileOption) (vfs.File, error) { if l == nil { return nil, errors.New("non-nil sftp.Location pointer receiver is required") } @@ -192,6 +192,7 @@ func (l *Location) NewFile(filePath string) (vfs.File, error) { fileSystem: l.fileSystem, Authority: l.Authority, path: utils.EnsureLeadingSlash(path.Join(l.path, filePath)), + opts: opts, } return newFile, nil } diff --git a/docs/azure.md b/docs/azure.md index 1811e110..db671c50 100644 --- a/docs/azure.md +++ b/docs/azure.md @@ -452,7 +452,7 @@ Name returns "azure" #### func (*FileSystem) NewFile ```go -func (fs *FileSystem) NewFile(volume, absFilePath string) (vfs.File, error) +func (fs *FileSystem) NewFile(volume, absFilePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile returns the azure implementation of vfs.File @@ -561,7 +561,7 @@ ListByRegex returns a list of base names that match the given regular expression #### func (*Location) NewFile ```go -func (l *Location) NewFile(relFilePath string) (vfs.File, error) +func (l *Location) NewFile(relFilePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile returns a new file instance at the given path, relative to the current location. diff --git a/docs/ftp.md b/docs/ftp.md index 2d4804dc..61077df8 100644 --- a/docs/ftp.md +++ b/docs/ftp.md @@ -386,7 +386,7 @@ Name returns "Secure File Transfer Protocol" #### func (*FileSystem) NewFile ```go -func (fs *FileSystem) NewFile(authority, filePath string) (vfs.File, error) +func (fs *FileSystem) NewFile(authority, filePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile function returns the FTP implementation of vfs.File. @@ -503,7 +503,7 @@ considerations of List() apply here as well. #### func (*Location) NewFile ```go -func (l *Location) NewFile(filePath string) (vfs.File, error) +func (l *Location) NewFile(filePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile uses the properties of the calling location to generate a vfs.File (backed by an ftp.File). The filePath argument is expected to be a relative path diff --git a/docs/gs.md b/docs/gs.md index 3848c28d..f1e1ae48 100644 --- a/docs/gs.md +++ b/docs/gs.md @@ -273,7 +273,7 @@ Name returns "Google Cloud Storage" #### func (*FileSystem) NewFile ```go -func (fs *FileSystem) NewFile(volume string, name string) (vfs.File, error) +func (fs *FileSystem) NewFile(volume string, name string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile function returns the gcs implementation of [vfs.File](../README.md#type-file). @@ -378,7 +378,7 @@ provided regular expression. #### func (*Location) NewFile ```go -func (l *Location) NewFile(filePath string) (vfs.File, error) +func (l *Location) NewFile(filePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile returns a new file instance at the given path, relative to the current location. diff --git a/docs/mem.md b/docs/mem.md index f05949a7..c18d9165 100644 --- a/docs/mem.md +++ b/docs/mem.md @@ -222,7 +222,7 @@ Name returns the name of the underlying FileSystem #### func (*FileSystem) NewFile ```go -func (fs *FileSystem) NewFile(volume string, absFilePath string) (vfs.File, error) +func (fs *FileSystem) NewFile(volume string, absFilePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile function returns the in-memory implementation of vfs.File. Since this is inside FileSystem, we assume that the caller knows that the CWD is the root. If @@ -325,7 +325,7 @@ empty slice upon nothing found #### func (*Location) NewFile ```go -func (l *Location) NewFile(relFilePath string) (vfs.File, error) +func (l *Location) NewFile(relFilePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile creates a vfs.File given its relative path and tags it onto "l's" path diff --git a/docs/os.md b/docs/os.md index 15f40c26..08b0af4d 100644 --- a/docs/os.md +++ b/docs/os.md @@ -206,7 +206,7 @@ Name returns "os" #### func (*FileSystem) NewFile ```go -func (fs *FileSystem) NewFile(volume string, name string) (vfs.File, error) +func (fs *FileSystem) NewFile(volume string, name string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile function returns the os implementation of [vfs.File](../README.md#type-file). @@ -295,7 +295,7 @@ of of the location. #### func (*Location) NewFile ```go -func (l *Location) NewFile(fileName string) (vfs.File, error) +func (l *Location) NewFile(fileName string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile uses the properties of the calling location to generate a [vfs.File](../README.md#type-file) (backed by an [os.File](#type-file)). A string argument is expected to be a relative path to diff --git a/docs/s3.md b/docs/s3.md index 41c913ac..e66601b7 100644 --- a/docs/s3.md +++ b/docs/s3.md @@ -300,7 +300,7 @@ Name returns "AWS S3" #### func (*FileSystem) NewFile ```go -func (fs *FileSystem) NewFile(volume string, name string) (vfs.File, error) +func (fs *FileSystem) NewFile(volume string, name string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile function returns the s3 implementation of [vfs.File](../README.md#type-file). @@ -406,7 +406,7 @@ considerations of [List()](#func-location-list) apply here as well. #### func (*Location) NewFile ```go -func (l *Location) NewFile(filePath string) (vfs.File, error) +func (l *Location) NewFile(filePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile uses the properties of the calling location to generate a vfs.File (backed by an [s3.File](#type-file)). The filePath argument is expected to be a relative path diff --git a/docs/sftp.md b/docs/sftp.md index 7709e496..4ac551f5 100644 --- a/docs/sftp.md +++ b/docs/sftp.md @@ -417,7 +417,7 @@ Name returns "Secure File Transfer Protocol" #### func (*FileSystem) NewFile ```go -func (fs *FileSystem) NewFile(authority, filePath string) (vfs.File, error) +func (fs *FileSystem) NewFile(authority, filePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile function returns the SFTP implementation of vfs.File. @@ -528,7 +528,7 @@ considerations of [List()](#func-location-list) apply here as well. #### func (*Location) NewFile ```go -func (l *Location) NewFile(filePath string) (vfs.File, error) +func (l *Location) NewFile(filePath string, opts ...options.NewFileOption) (vfs.File, error) ``` NewFile uses the properties of the calling location to generate a [vfs.File](../README.md#type-file) (backed by an sftp.File). The filePath argument is expected to be a relative diff --git a/mocks/FileSystem.go b/mocks/FileSystem.go index 929bdf91..7a68b10b 100644 --- a/mocks/FileSystem.go +++ b/mocks/FileSystem.go @@ -3,8 +3,10 @@ package mocks import ( - vfs "github.com/c2fo/vfs/v6" + options "github.com/c2fo/vfs/v6/options" mock "github.com/stretchr/testify/mock" + + vfs "github.com/c2fo/vfs/v6" ) // FileSystem is an autogenerated mock type for the FileSystem type @@ -65,9 +67,16 @@ func (_c *FileSystem_Name_Call) RunAndReturn(run func() string) *FileSystem_Name return _c } -// NewFile provides a mock function with given fields: volume, absFilePath -func (_m *FileSystem) NewFile(volume string, absFilePath string) (vfs.File, error) { - ret := _m.Called(volume, absFilePath) +// NewFile provides a mock function with given fields: volume, absFilePath, opts +func (_m *FileSystem) NewFile(volume string, absFilePath string, opts ...options.NewFileOption) (vfs.File, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, volume, absFilePath) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) if len(ret) == 0 { panic("no return value specified for NewFile") @@ -75,19 +84,19 @@ func (_m *FileSystem) NewFile(volume string, absFilePath string) (vfs.File, erro var r0 vfs.File var r1 error - if rf, ok := ret.Get(0).(func(string, string) (vfs.File, error)); ok { - return rf(volume, absFilePath) + if rf, ok := ret.Get(0).(func(string, string, ...options.NewFileOption) (vfs.File, error)); ok { + return rf(volume, absFilePath, opts...) } - if rf, ok := ret.Get(0).(func(string, string) vfs.File); ok { - r0 = rf(volume, absFilePath) + if rf, ok := ret.Get(0).(func(string, string, ...options.NewFileOption) vfs.File); ok { + r0 = rf(volume, absFilePath, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(vfs.File) } } - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(volume, absFilePath) + if rf, ok := ret.Get(1).(func(string, string, ...options.NewFileOption) error); ok { + r1 = rf(volume, absFilePath, opts...) } else { r1 = ret.Error(1) } @@ -103,13 +112,21 @@ type FileSystem_NewFile_Call struct { // NewFile is a helper method to define mock.On call // - volume string // - absFilePath string -func (_e *FileSystem_Expecter) NewFile(volume interface{}, absFilePath interface{}) *FileSystem_NewFile_Call { - return &FileSystem_NewFile_Call{Call: _e.mock.On("NewFile", volume, absFilePath)} +// - opts ...options.NewFileOption +func (_e *FileSystem_Expecter) NewFile(volume interface{}, absFilePath interface{}, opts ...interface{}) *FileSystem_NewFile_Call { + return &FileSystem_NewFile_Call{Call: _e.mock.On("NewFile", + append([]interface{}{volume, absFilePath}, opts...)...)} } -func (_c *FileSystem_NewFile_Call) Run(run func(volume string, absFilePath string)) *FileSystem_NewFile_Call { +func (_c *FileSystem_NewFile_Call) Run(run func(volume string, absFilePath string, opts ...options.NewFileOption)) *FileSystem_NewFile_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string), args[1].(string)) + variadicArgs := make([]options.NewFileOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(options.NewFileOption) + } + } + run(args[0].(string), args[1].(string), variadicArgs...) }) return _c } @@ -119,7 +136,7 @@ func (_c *FileSystem_NewFile_Call) Return(_a0 vfs.File, _a1 error) *FileSystem_N return _c } -func (_c *FileSystem_NewFile_Call) RunAndReturn(run func(string, string) (vfs.File, error)) *FileSystem_NewFile_Call { +func (_c *FileSystem_NewFile_Call) RunAndReturn(run func(string, string, ...options.NewFileOption) (vfs.File, error)) *FileSystem_NewFile_Call { _c.Call.Return(run) return _c } diff --git a/mocks/Location.go b/mocks/Location.go index e3979f07..fe35207c 100644 --- a/mocks/Location.go +++ b/mocks/Location.go @@ -406,9 +406,16 @@ func (_c *Location_ListByRegex_Call) RunAndReturn(run func(*regexp.Regexp) ([]st return _c } -// NewFile provides a mock function with given fields: relFilePath -func (_m *Location) NewFile(relFilePath string) (vfs.File, error) { - ret := _m.Called(relFilePath) +// NewFile provides a mock function with given fields: relFilePath, opts +func (_m *Location) NewFile(relFilePath string, opts ...options.NewFileOption) (vfs.File, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, relFilePath) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) if len(ret) == 0 { panic("no return value specified for NewFile") @@ -416,19 +423,19 @@ func (_m *Location) NewFile(relFilePath string) (vfs.File, error) { var r0 vfs.File var r1 error - if rf, ok := ret.Get(0).(func(string) (vfs.File, error)); ok { - return rf(relFilePath) + if rf, ok := ret.Get(0).(func(string, ...options.NewFileOption) (vfs.File, error)); ok { + return rf(relFilePath, opts...) } - if rf, ok := ret.Get(0).(func(string) vfs.File); ok { - r0 = rf(relFilePath) + if rf, ok := ret.Get(0).(func(string, ...options.NewFileOption) vfs.File); ok { + r0 = rf(relFilePath, opts...) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(vfs.File) } } - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(relFilePath) + if rf, ok := ret.Get(1).(func(string, ...options.NewFileOption) error); ok { + r1 = rf(relFilePath, opts...) } else { r1 = ret.Error(1) } @@ -443,13 +450,21 @@ type Location_NewFile_Call struct { // NewFile is a helper method to define mock.On call // - relFilePath string -func (_e *Location_Expecter) NewFile(relFilePath interface{}) *Location_NewFile_Call { - return &Location_NewFile_Call{Call: _e.mock.On("NewFile", relFilePath)} +// - opts ...options.NewFileOption +func (_e *Location_Expecter) NewFile(relFilePath interface{}, opts ...interface{}) *Location_NewFile_Call { + return &Location_NewFile_Call{Call: _e.mock.On("NewFile", + append([]interface{}{relFilePath}, opts...)...)} } -func (_c *Location_NewFile_Call) Run(run func(relFilePath string)) *Location_NewFile_Call { +func (_c *Location_NewFile_Call) Run(run func(relFilePath string, opts ...options.NewFileOption)) *Location_NewFile_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(string)) + variadicArgs := make([]options.NewFileOption, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(options.NewFileOption) + } + } + run(args[0].(string), variadicArgs...) }) return _c } @@ -459,7 +474,7 @@ func (_c *Location_NewFile_Call) Return(_a0 vfs.File, _a1 error) *Location_NewFi return _c } -func (_c *Location_NewFile_Call) RunAndReturn(run func(string) (vfs.File, error)) *Location_NewFile_Call { +func (_c *Location_NewFile_Call) RunAndReturn(run func(string, ...options.NewFileOption) (vfs.File, error)) *Location_NewFile_Call { _c.Call.Return(run) return _c } diff --git a/options/newfile/contentType.go b/options/newfile/contentType.go new file mode 100644 index 00000000..559361c5 --- /dev/null +++ b/options/newfile/contentType.go @@ -0,0 +1,19 @@ +package newfile + +import "github.com/c2fo/vfs/v6/options" + +const optionNameNewFileContentType = "newFileContentType" + +// WithContentType returns ContentType implementation of NewFileOption +func WithContentType(contentType string) options.NewFileOption { + ct := ContentType(contentType) + return &ct +} + +// ContentType represents the NewFileOption that is used to explicitly specify a content type on created files. +type ContentType string + +// NewFileOptionName returns the name of ContentType option +func (ct *ContentType) NewFileOptionName() string { + return optionNameNewFileContentType +} diff --git a/options/options.go b/options/options.go index 558d1438..d613278f 100644 --- a/options/options.go +++ b/options/options.go @@ -16,3 +16,8 @@ package options type DeleteOption interface { DeleteOptionName() string } + +// NewFileOption interface contains function that should be implemented by any custom option to qualify as a new file option. +type NewFileOption interface { + NewFileOptionName() string +} diff --git a/vfs.go b/vfs.go index 62dba170..69cb4040 100644 --- a/vfs.go +++ b/vfs.go @@ -22,7 +22,7 @@ type FileSystem interface { // s3://mybucket/path/to/file has a volume of "mybucket and name /path/to/file // results in /tmp/dir1/newerdir/file.txt for the final vfs.File path. // * The file may or may not already exist. - NewFile(volume string, absFilePath string) (File, error) + NewFile(volume string, absFilePath string, opts ...options.NewFileOption) (File, error) // NewLocation initializes a Location on the specified volume with the given path. // @@ -123,7 +123,7 @@ type Location interface { // results in /tmp/dir1/newerdir/file.txt for the final vfs.File path. // * Upon success, a vfs.File, representing the file's new path (location path + file relative path), will be returned. // * The file may or may not already exist. - NewFile(relFilePath string) (File, error) + NewFile(relFilePath string, opts ...options.NewFileOption) (File, error) // DeleteFile deletes the file of the given name at the location. //