Repository: C2FO/vfs
Pull request #69 (2021-05-10 16:48:36)

Fix tokenCredentialFactory nil pointer error
When you don't use the NewOptions function (mainly intended to be used with vfssmiple) tokenCredentialFactory is nil and cannot be set.  This causes nil pointer errors when doing token based authentication.

This PR also includes some linter issues that did not get picked up in the previous PR due to the vfsintegration build tag.


Patch:
diff --git a/README.md b/README.md
index 0c21bac..1682fd5 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,7 @@ Feel free to send a pull request if you want to add your backend to the list.
   * [s3 backend](docs/s3.md)
   * [in-memory backend](docs/mem.md)
   * [sftp backend](docs/sftp.md)
+  * [azure backend](docs/azure.md)  
 * [utils](docs/utils.md)
 
 ### Ideas
diff --git a/backend/azure/options.go b/backend/azure/options.go
index 932fa70..82d63f4 100644
--- a/backend/azure/options.go
+++ b/backend/azure/options.go
@@ -68,6 +68,10 @@ func NewOptions() *Options {
 //       is used with storage accounts and only provides access to a single storage account.
 //    3. Returns an anonymous credential.  This allows access only to public blobs.
 func (o *Options) Credential() (azblob.Credential, error) {
+	if o.tokenCredentialFactory == nil {
+		o.tokenCredentialFactory = &DefaultTokenCredentialFactory{}
+	}
+
 	// Check to see if we have service account credentials
 	if o.TenantID != "" && o.ClientID != "" && o.ClientSecret != "" {
 		return o.tokenCredentialFactory.New(o.TenantID, o.ClientID, o.ClientSecret, o.AzureEnvName)
diff --git a/backend/mem/file.go b/backend/mem/file.go
index b66bc65..2934570 100644
--- a/backend/mem/file.go
+++ b/backend/mem/file.go
@@ -93,7 +93,6 @@ func (f *File) Read(p []byte) (n int, err error) {
 		return 0, doesNotExist()
 	}
 	// if file exists:
-	offset := f.cursor
 	if !f.isOpen {
 		f.isOpen = true
 	}
@@ -130,7 +129,7 @@ func (f *File) Read(p []byte) (n int, err error) {
 	if f.cursor > len(f.contents) {
 		f.cursor = len(f.contents)
 	}
-	return readBufferLength - offset, nil
+	return readBufferLength, nil
 
 }
 
diff --git a/docs/azure.md b/docs/azure.md
new file mode 100644
index 0000000..755ffdc
--- /dev/null
+++ b/docs/azure.md
@@ -0,0 +1,792 @@
+# azure
+--
+    import "github.com/c2fo/vfs/backend/azure"
+
+Package azure Microsoft Azure Blob Storage VFS Implementation
+
+
+### Usage
+
+Rely on github.com/c2fo/vfs/backend
+
+    import(
+        "github.com/c2fo/vfs/v5/backend"
+        "github.com/c2fo/vfs/v5/backend/azure"
+    )
+
+    func UseFs() error {
+        fs := backend.Backend(azure.Scheme)
+        ...
+    }
+
+Or call directly:
+
+    import "github.com/c2fo/vfs/v5/backend/azure"
+
+    func DoSomething() {
+        fs := azure.NewFilesystem()
+        ...
+    }
+
+azure can be augmented with the following implementation-specific methods.
+Backend returns vfs.Filesystem interface so it would have to be cast as
+azure.Filesystem to use the following:
+
+    func DoSomething() {
+
+        ...
+
+        // cast if fs was created using backend.Backend().  Not necessary if created directly from azure.NewFilesystem().
+        fs = fs.(azure.Filesystem)
+
+        // to pass in client options
+        fs = fs.WithOptions(
+            azure.Options{
+                AccountName: "...",
+                AccountKey: "...
+            },
+        )
+
+        // to pass specific client, for instance mock client
+        client, _ := azure.NewClient(MockAzureClient{...})
+        fs = fs.WithClient(client)
+    }
+
+
+### Authentication
+
+Authentication, by default, occurs automatically when Client() is called. It
+looks for credentials in the following places, preferring the first location
+found:
+
+    1. When the ENV vars VFS_AZURE_ENV_NAME, VFS_AZURE_STORAGE_ACCOUNT, VFS_AZURE_TENANT_ID, VFS_AZURE_CLIENT_ID, and
+       VFS_AZURE_CLIENT_SECRET, authentication is performed using an OAuth Token Authenticator.  This will allow access
+       to containers from multiple storage accounts.
+    2. The ENV vars VFS_AZURE_STORAGE_ACCOUNT and VFS_AZURE_STORAGE_KEY, a shared key authenticator is used.  This will
+       allow access to any containers owned by the designated storage account.
+    3. If none of the above are present, then an anonymous authenticator is created and only publicly accessible blobs
+       will be available
+
+## Usage
+
+```go
+const Name = "azure"
+```
+Name defines the name for the azure implementation
+
+```go
+const Scheme = "https"
+```
+Scheme defines the scheme for the azure implementation
+
+#### func  IsValidURI
+
+```go
+func IsValidURI(u *url.URL) bool
+```
+IsValidURI us a utility function used by vfssimple to determine if the given URI
+is a valid Azure URI
+
+#### func  ParsePath
+
+```go
+func ParsePath(p string) (host, pth string, err error)
+```
+ParsePath is a utility function used by vfssiple to separate the host from the
+path. The first parameter returned is the host and the second parameter is the
+path.
+
+#### type BlobProperties
+
+```go
+type BlobProperties struct {
+	// Size holds the size of the blob.
+	Size uint64
+
+	// LastModified holds the last modified time.Time
+	LastModified *time.Time
+
+	// Metadata holds the Azure metadata
+	Metadata map[string]string
+}
+```
+
+BlobProperties holds a subset of information returned by Blob.GetProperties(..)
+
+#### func  NewBlobProperties
+
+```go
+func NewBlobProperties(azureProps *azblob.BlobGetPropertiesResponse) *BlobProperties
+```
+NewBlobProperties creates a new BlobProperties from an
+azblob.BlobGetPropertiesResponse
+
+#### type Client
+
+```go
+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)
+
+	// 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
+
+	// 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
+
+	// Download should return a reader for the blob specified by the file parameter
+	Download(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
+
+	// List should return a listing for the specified location. Listings should include the full path for the file.
+	List(l vfs.Location) ([]string, error)
+
+	// Delete should delete the file specified by the parameter file.
+	Delete(file vfs.File) error
+}
+```
+
+The Client interface contains methods that perform specific operations to Azure
+Blob Storage. This interface is here so we can write mocks over the actual
+functionality.
+
+#### type DefaultClient
+
+```go
+type DefaultClient struct {
+}
+```
+
+DefaultClient is the main implementation that actually makes the calls to Azure
+Blob Storage
+
+#### func  NewClient
+
+```go
+func NewClient(options *Options) (*DefaultClient, error)
+```
+NewClient initializes a new DefaultClient
+
+#### func (*DefaultClient) Copy
+
+```go
+func (a *DefaultClient) Copy(srcFile, tgtFile vfs.File) 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 (*DefaultClient) Delete
+
+```go
+func (a *DefaultClient) Delete(file vfs.File) error
+```
+Delete deletes the given file from Azure Blob Storage.
+
+#### func (*DefaultClient) Download
+
+```go
+func (a *DefaultClient) Download(file vfs.File) (io.ReadCloser, error)
+```
+Download returns an io.ReadCloser for the given vfs.File
+
+#### func (*DefaultClient) List
+
+```go
+func (a *DefaultClient) List(l vfs.Location) ([]string, 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 (incliding the
+virtual 'path').
+
+#### func (*DefaultClient) Properties
+
+```go
+func (a *DefaultClient) Properties(containerURI, filePath string) (*BlobProperties, error)
+```
+Properties fetches the properties for the blob specified by the parameters
+containerURI and filePath
+
+#### func (*DefaultClient) SetMetadata
+
+```go
+func (a *DefaultClient) SetMetadata(file vfs.File, metadata map[string]string) error
+```
+SetMetadata sets the given metadata for the blob
+
+#### func (*DefaultClient) Upload
+
+```go
+func (a *DefaultClient) Upload(file vfs.File, content io.ReadSeeker) error
+```
+Upload uploads a new file to Azure Blob Storage
+
+#### type DefaultTokenCredentialFactory
+
+```go
+type DefaultTokenCredentialFactory struct{}
+```
+
+DefaultTokenCredentialFactory knows how to make azblob.TokenCredential structs
+for OAuth authentication
+
+#### func (*DefaultTokenCredentialFactory) New
+
+```go
+func (f *DefaultTokenCredentialFactory) New(tenantID, clientID, clientSecret, azureEnvName string) (azblob.TokenCredential, error)
+```
+New creates a new azblob.TokenCredntial struct
+
+#### type File
+
+```go
+type File struct {
+}
+```
+
+File implements the vfs.File interface for Azure Blob Storage
+
+#### func (*File) Close
+
+```go
+func (f *File) Close() error
+```
+Close cleans up all of the backing data structures used for reading/writing
+files. This includes, closing the temp file, uploading the contents of the temp
+file to Azure Blob Storage (if necessary), and calling Seek(0, 0).
+
+#### func (*File) CopyToFile
+
+```go
+func (f *File) CopyToFile(file vfs.File) error
+```
+CopyToFile puts the contents of the receiver (f *File) into the passed vfs.File
+parameter.
+
+#### func (*File) CopyToLocation
+
+```go
+func (f *File) CopyToLocation(location vfs.Location) (vfs.File, error)
+```
+CopyToLocation creates a copy of *File, using the file's current name as the new
+file's name at the given location. If the given location is also azure, the
+azure API for copying files will be utilized, otherwise, standard io.Copy will
+be done to the new file.
+
+#### func (*File) Delete
+
+```go
+func (f *File) Delete() error
+```
+Delete deletes the file.
+
+#### func (*File) Exists
+
+```go
+func (f *File) Exists() (bool, error)
+```
+Exists returns true/false if the file exists/does not exist on Azure
+
+#### func (*File) LastModified
+
+```go
+func (f *File) LastModified() (*time.Time, error)
+```
+LastModified returns the last modified time as a time.Time
+
+#### func (*File) Location
+
+```go
+func (f *File) Location() vfs.Location
+```
+Location returns a Location instance for the files current location
+
+#### func (*File) MoveToFile
+
+```go
+func (f *File) MoveToFile(file vfs.File) error
+```
+MoveToFile copies the receiver to the specified file and deletes the original
+file.
+
+#### func (*File) MoveToLocation
+
+```go
+func (f *File) MoveToLocation(location vfs.Location) (vfs.File, error)
+```
+MoveToLocation copies the receiver to the passed location. After the copy
+succeeds, the original is deleted.
+
+#### func (*File) Name
+
+```go
+func (f *File) Name() string
+```
+Name returns the name of the file
+
+#### func (*File) Path
+
+```go
+func (f *File) Path() string
+```
+Path returns full path with leading slash.
+
+#### func (*File) Read
+
+```go
+func (f *File) Read(p []byte) (n int, err error)
+```
+Read implements the io.Reader interface. For this to work with Azure Blob
+Storage, a temporary local copy of the file is created and read operations are
+performed against that. The temp file is closed and flushed to Azure when
+f.Close() is called.
+
+#### func (*File) Seek
+
+```go
+func (f *File) Seek(offset int64, whence int) (int64, error)
+```
+Seek implements the io.Seeker interface. For this to work with Azure Blob
+Storage, a temporary local copy of the file is created and operations are
+performed against that. The temp file is closed and flushed to Azure when
+f.Close() is called.
+
+#### func (*File) Size
+
+```go
+func (f *File) Size() (uint64, error)
+```
+Size returns the size of the blob
+
+#### func (*File) String
+
+```go
+func (f *File) String() string
+```
+String returns the file URI
+
+#### func (*File) Touch
+
+```go
+func (f *File) Touch() error
+```
+Touch creates a zero-length file on the vfs.File if no File exists. If the file
+exists, Touch updates the file's last modified parameter.
+
+#### func (*File) URI
+
+```go
+func (f *File) URI() string
+```
+URI returns a full Azure URI for the file
+
+#### func (*File) Write
+
+```go
+func (f *File) Write(p []byte) (int, error)
+```
+Write implements the io.Writer interface. Writes are performed against a
+temporary local file. The temp file is closed and flushed to Azure with
+f.Close() is called.
+
+#### type FileSystem
+
+```go
+type FileSystem struct {
+}
+```
+
+FileSystem implements the vfs.FileSystem interface for Azure Blob Storage
+
+#### func  NewFileSystem
+
+```go
+func NewFileSystem() *FileSystem
+```
+NewFileSystem creates a new default FileSystem. This will set the options
+options.AccountName and options.AccountKey with the env variables
+AZURE_STORAGE_ACCOUNT and AZURE_STORAGE_ACCESS_KEY respectively.
+
+#### func (*FileSystem) Client
+
+```go
+func (fs *FileSystem) Client() (Client, error)
+```
+Client returns a Client to perform operations against Azure Blob Storage
+
+#### func (*FileSystem) Host
+
+```go
+func (fs *FileSystem) Host() string
+```
+Host returns the host portion of the URI. For azure this consists of
+<account_name>.blob.core.windows.net.
+
+#### func (*FileSystem) Name
+
+```go
+func (fs *FileSystem) Name() string
+```
+Name returns "azure"
+
+#### func (*FileSystem) NewFile
+
+```go
+func (fs *FileSystem) NewFile(volume, absFilePath string) (vfs.File, error)
+```
+NewFile returns the azure implementation of vfs.File
+
+#### func (*FileSystem) NewLocation
+
+```go
+func (fs *FileSystem) NewLocation(volume, absLocPath string) (vfs.Location, error)
+```
+NewLocation returns the azure implementation of vfs.Location
+
+#### func (*FileSystem) Retry
+
+```go
+func (fs *FileSystem) Retry() vfs.Retry
+```
+Retry returns the default retry function. This is overridable via the
+WithOptions function.
+
+#### func (*FileSystem) Scheme
+
+```go
+func (fs *FileSystem) Scheme() string
+```
+Scheme returns "https" as the initial part of the URI i.e. https://..
+
+#### func (*FileSystem) WithClient
+
+```go
+func (fs *FileSystem) WithClient(client Client) *FileSystem
+```
+WithClient allows the caller to specify a specific client to be used
+
+#### func (*FileSystem) WithOptions
+
+```go
+func (fs *FileSystem) WithOptions(opts vfs.Options) *FileSystem
+```
+WithOptions allows the caller to override the default options
+
+#### type Location
+
+```go
+type Location struct {
+}
+```
+
+Location is the azure implementation of vfs.Location
+
+#### func (*Location) ChangeDir
+
+```go
+func (l *Location) ChangeDir(relLocPath string) error
+```
+ChangeDir changes the current location's path to the new, relative path.
+
+#### func (*Location) ContainerURL
+
+```go
+func (l *Location) ContainerURL() string
+```
+ContainerURL returns the URL for the Azure Blob Storage container.
+
+#### func (*Location) DeleteFile
+
+```go
+func (l *Location) DeleteFile(relFilePath string) error
+```
+DeleteFile deletes the file at the given path, relative to the current location.
+
+#### func (*Location) Exists
+
+```go
+func (l *Location) Exists() (bool, error)
+```
+Exists returns true if the file exists and false. In the case of errors false is
+always returned along with the error
+
+#### func (*Location) FileSystem
+
+```go
+func (l *Location) FileSystem() vfs.FileSystem
+```
+FileSystem returns the azure FileSystem instance
+
+#### func (*Location) List
+
+```go
+func (l *Location) List() ([]string, error)
+```
+List returns a list of base names for the given location.
+
+#### func (*Location) ListByPrefix
+
+```go
+func (l *Location) ListByPrefix(prefix string) ([]string, error)
+```
+ListByPrefix returns a list of base names that contain the given prefix
+
+#### func (*Location) ListByRegex
+
+```go
+func (l *Location) ListByRegex(regex *regexp.Regexp) ([]string, error)
+```
+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)
+```
+NewFile returns a new file instance at the given path, relative to the current
+location.
+
+#### func (*Location) NewLocation
+
+```go
+func (l *Location) NewLocation(relLocPath string) (vfs.Location, error)
+```
+NewLocation creates a new location instance relative to the current location's
+path.
+
+#### func (*Location) Path
+
+```go
+func (l *Location) Path() string
+```
+Path returns the absolute path for the Location
+
+#### func (*Location) String
+
+```go
+func (l *Location) String() string
+```
+String returns the URI
+
+#### func (*Location) URI
+
+```go
+func (l *Location) URI() string
+```
+URI returns a URI string for the azure location.
+
+#### func (*Location) Volume
+
+```go
+func (l *Location) Volume() string
+```
+Volume returns the azure container. Azure containers are equivalent to AWS
+Buckets
+
+#### type MockAzureClient
+
+```go
+type MockAzureClient struct {
+	PropertiesError  error
+	PropertiesResult *BlobProperties
+	ExpectedError    error
+	ExpectedResult   interface{}
+}
+```
+
+MockAzureClient is a mock implementation of azure.Client.
+
+#### func (*MockAzureClient) Copy
+
+```go
+func (a *MockAzureClient) Copy(srcFile, tgtFile vfs.File) error
+```
+Copy returns the value of ExpectedError
+
+#### func (*MockAzureClient) Delete
+
+```go
+func (a *MockAzureClient) Delete(file vfs.File) error
+```
+Delete returns the value of ExpectedError
+
+#### func (*MockAzureClient) Download
+
+```go
+func (a *MockAzureClient) Download(file vfs.File) (io.ReadCloser, error)
+```
+Download returns ExpectedResult if it exists, otherwise it returns ExpectedError
+
+#### func (*MockAzureClient) List
+
+```go
+func (a *MockAzureClient) List(l vfs.Location) ([]string, error)
+```
+List returns the value of ExpectedResult if it exists, otherwise it returns
+ExpectedError.
+
+#### func (*MockAzureClient) Properties
+
+```go
+func (a *MockAzureClient) Properties(locationURI, filePath string) (*BlobProperties, error)
+```
+Properties returns a PropertiesResult if it exists, otherwise it will return the
+value of PropertiesError
+
+#### func (*MockAzureClient) SetMetadata
+
+```go
+func (a *MockAzureClient) SetMetadata(file vfs.File, metadata map[string]string) error
+```
+SetMetadata returns the value of ExpectedError
+
+#### func (*MockAzureClient) Upload
+
+```go
+func (a *MockAzureClient) Upload(file vfs.File, content io.ReadSeeker) error
+```
+Upload returns the value of ExpectedError
+
+#### type MockStorageError
+
+```go
+type MockStorageError struct {
+	azblob.ResponseError
+}
+```
+
+MockStorageError is a mock for the azblob.StorageError interface
+
+#### func (MockStorageError) Error
+
+```go
+func (mse MockStorageError) Error() string
+```
+Error returns empty string
+
+#### func (MockStorageError) Response
+
+```go
+func (mse MockStorageError) Response() *http.Response
+```
+Response returns nil
+
+#### func (MockStorageError) ServiceCode
+
+```go
+func (mse MockStorageError) ServiceCode() azblob.ServiceCodeType
+```
+ServiceCode always returns "BlobNotFound" to simulate the not found condition
+
+#### func (MockStorageError) Temporary
+
+```go
+func (mse MockStorageError) Temporary() bool
+```
+Temporary returns nil
+
+#### func (MockStorageError) Timeout
+
+```go
+func (mse MockStorageError) Timeout() bool
+```
+Timeout returns nil
+
+#### type MockTokenCredentialFactory
+
+```go
+type MockTokenCredentialFactory struct{}
+```
+
+MockTokenCredentialFactory knows how to create a "do-nothing" credential used
+for unit testing
+
+#### func (*MockTokenCredentialFactory) New
+
+```go
+func (f *MockTokenCredentialFactory) New(tenantID, clientID, clientSecret, azureEnvName string) (azblob.TokenCredential, error)
+```
+New creates a new azblob.TokenCredntial struct
+
+#### type Options
+
+```go
+type Options struct {
+	// AccountName holds the Azure Blob Storage account name for authentication.  This field is required for all
+	// authentication types.
+	AccountName string
+
+	// AccountKey holds the Azure Blob Storage account key for authentication.  This field is used for shared key
+	// authentication.
+	AccountKey string
+
+	// TenantID holds the Azure Service Account tenant id for authentication.  This field is used for OAuth token
+	// based authentication.
+	TenantID string
+
+	// ClientID holds the Azure Service Account client id for authentication.  This field is used for OAuth token
+	// based authentication.
+	ClientID string
+
+	// ClientSecret holds the Azure Service Account client secret for authentication.  This field is used for OAuth token
+	// based authentication.
+	ClientSecret string
+
+	// AzureEnvName holds the name for the Azure environment.  This field is used for OAuth token
+	// based authentication.
+	AzureEnvName string
+
+	// RetryFunc holds the retry function
+	RetryFunc vfs.Retry
+}
+```
+
+Options contains options necessary for the azure vfs implementation
+
+#### func  NewOptions
+
+```go
+func NewOptions() *Options
+```
+NewOptions creates a new Options struct by populating values from environment
+variables.
+
+    Env Vars:
+      *VFS_AZURE_STORAGE_ACCOUNT
+      *VFS_AZURE_STORAGE_ACCESS_KEY
+      *VFS_AZURE_TENANT_ID
+      *VFS_AZURE_CLIENT_ID
+      *VFS_AZURE_CLIENT_SECRET
+      *VFS_AZURE_ENV_NAME
+
+#### func (*Options) Credential
+
+```go
+func (o *Options) Credential() (azblob.Credential, error)
+```
+Credential returns an azblob.Credential struct based on how options are
+configured. Options are checked and evaluated in the following order:
+
+    1. If TenantID, ClientID, and ClientSecret are non-empty, return azblob.TokenCredential.  This form of authentication
+       is used with service accounts and can be used to access containers across multiple storage accounts.
+    2. If AccountName, and AccountKey are non-empty, return azblob.SharedKeyCredential.  This form or authentication
+       is used with storage accounts and only provides access to a single storage account.
+    3. Returns an anonymous credential.  This allows access only to public blobs.
+
+#### type TokenCredentialFactory
+
+```go
+type TokenCredentialFactory interface {
+	// New creates a new azblob.TokenCredntial struct
+	New(tenantID, clientID, clientSecret, azureEnvName string) (azblob.TokenCredential, error)
+}
+```
+
+TokenCredentialFactory is an interface that provides a single factory method to
+create azure.TokenCredentials. This interface is provided to allow for mocking
+in unit tests.


Test patch:
diff --git a/backend/azure/client_integration_test.go b/backend/azure/client_integration_test.go
index 31a7d70..7b0d7d8 100644
--- a/backend/azure/client_integration_test.go
+++ b/backend/azure/client_integration_test.go
@@ -1,4 +1,4 @@
-//+build vfsintegration
+// +build vfsintegration
 
 package azure
 
@@ -23,199 +23,202 @@ type ClientIntegrationTestSuite struct {
 	accountKey       string
 }
 
-func (suite *ClientIntegrationTestSuite) SetupSuite() {
-	suite.accountName, suite.accountKey = os.Getenv("VFS_AZURE_STORAGE_ACCOUNT"), os.Getenv("VFS_AZURE_STORAGE_ACCESS_KEY")
+func (s *ClientIntegrationTestSuite) SetupSuite() {
+	s.accountName, s.accountKey = os.Getenv("VFS_AZURE_STORAGE_ACCOUNT"), os.Getenv("VFS_AZURE_STORAGE_ACCESS_KEY")
 
-	credential, err := azblob.NewSharedKeyCredential(suite.accountName, suite.accountKey)
+	credential, err := azblob.NewSharedKeyCredential(s.accountName, s.accountKey)
 	if err != nil {
 		panic(err)
 	}
 
 	p := azblob.NewPipeline(credential, azblob.PipelineOptions{})
-	baseURL, err := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net", suite.accountName))
-	suite.NoError(err)
+	baseURL, err := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net", s.accountName))
+	s.NoError(err)
 	serviceURL := azblob.NewServiceURL(*baseURL, p)
-	suite.testContainerURL = serviceURL.NewContainerURL("test-container")
-	_, err = suite.testContainerURL.Create(context.Background(), azblob.Metadata{}, azblob.PublicAccessNone)
-	//suite.NoError(err)
+	s.testContainerURL = serviceURL.NewContainerURL("test-container")
+	_, err = s.testContainerURL.Create(context.Background(), azblob.Metadata{}, azblob.PublicAccessNone)
+	s.NoError(err)
 
 	// The create function claims to be synchronous but for some reason it does not exist for a little bit so
 	// we need to wait for it to be there.
-	_, err = suite.testContainerURL.GetProperties(context.Background(), azblob.LeaseAccessConditions{})
+	_, err = s.testContainerURL.GetProperties(context.Background(), azblob.LeaseAccessConditions{})
 	for {
 		time.Sleep(2 * time.Second)
 		if err == nil || err.(azblob.StorageError).ServiceCode() != "BlobNotFound" {
 			break
 		}
-		_, err = suite.testContainerURL.GetProperties(context.Background(), azblob.LeaseAccessConditions{})
+		_, err = s.testContainerURL.GetProperties(context.Background(), azblob.LeaseAccessConditions{})
 	}
 }
 
-func (suite *ClientIntegrationTestSuite) TearDownSuite() {
-	_, err := suite.testContainerURL.Delete(context.Background(), azblob.ContainerAccessConditions{})
-	suite.NoError(err)
+func (s *ClientIntegrationTestSuite) TearDownSuite() {
+	_, err := s.testContainerURL.Delete(context.Background(), azblob.ContainerAccessConditions{})
+	s.NoError(err)
 }
 
-func (suite *ClientIntegrationTestSuite) TestAllTheThings_FileWithNoPath() {
+func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithNoPath() {
 	fs := NewFileSystem()
 	f, err := fs.NewFile("test-container", "/test.txt")
-	suite.NoError(err)
+	s.NoError(err)
 	client, err := fs.Client()
-	suite.NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials")
+	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!"))
-	suite.NoError(err, "The file should be successfully uploaded to azure")
+	s.NoError(err, "The file should be successfully uploaded to azure")
 
 	// make sure it exists
 	_, err = client.Properties(f.Location().URI(), f.Name())
-	suite.NoError(err, "If the file exists no error should be returned")
+	s.NoError(err, "If the file exists no error should be returned")
 
 	// download it
 	reader, err := client.Download(f)
-	suite.NoError(err)
+	s.NoError(err)
 	dlContent, err := ioutil.ReadAll(reader)
+	s.NoError(err)
 	err = reader.Close()
-	suite.NoError(err)
-	suite.NoError(err, "there should be no error reading the downloaded file")
-	suite.Equal("Hello world!", string(dlContent))
+	s.NoError(err, "there should be no error reading the downloaded file")
+	s.Equal("Hello world!", string(dlContent))
 
 	// copy it
 	copyOf, err := fs.NewFile("test-container", "/copy_of_test.txt")
+	s.NoError(err)
 	err = client.Copy(f, copyOf)
-	suite.NoError(err, "Copy should succeed so there should be no error")
+	s.NoError(err, "Copy should succeed so there should be no error")
 	_, err = client.Properties(copyOf.Location().URI(), copyOf.Name())
-	suite.NoError(err, "The copy should succeed so we should not get an error on the properties call")
+	s.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())
-	suite.NoError(err)
-	suite.Len(list, 2)
-	suite.Equal("copy_of_test.txt", list[0])
-	suite.Equal("test.txt", list[1])
+	s.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)
-	suite.NoError(err, "if the file was deleted no error should be returned")
+	s.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())
-	suite.Error(err, "File should have been deleted so we should get an error")
+	s.Error(err, "File should have been deleted so we should get an error")
 }
 
-func (suite *ClientIntegrationTestSuite) TestAllTheThings_FileWithPath() {
+func (s *ClientIntegrationTestSuite) TestAllTheThings_FileWithPath() {
 	fs := NewFileSystem()
 	f, err := fs.NewFile("test-container", "/foo/bar/test.txt")
-	suite.NoError(err)
+	s.NoError(err)
 	client, err := fs.Client()
-	suite.NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials")
+	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!"))
-	suite.NoError(err, "The file should be successfully uploaded to azure")
+	s.NoError(err, "The file should be successfully uploaded to azure")
 
-	//check to see if it exists
+	// check to see if it exists
 	_, err = client.Properties(f.Location().(*Location).ContainerURL(), f.Path())
-	suite.NoError(err, "If the file exists no error should be returned")
+	s.NoError(err, "If the file exists no error should be returned")
 
 	// download it
 	reader, err := client.Download(f)
-	suite.NoError(err)
+	s.NoError(err)
 	dlContent, err := ioutil.ReadAll(reader)
+	s.NoError(err)
 	err = reader.Close()
-	suite.NoError(err)
-	suite.NoError(err, "there should be no error reading the downloaded file")
-	suite.Equal("Hello world!", string(dlContent))
+	s.NoError(err)
+	s.NoError(err, "there should be no error reading the downloaded file")
+	s.Equal("Hello world!", string(dlContent))
 
 	// list the location
 	list, err := client.List(f.Location())
-	suite.NoError(err)
-	suite.Len(list, 1)
-	suite.Equal("foo/bar/test.txt", list[0])
+	s.NoError(err)
+	s.Len(list, 1)
+	s.Equal("foo/bar/test.txt", list[0])
 }
 
-func (suite *ClientIntegrationTestSuite) TestProperties() {
+func (s *ClientIntegrationTestSuite) TestProperties() {
 	fs := NewFileSystem()
 	f, err := fs.NewFile("test-container", "/foo/bar/test.txt")
-	suite.NoError(err)
+	s.NoError(err)
 	client, err := fs.Client()
-	suite.NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials")
+	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!"))
-	suite.NoError(err, "The file should be successfully uploaded to azure so we shouldn't get an error")
+	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())
-	suite.NoError(err, "Tne file exists so we shouldn't get an error")
-	suite.NotNil(props, "We should get a non-nil BlobProperties pointer back")
-	suite.Greater(props.Size, uint64(0), "The size should be greater than zero")
-	suite.NotNil(props.LastModified, "Should have a non-nil LastModified")
+	s.NoError(err, "Tne file exists so we shouldn't get an error")
+	s.NotNil(props, "We should get a non-nil BlobProperties pointer back")
+	s.Greater(props.Size, uint64(0), "The size should be greater than zero")
+	s.NotNil(props.LastModified, "Should have a non-nil LastModified")
 }
 
-func (suite *ClientIntegrationTestSuite) TestProperties_Location() {
+func (s *ClientIntegrationTestSuite) TestProperties_Location() {
 	fs := NewFileSystem()
 	f, err := fs.NewFile("test-container", "/foo/bar/test.txt")
+	s.NoError(err)
 	l, _ := fs.NewLocation("test-container", "/")
 	client, _ := fs.Client()
 
 	err = client.Upload(f, strings.NewReader("Hello world!"))
-	suite.NoError(err, "The file should be successfully uploaded to azure so we shouldn't get an error")
+	s.NoError(err, "The file should be successfully uploaded to azure so we shouldn't get an error")
 
 	props, err := client.Properties(l.URI(), "")
-	suite.NoError(err)
-	suite.Nil(props, "no props returned when calling properties on a location")
+	s.NoError(err)
+	s.Nil(props, "no props returned when calling properties on a location")
 }
 
-func (suite *ClientIntegrationTestSuite) TestProperties_NonExistentFile() {
+func (s *ClientIntegrationTestSuite) TestProperties_NonExistentFile() {
 	fs := NewFileSystem()
 	f, err := fs.NewFile("test-container", "/nosuchfile.txt")
-	suite.NoError(err)
+	s.NoError(err)
 	client, err := fs.Client()
-	suite.NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials")
+	s.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())
-	suite.Error(err, "Tne file does not exist so we expect an error")
-	suite.Equal(404, err.(azblob.ResponseError).Response().StatusCode)
+	s.Error(err, "Tne file does not exist so we expect an error")
+	s.Equal(404, err.(azblob.ResponseError).Response().StatusCode)
 }
 
-func (suite *ClientIntegrationTestSuite) TestDelete_NonExistentFile() {
+func (s *ClientIntegrationTestSuite) TestDelete_NonExistentFile() {
 	fs := NewFileSystem()
 	f, err := fs.NewFile("test-container", "/nosuchfile.txt")
-	suite.NoError(err)
+	s.NoError(err)
 	client, err := fs.Client()
-	suite.NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials")
+	s.NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials")
 
 	err = client.Delete(f)
-	suite.Error(err, "Tne file does not exist so we expect an error")
+	s.Error(err, "Tne file does not exist so we expect an error")
 }
 
-func (suite *ClientIntegrationTestSuite) TestTouch_NonexistantContainer() {
+func (s *ClientIntegrationTestSuite) TestTouch_NonexistantContainer() {
 	fs := NewFileSystem()
 	f, err := fs.NewFile("nosuchcontainer", "/file.txt")
-	suite.NoError(err)
+	s.NoError(err)
 	client, err := fs.Client()
-	suite.NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials")
+	s.NoError(err, "Env variables (AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_ACCESS_KEY) should contain valid azure account credentials")
 
 	err = client.Upload(f, strings.NewReader(""))
-	suite.Error(err, "The container doesn't exist so we should get an error")
+	s.Error(err, "The container doesn't exist so we should get an error")
 }
 
-func (suite *ClientIntegrationTestSuite) TestTouch_FileAlreadyExists() {
+func (s *ClientIntegrationTestSuite) TestTouch_FileAlreadyExists() {
 	fs := NewFileSystem()
 	f, err := fs.NewFile("test-container", "/touch-test.txt")
-	suite.NoError(err)
+	s.NoError(err)
 	client, err := fs.Client()
-	suite.NoError(err)
+	s.NoError(err)
 
 	err = client.Upload(f, strings.NewReader("One fish, two fish, red fish, blue fish."))
-	suite.NoError(err)
+	s.NoError(err)
 	originalProps, err := client.Properties(f.Location().(*Location).ContainerURL(), f.Path())
-	suite.NoError(err, "Should get properties back from azure with no error")
+	s.NoError(err, "Should get properties back from azure with no error")
 
 	err = f.Touch()
-	suite.NoError(err, "Should not receive an error when touching an existing file")
+	s.NoError(err, "Should not receive an error when touching an existing file")
 	newProps, err := client.Properties(f.Location().(*Location).ContainerURL(), f.Path())
-	suite.NoError(err)
-	suite.NotNil(newProps, "New props should be non-nil")
-	suite.True(newProps.LastModified.After(*originalProps.LastModified), "newProps.LastModified should be after originalProps.LastModified")
+	s.NoError(err)
+	s.NotNil(newProps, "New props should be non-nil")
+	s.True(newProps.LastModified.After(*originalProps.LastModified), "newProps.LastModified should be after originalProps.LastModified")
 }
 
 func TestAzureClient(t *testing.T) {
diff --git a/backend/mem/file_test.go b/backend/mem/file_test.go
index fc72274..de53001 100644
--- a/backend/mem/file_test.go
+++ b/backend/mem/file_test.go
@@ -1,6 +1,7 @@
 package mem
 
 import (
+	"io"
 	"io/ioutil"
 	"log"
 	"os"
@@ -603,6 +604,36 @@ func (s *memFileTest) TestWrite() {
 
 }
 
+// TestRead ensures read can be called successively to get an entire file's contents in chunks
+func (s *memFileTest) TestRead() {
+	expectedSlice := []byte("Hello World!")
+	length := len(expectedSlice)
+	fileToRead, err := s.fileSystem.NewFile("", "/fileToRead.txt")
+	s.NoError(err, "unexpected new file error")
+	num, err := fileToRead.Write(expectedSlice)
+	s.NoError(err, "unexpected write error")
+	s.EqualValues(length, num)
+	s.NoError(fileToRead.Close(), "close error not expected")
+
+	chunk1 := make([]byte, 5)
+	num, err = fileToRead.Read(chunk1)
+	s.NoError(err, "unexpected read error")
+	s.EqualValues(5, num)
+	s.EqualValues(expectedSlice[:5], chunk1)
+
+	chunk2 := make([]byte, 5)
+	num, err = fileToRead.Read(chunk2)
+	s.NoError(err, "unexpected read error")
+	s.EqualValues(5, num)
+	s.EqualValues(expectedSlice[5:10], chunk2)
+
+	chunk3 := make([]byte, 5)
+	num, err = fileToRead.Read(chunk3)
+	s.EqualValues(io.EOF, err)
+	s.EqualValues(2, num)
+	s.EqualValues(expectedSlice[10:], chunk3[:2])
+}
+
 // TestWriteThenReadNoClose writes to a file, and reads from it without closing it by seeking to the start
 func (s *memFileTest) TestWriteThenReadNoClose() {
 	expectedText := "new file"