Repository: C2FO/vfs
Pull request #25 (2019-07-08 07:51:10)

Touch

Roosh513: Is there a reason to keep TouchCopy around other than passivity?
funkyshu: > Is there a reason to keep TouchCopy around other than passivity?

Mostly because of the note described at https://github.com/C2FO/vfs/pull/25/files#diff-04c6e90faac2675aa89e2176d2eec7d8R121 ... it COULD be removed but may actually still be useful.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b1ece8..a4a0891 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+## [5.0.1] - 2019-07-08
+### Added
+- Added Touch() method to File interface and implemented in each backend.
+- Increased test coverage in both os and s3 backends.
 
 ## [5.0.0] - 2019-06-18
 ### Changed
@@ -23,7 +27,7 @@ between implementations.
 - Fixed [#10](https://github.com/C2FO/vfs/issues/10)
 - Fixed vfssimple bug that would return os file system if the uri had the word 'file' anywhere in it even if was s3, like s3://bucket/path/to/file.txt
 - Fixed bug where MoveToLocation was returning the old file with updated location rather
-than a brand new file with new location.  Since File may represent a non-existant file, the original file should still be valid.
+than a brand new file with new location.  Since File may represent a non-existent file, the original file should still be valid.
 
 ## [4.0.0] - 2019-05-22
 
diff --git a/README.md b/README.md
index c3898b8..c084cee 100644
--- a/README.md
+++ b/README.md
@@ -106,9 +106,8 @@ File's [io.*](https://godoc.org/io) interfaces may be used directly:
 
     byteCount, err := io.Copy(gsFile, reader)
     err := gsFile.Close()
-    
-###### Note:
-[io.Copy()](https://godoc.org/io#Copy) doesn't strictly define what happens if a reader is empty.  This is complicated because io.Copy
+
+Note: [io.Copy()](https://godoc.org/io#Copy) doesn't strictly define what happens if a reader is empty.  This is complicated because io.Copy
 will first delegate actual copying in the following:
   1. if the io.Reader also implements io.WriterTo, WriteTo() will do the copy
   2. if the io.Writer also implements io.ReaderFrom, ReadFrom() will do the copy
@@ -153,7 +152,9 @@ Things to add:
 Brought to you by the Enterprise Pipeline team at C2FO:
 
 * John Judd - john.judd@c2fo.com
+
 * Jason Coble - [@jasonkcoble](https://twitter.com/jasonkcoble) - jason@c2fo.com
+
 * Chris Roush – chris.roush@c2fo.com
 
 https://github.com/c2fo/
@@ -402,6 +403,10 @@ type Location interface {
 	// * Accepts relative file path.
 	DeleteFile(relFilePath string) error
 
+	// Touch creates a zero-length file on the vfs.File if no File exists.  Update File's last modified timestamp.
+	// Returns error if unable to touch File.
+	Touch() error
+
 	// URI returns the fully qualified absolute URI for the Location.  IE, s3://bucket/some/path/
 	//
 	// URI's for locations must always end with a slash.
diff --git a/backend/gs/file.go b/backend/gs/file.go
index 5dd64a2..be7379e 100644
--- a/backend/gs/file.go
+++ b/backend/gs/file.go
@@ -164,7 +164,7 @@ func (f *File) CopyToLocation(location vfs.Location) (vfs.File, error) {
 	return newFile, nil
 }
 
-// CopyToFile puts the contents of File into the targetFile passed. Uses the GCS CopierFrom
+// CopyToFile puts the contents of File into the target vfs.File passed in. Uses the GCS CopierFrom
 // method if the target file is also on GCS, otherwise uses io.Copy.
 func (f *File) CopyToFile(file vfs.File) error {
 	if tf, ok := file.(*File); ok {
@@ -195,7 +195,7 @@ func (f *File) MoveToLocation(location vfs.Location) (vfs.File, error) {
 	return newFile, delErr
 }
 
-// MoveToFile puts the contents of File into the target vfs.File passed passed in using File.CopyToFile.
+// MoveToFile puts the contents of File into the target vfs.File passed in using File.CopyToFile.
 // If the copy succeeds, the source file is deleted. Any errors from the copy or delete are
 // returned.
 func (f *File) MoveToFile(file vfs.File) error {
@@ -220,6 +220,113 @@ func (f *File) Delete() error {
 	return handle.Delete(f.fileSystem.ctx)
 }
 
+// Touch creates a zero-length file on the vfs.File if no File exists.  Update File's last modified timestamp.
+// Returns error if unable to touch File.
+func (f *File) Touch() error {
+
+	//check if file exists
+	exists, err := f.Exists()
+	if err != nil {
+		return err
+	}
+
+	// if file doesn't already exist, create it
+	if !exists {
+		return f.createEmptyFile()
+	}
+
+	// already exists so update it so Last-Modified is updated
+
+	//  With versioning enabled, updates to custom meta won't update Updated date:
+	//  from https://godoc.org/cloud.google.com/go/storage#ObjectAttrs:
+	//        // Updated is the creation or modification time of the object.
+	//        // For buckets with versioning enabled, changing an object's
+	//        // metadata does not change this property. This field is read-only.
+	//        Updated time.Time
+
+	enabled, err := f.isBucketVersioningEnabled()
+	if err != nil {
+		return err
+	}
+
+	if enabled {
+		return utils.UpdateLastModifiedByMoving(f)
+	}
+
+	return f.updateLastModifiedByAttrUpdate()
+}
+
+func (f *File) updateLastModifiedByAttrUpdate() error {
+
+	//save original metadata (in case it was set already)
+	objAttrs, err := f.getObjectAttrs()
+	if err != nil {
+		return err
+	}
+	oldMetaData := objAttrs.Metadata
+
+	// setup dummy metadata tag and update object with it
+	var updateAttrs storage.ObjectAttrsToUpdate
+	updateAttrs.Metadata = map[string]string{"updateMe": "true"}
+
+	obj, err := f.getObjectHandle()
+	if err != nil {
+		return err
+	}
+
+	cctx, cancel := context.WithCancel(f.fileSystem.ctx)
+	defer func() { cancel() }()
+
+	_, err = obj.Update(cctx, updateAttrs)
+	if err != nil {
+		return err
+	}
+
+	//now switch metadata back to original values
+	updateAttrs.Metadata = oldMetaData
+	_, err = obj.Update(cctx, updateAttrs)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (f *File) isBucketVersioningEnabled() (bool, error) {
+	client, err := f.fileSystem.Client()
+	if err != nil {
+		return false, err
+	}
+	cctx, cancel := context.WithCancel(f.fileSystem.ctx)
+	defer func() { cancel() }()
+	attrs, err := client.Bucket(f.bucket).Attrs(cctx)
+	if err != nil {
+		return false, err
+	}
+	return attrs.VersioningEnabled, nil
+}
+
+func (f *File) createEmptyFile() error {
+
+	handle, err := f.getObjectHandle()
+	if err != nil {
+		return err
+	}
+
+	//write zero length file.
+	ctx, cancel := context.WithCancel(f.fileSystem.ctx)
+	defer func() { cancel() }()
+
+	w := handle.NewWriter(ctx)
+	defer func() { _ = w.Close() }()
+	if _, err := w.Write(make([]byte, 0)); err != nil {
+		return err
+	}
+
+	// return early
+	return nil
+}
+
 // LastModified returns the 'Updated' property from the GCS attributes.
 func (f *File) LastModified() (*time.Time, error) {
 	attr, err := f.getObjectAttrs()
diff --git a/backend/os/file.go b/backend/os/file.go
index fb51756..3af47bc 100644
--- a/backend/os/file.go
+++ b/backend/os/file.go
@@ -197,6 +197,26 @@ func (f *File) String() string {
 	return f.URI()
 }
 
+// Touch creates a zero-length file on the vfs.File if no File exists.  Update File's last modified timestamp.
+// Returns error if unable to touch File.
+func (f *File) Touch() error {
+	exists, err := f.Exists()
+	if err != nil {
+		return err
+	}
+
+	if !exists {
+		file, err := f.openFile()
+		if err != nil {
+			return err
+		}
+		f.file = file
+		return f.Close()
+	}
+	now := time.Now()
+	return os.Chtimes(f.Path(), now, now)
+}
+
 func (f *File) copyWithName(name string, location vfs.Location) (vfs.File, error) {
 	newFile, err := location.FileSystem().NewFile(location.Volume(), path.Join(location.Path(), name))
 	if err != nil {
diff --git a/backend/s3/file.go b/backend/s3/file.go
index c73d4bd..d8ae64e 100644
--- a/backend/s3/file.go
+++ b/backend/s3/file.go
@@ -133,28 +133,12 @@ func (f *File) MoveToLocation(location vfs.Location) (vfs.File, error) {
 // name at the given location. If the given location is also s3, the AWS API for copying
 // files will be utilized, otherwise, standard io.Copy will be done to the new file.
 func (f *File) CopyToLocation(location vfs.Location) (vfs.File, error) {
-	// This is a copy to s3, from s3, we should attempt to utilize the AWS S3 API for this.
-	if location.FileSystem().Scheme() == Scheme {
-		return f.copyWithinS3ToLocation(location)
-	}
-
-	newFile, err := location.FileSystem().NewFile(location.Volume(), path.Join(location.Path(), f.Name()))
+	newFile, err := location.NewFile(f.Name())
 	if err != nil {
 		return nil, err
 	}
 
-	if err := utils.TouchCopy(newFile, f); err != nil {
-		return nil, err
-	}
-	//Close target file to flush and ensure that cursor isn't at the end of the file when the caller reopens for read
-	if cerr := newFile.Close(); cerr != nil {
-		return nil, cerr
-	}
-	//Close file (f) reader
-	if cerr := f.Close(); cerr != nil {
-		return nil, cerr
-	}
-	return newFile, nil
+	return newFile, f.CopyToFile(newFile)
 }
 
 // CRUD Operations
@@ -184,7 +168,7 @@ func (f *File) Delete() error {
 func (f *File) Close() error {
 
 	if f.tempFile != nil {
-		defer f.tempFile.Close()
+		defer func() { _ = f.tempFile.Close() }()
 
 		err := os.Remove(f.tempFile.Name())
 		if err != nil && !os.IsNotExist(err) {
@@ -253,6 +237,34 @@ func (f *File) Write(data []byte) (res int, err error) {
 	return f.writeBuffer.Write(data)
 }
 
+// Touch creates a zero-length file on the vfs.File if no File exists.  Update File's last modified timestamp.
+// Returns error if unable to touch File.
+func (f *File) Touch() error {
+
+	//check if file exists
+	exists, err := f.Exists()
+	if err != nil {
+		return err
+	}
+
+	// file doesn't already exist so create it
+	if !exists {
+		_, err = f.Write([]byte(""))
+		if err != nil {
+			return err
+		}
+
+		if err := f.Close(); err != nil {
+			return err
+		}
+	} else {
+		// file already exists so update its last modified date
+		return utils.UpdateLastModifiedByMoving(f)
+	}
+
+	return nil
+}
+
 // URI returns the File's URI as a string.
 func (f *File) URI() string {
 	return utils.GetFileURI(f)
@@ -289,24 +301,6 @@ func (f *File) copyWithinS3ToFile(targetFile *File) error {
 	return err
 }
 
-func (f *File) copyWithinS3ToLocation(location vfs.Location) (vfs.File, error) {
-	copyInput := new(s3.CopyObjectInput).
-		SetKey(path.Join(location.Path(), f.Name())).
-		SetBucket(location.Volume()).
-		SetCopySource(path.Join(f.bucket, f.key))
-
-	client, err := f.fileSystem.Client()
-	if err != nil {
-		return nil, err
-	}
-	_, err = client.CopyObject(copyInput)
-	if err != nil {
-		return nil, err
-	}
-
-	return location.FileSystem().NewFile(location.Volume(), path.Join(location.Path(), f.Name()))
-}
-
 func (f *File) checkTempFile() error {
 	if f.tempFile == nil {
 		localTempFile, err := f.copyToLocalTempReader()
diff --git a/utils/utils.go b/utils/utils.go
index ddf44b7..4126e9e 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"regexp"
 	"strings"
+	"time"
 
 	"github.com/c2fo/vfs/v5"
 )
@@ -112,3 +113,23 @@ func TouchCopy(writer, reader vfs.File) error {
 	}
 	return nil
 }
+
+// UpdateLastModifiedByMoving is used by some backends' Touch() method when a file already exists.
+func UpdateLastModifiedByMoving(file vfs.File) error {
+	// setup a tempfile
+	tempfile, err := file.Location().
+		NewFile(fmt.Sprintf("%s.%d", file.Name(), time.Now().UnixNano()))
+
+	// copy file file to tempfile
+	err = file.CopyToFile(tempfile)
+	if err != nil {
+		return err
+	}
+
+	// move tempfile back to file
+	err = tempfile.MoveToFile(file)
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/vfs.go b/vfs.go
index 86d9ff7..f0a4635 100644
--- a/vfs.go
+++ b/vfs.go
@@ -202,6 +202,10 @@ type File interface {
 	// For file:///some/path/to/file.txt, it would return file.txt
 	Name() string
 
+	// Touch creates a zero-length file on the vfs.File if no File exists.  Update File's last modified timestamp.
+	// Returns error if unable to touch File.
+	Touch() error
+
 	// URI returns the fully qualified absolute URI for the File.  IE, s3://bucket/some/path/to/file.txt
 	URI() string
 }


Test patch:
diff --git a/backend/os/file_test.go b/backend/os/file_test.go
index ad52f94..558af78 100644
--- a/backend/os/file_test.go
+++ b/backend/os/file_test.go
@@ -64,6 +64,50 @@ func (s *osFileTest) TestExists() {
 	s.False(otherFileExists)
 }
 
+func (s *osFileTest) TestTouch() {
+
+	//set up testfile
+	testfile, err := s.tmploc.NewFile("test_files/foo.txt")
+	s.NoError(err)
+
+	// testfile should NOT exist
+	exists, err := testfile.Exists()
+	s.NoError(err)
+	s.False(exists)
+
+	//touch file
+	err = testfile.Touch()
+	s.NoError(err)
+
+	// testfile SHOULD exist
+	exists, err = testfile.Exists()
+	s.NoError(err)
+	s.True(exists)
+
+	// size should be zero
+	size, err := testfile.Size()
+	s.NoError(err)
+	s.Equal(size, uint64(0), "size should be zero")
+
+	//capture last_modified
+	firstModTime, err := testfile.LastModified()
+	s.NoError(err)
+
+	//touch again
+	err = testfile.Touch()
+	s.NoError(err)
+
+	// size should still be zero
+	size, err = testfile.Size()
+	s.NoError(err)
+	s.Equal(size, uint64(0), "size should be zero")
+
+	//LastModified should be later than previous LastModified
+	nextModTime, err := testfile.LastModified()
+	s.NoError(err)
+	s.True(firstModTime.Before(*nextModTime), "Last Modified was updated")
+}
+
 func (s *osFileTest) TestOpenFile() {
 	expectedText := "hello world"
 	data := make([]byte, len(expectedText))
@@ -236,6 +280,30 @@ func (s *osFileTest) TestMoveToLocation() {
 	origFound, eerr := origFile.Exists()
 	s.NoError(eerr, "exists error not expected")
 	s.False(origFound)
+
+	// test non-scheme MoveToLocation
+	mockLocation := new(mocks.Location)
+	mockfs := new(mocks.FileSystem)
+
+	// Expected behavior
+	mockfs.On("Scheme").Return("mock")
+	fsMockFile := new(mocks.File)
+	fsMockFile.On("Write", mock.Anything).Return(10, nil)
+	fsMockFile.On("Close").Return(nil)
+	mockfs.On("NewFile", mock.Anything, mock.Anything).Return(fsMockFile, nil)
+	mockLocation.On("FileSystem").Return(mockfs)
+	mockLocation.On("Volume").Return("")
+	mockLocation.On("Path").Return("/some/path/to/")
+	mockLocation.On("Close").Return(nil)
+	mockLocation.On("NewFile", mock.Anything).Return(&File{}, nil)
+	mockFile := new(mocks.File)
+	mockFile.On("Location").Return(mockLocation, nil)
+	mockFile.On("Name").Return("/some/path/to/move.txt")
+	mockFile.On("Location ").Return(mockLocation, nil)
+	mockfs.On("NewLocation", mock.Anything, mock.Anything).Return(mockLocation)
+
+	_, err = movedFile.MoveToLocation(mockLocation)
+	s.NoError(err)
 }
 
 func (s *osFileTest) TestMoveToFile() {
@@ -284,6 +352,28 @@ func (s *osFileTest) TestMoveToFile() {
 	s.NoError(cErr, "close error not expected")
 
 	s.Equal(text, string(data))
+
+	// test non-scheme MoveToFile
+	mockFile := new(mocks.File)
+	mockLocation := new(mocks.Location)
+	mockfs := new(mocks.FileSystem)
+
+	// Expected behavior
+	mockfs.On("Scheme").Return("mock")
+	fsMockFile := new(mocks.File)
+	fsMockFile.On("Write", mock.Anything).Return(13, nil)
+	fsMockFile.On("Close").Return(nil)
+	mockfs.On("NewFile", mock.Anything, mock.Anything).Return(fsMockFile, nil)
+	mockLocation.On("FileSystem").Return(mockfs)
+	mockLocation.On("Volume").Return("")
+	mockLocation.On("Path").Return("/some/path/to/")
+	mockLocation.On("Close").Return(nil)
+	mockFile.On("Location").Return(mockLocation, nil)
+	mockFile.On("Name").Return("/some/path/to/file.txt")
+	mockFile.On("Location ").Return(mockLocation, nil)
+	mockfs.On("NewLocation", mock.Anything, mock.Anything).Return(mockLocation)
+
+	s.NoError(file2.MoveToFile(mockFile))
 }
 
 func (s *osFileTest) TestWrite() {
diff --git a/backend/s3/fileSystem_test.go b/backend/s3/fileSystem_test.go
index fa81d0b..0a4b4ba 100644
--- a/backend/s3/fileSystem_test.go
+++ b/backend/s3/fileSystem_test.go
@@ -6,6 +6,8 @@ import (
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/s3"
 	"github.com/stretchr/testify/suite"
+
+	"github.com/c2fo/vfs/v5/utils"
 )
 
 type fileSystemTestSuite struct {
@@ -39,12 +41,44 @@ func (ts *fileSystemTestSuite) TestNewFile() {
 }
 
 func (ts *fileSystemTestSuite) TestNewFile_Error() {
+	// test nil pointer
+	var nils3fs *FileSystem
+	_, err := nils3fs.NewFile("", "/path/to/file.txt")
+	ts.EqualError(err, "non-nil s3.FileSystem pointer is required", "errors returned by NewFile")
+
+	//test validation error
+	file, err := s3fs.NewFile("bucketName", "relative/path/to/file.txt")
+	ts.EqualError(err, utils.ErrBadAbsFilePath, "errors returned by NewFile")
+
 	filePath := ""
-	file, err := s3fs.NewFile("", filePath)
+	file, err = s3fs.NewFile("", filePath)
 	ts.Error(err, "NewFile(%s)", filePath)
 	ts.Nil(file, "NewFile(%s) shouldn't return a file", filePath)
 }
 
+func (ts *fileSystemTestSuite) TestNewLocation() {
+	locPath := "/path/to/"
+	loc, err := s3fs.NewLocation("bucketName", locPath)
+	ts.NoError(err, "No errors returned by NewLocation(%s)", locPath)
+	ts.NotNil(loc, "fs.NewLocation(%s) should assign all but first name component to key", locPath)
+}
+
+func (ts *fileSystemTestSuite) TestNewLocation_Error() {
+	// test nil pointer
+	var nils3fs *FileSystem
+	_, err := nils3fs.NewLocation("", "/path/to/")
+	ts.EqualError(err, "non-nil s3.FileSystem pointer is required", "errors returned by NewLocation")
+
+	//test validation error
+	file, err := s3fs.NewLocation("bucketName", "relative/path/to/")
+	ts.EqualError(err, utils.ErrBadAbsLocationPath, "errors returned by NewLocation")
+
+	locPath := ""
+	file, err = s3fs.NewLocation("", locPath)
+	ts.EqualError(err, "non-empty strings for bucket and key are required", "NewLocation(%s)", locPath)
+	ts.Nil(file, "NewLocation(%s) shouldn't return a file", locPath)
+}
+
 func (ts *fileSystemTestSuite) TestName_Error() {
 	ts.Equal(name, s3fs.Name(), "Name() is s3.name const")
 }
@@ -67,6 +101,14 @@ func (ts *fileSystemTestSuite) TestClient() {
 	ts.NoError(err, "no error")
 	ts.Equal(s3fs.client, client, "client was already set")
 
+	// bad options
+	badOpt := "not an s3.Options"
+	s3fs.client = nil
+	s3fs.options = badOpt
+	client, err = s3fs.Client()
+	ts.Error(err, "error found")
+	ts.Equal("unable to create client, vfs.Options must be an s3.Options", err.Error(), "client was already set")
+
 	s3fs = &FileSystem{}
 	client, err = s3fs.Client()
 	ts.NoError(err, "no error")
diff --git a/backend/s3/file_test.go b/backend/s3/file_test.go
index ee4c4f4..a18cef6 100644
--- a/backend/s3/file_test.go
+++ b/backend/s3/file_test.go
@@ -4,10 +4,13 @@ import (
 	"bytes"
 	"errors"
 	"io"
+	"net/http"
+	"net/url"
 	"testing"
 	"time"
 
 	"github.com/aws/aws-sdk-go/aws/awserr"
+	"github.com/aws/aws-sdk-go/aws/request"
 	"github.com/aws/aws-sdk-go/service/s3"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
@@ -245,47 +248,58 @@ func (ts *fileTestSuite) TestCopyToLocation() {
 
 }
 
-func (ts *fileTestSuite) TestCopyToLocationWithinS3() {
-	otherFs := new(mocks.FileSystem)
-	otherFs.On("Scheme", mock.Anything).Return(Scheme)
-	otherFile := new(mocks.File)
-	location := new(mocks.Location)
-	location.On("FileSystem", mock.Anything).Return(otherFs)
-	location.On("Path", mock.Anything).Return("new/file/path").Twice()
-	location.On("Volume", mock.Anything).Return("newBucket").Twice()
-
-	s3apiMock.On("CopyObject", mock.AnythingOfType("*s3.CopyObjectInput")).Return(&s3.CopyObjectOutput{}, nil)
-	s3apiMock.On("HeadObject", mock.AnythingOfType("*s3.HeadObjectInput")).Return(&s3.HeadObjectOutput{}, nil)
-
-	file, err := fs.NewFile("bucket", "/hello.txt")
-	if err != nil {
-		ts.Fail("Shouldn't return error creating test s3.File instance.")
+func (ts *fileTestSuite) TestTouch() {
+	// Copy portion tested through CopyToLocation, just need to test whether or not Delete happens
+	// in addition to CopyToLocation
+	s3Mock1 := &mocks.S3API{}
+	s3Mock1.On("HeadObject", mock.AnythingOfType("*s3.HeadObjectInput")).Return(&s3.HeadObjectOutput{}, nil)
+	s3Mock1.On("CopyObject", mock.AnythingOfType("*s3.CopyObjectInput")).Return(nil, nil)
+	s3Mock1.On("DeleteObject", mock.AnythingOfType("*s3.DeleteObjectInput")).Return(&s3.DeleteObjectOutput{}, nil)
+	file := &File{
+		fileSystem: &FileSystem{
+			client: s3Mock1,
+		},
+		bucket: "newBucket",
+		key:    "/new/file/path/hello.txt",
 	}
+	terr := file.Touch()
+	ts.NoError(terr, "Shouldn't return error creating test s3.File instance.")
 
-	otherFs.On("Scheme").Return(Scheme)
-	otherFs.On("NewFile", "newBucket", "new/file/path/hello.txt").Return(otherFile, nil)
+	s3Mock1.AssertExpectations(ts.T())
 
-	_, err = file.CopyToLocation(location)
-	assert.NoError(ts.T(), err, "no error expected")
+	// test non-existent length
+	s3Mock2 := &mocks.S3API{}
+	s3Mock2.On("HeadObject", mock.AnythingOfType("*s3.HeadObjectInput")).Return(&s3.HeadObjectOutput{}, awserr.New(s3.ErrCodeNoSuchKey, "", nil)).Once()
+	s3Mock2.On("PutObjectRequest", mock.AnythingOfType("*s3.PutObjectInput")).Return(&request.Request{HTTPRequest: &http.Request{Header: make(map[string][]string), URL: &url.URL{}}}, &s3.PutObjectOutput{})
+	s3Mock2.On("HeadObject", mock.AnythingOfType("*s3.HeadObjectInput")).Return(&s3.HeadObjectOutput{}, nil)
+	file2 := &File{
+		fileSystem: &FileSystem{
+			client: s3Mock2,
+		},
+		bucket: "newBucket",
+		key:    "/new/file/path/hello.txt",
+	}
+	terr2 := file2.Touch()
+	ts.NoError(terr2, "Shouldn't return error creating test s3.File instance.")
 
-	closeErr := file.Close()
-	assert.NoError(ts.T(), closeErr, "no error expected")
+	s3Mock2.AssertExpectations(ts.T())
 
-	s3apiMock.AssertExpectations(ts.T())
-	otherFs.AssertExpectations(ts.T())
-	location.AssertExpectations(ts.T())
 }
-
 func (ts *fileTestSuite) TestMoveToLocation() {
 	// Copy portion tested through CopyToLocation, just need to test whether or not Delete happens
 	// in addition to CopyToLocation
-	otherFs := new(mocks.FileSystem)
-	otherFs.On("Scheme", mock.Anything).Return(Scheme)
-	otherFile := new(mocks.File)
+	s3Mock1 := &mocks.S3API{}
+	s3Mock1.On("CopyObject", mock.AnythingOfType("*s3.CopyObjectInput")).Return(nil, nil)
+	s3Mock1.On("HeadObject", mock.AnythingOfType("*s3.HeadObjectInput")).Return(&s3.HeadObjectOutput{}, nil)
+	f := &File{
+		fileSystem: &FileSystem{
+			client: s3Mock1,
+		},
+		bucket: "newBucket",
+		key:    "/new/file/path/hello.txt",
+	}
 	location := new(mocks.Location)
-	location.On("FileSystem", mock.Anything).Return(otherFs)
-	location.On("Path", mock.Anything).Return("new/file/path").Twice()
-	location.On("Volume", mock.Anything).Return("newBucket").Twice()
+	location.On("NewFile", mock.Anything).Return(f, nil)
 
 	s3apiMock.On("CopyObject", mock.AnythingOfType("*s3.CopyObjectInput")).Return(&s3.CopyObjectOutput{}, nil)
 	s3apiMock.On("DeleteObject", mock.AnythingOfType("*s3.DeleteObjectInput")).Return(&s3.DeleteObjectOutput{}, nil)
@@ -298,28 +312,40 @@ func (ts *fileTestSuite) TestMoveToLocation() {
 
 	defer func() {
 		closeErr := file.Close()
-		assert.NoError(ts.T(), closeErr, "no error expected")
+		ts.NoError(closeErr, "no error expected")
 	}()
 
-	otherFs.On("Scheme").Return(Scheme)
-	otherFs.On("NewFile", "newBucket", "new/file/path/hello.txt").Return(otherFile, nil)
-
 	_, err = file.MoveToLocation(location)
-	assert.NoError(ts.T(), err, "no error expected")
+	ts.NoError(err, "no error expected")
+
+	// test non-scheme MoveToLocation
+	mockLocation := new(mocks.Location)
+	mockLocation.On("NewFile", mock.Anything).Return(&File{}, nil)
+
+	s3apiMock2 := &mocks.S3API{}
+	s3apiMock2.On("CopyObject", mock.AnythingOfType("*s3.CopyObjectInput")).Return(&s3.CopyObjectOutput{}, nil)
+	val := int64(0)
+	s3apiMock2.On("HeadObject", mock.AnythingOfType("*s3.HeadObjectInput")).Return(&s3.HeadObjectOutput{ContentLength: &val}, nil)
+
+	fs = FileSystem{client: s3apiMock2}
+	file2, err := fs.NewFile("bucket", "/hello.txt")
+	if err != nil {
+		ts.Fail("Shouldn't return error creating test s3.File instance.")
+	}
+
+	_, err = file2.CopyToLocation(mockLocation)
+	ts.NoError(err, "MoveToLocation error not expected")
 
 	s3apiMock.AssertExpectations(ts.T())
-	otherFs.AssertExpectations(ts.T())
 	location.AssertExpectations(ts.T())
+	mockLocation.AssertExpectations(ts.T())
 }
 
 func (ts *fileTestSuite) TestMoveToLocationFail() {
 	// If CopyToLocation fails we need to ensure DeleteObject isn't called.
 	otherFs := new(mocks.FileSystem)
-	otherFs.On("Scheme", mock.Anything).Return(Scheme).Once()
 	location := new(mocks.Location)
-	location.On("FileSystem", mock.Anything).Return(otherFs).Once()
-	location.On("Path", mock.Anything).Return("new/file/path").Once()
-	location.On("Volume", mock.Anything).Return("newBucket").Once()
+	location.On("NewFile", mock.Anything).Return(&File{}, nil)
 
 	s3apiMock.On("CopyObject", mock.AnythingOfType("*s3.CopyObjectInput")).Return(nil, errors.New("didn't copy, oh noes"))
 	s3apiMock.On("HeadObject", mock.AnythingOfType("*s3.HeadObjectInput")).Return(&s3.HeadObjectOutput{}, nil)
@@ -330,10 +356,10 @@ func (ts *fileTestSuite) TestMoveToLocationFail() {
 	}
 
 	_, merr := file.MoveToLocation(location)
-	assert.Error(ts.T(), merr, "MoveToLocation error not expected")
+	ts.Error(merr, "MoveToLocation error not expected")
 
 	closeErr := file.Close()
-	assert.NoError(ts.T(), closeErr, "no close error expected")
+	ts.NoError(closeErr, "no close error expected")
 
 	s3apiMock.AssertExpectations(ts.T())
 	s3apiMock.AssertNotCalled(ts.T(), "DeleteObject", mock.AnythingOfType("*s3.DeleteObjectInput"))
diff --git a/backend/s3/location_test.go b/backend/s3/location_test.go
index fa22ff1..56dd67e 100644
--- a/backend/s3/location_test.go
+++ b/backend/s3/location_test.go
@@ -190,6 +190,19 @@ func (lt *locationTestSuite) TestNewFile() {
 
 	newrelfile, _ := loc.NewFile("../../where/file.txt")
 	lt.Equal("/some/where/file.txt", newrelfile.Path(), "Newfile relative dot path works")
+
+	// test nil pointer
+	var nilLoc *Location
+	_, err = nilLoc.NewFile("/path/to/file.txt")
+	lt.EqualError(err, "non-nil s3.Location pointer is required", "errors returned by NewFile")
+
+	//test empty path error
+	_, err = loc.NewFile("")
+	lt.EqualError(err, "non-empty string filePath is required", "errors returned by NewFile")
+
+	//test validation error
+	_, err = loc.NewFile("/absolute/path/to/file.txt")
+	lt.EqualError(err, utils.ErrBadRelFilePath, "errors returned by NewLocation")
 }
 
 func (lt *locationTestSuite) TestExists_true() {
@@ -219,6 +232,11 @@ func (lt *locationTestSuite) TestExists_false() {
 }
 
 func (lt *locationTestSuite) TestChangeDir() {
+	//test nil Location
+	var nilLoc *Location
+	err := nilLoc.ChangeDir("path/to/")
+	lt.EqualErrorf(err, "non-nil s3.Location pointer is required", "error expected for nil location")
+
 	loc := &Location{fileSystem: lt.fs, prefix: "/", bucket: "bucket"}
 
 	err1 := loc.ChangeDir("../")
@@ -253,6 +271,19 @@ func (lt *locationTestSuite) TestNewLocation() {
 	newRelLoc, err := newLoc.NewLocation("../../some/path/")
 	lt.NoError(err)
 	lt.Equal("/old/some/path/", newRelLoc.Path(), "NewLocation works with rel dot paths")
+
+	// test nil pointer
+	var nilLoc *Location
+	_, err = nilLoc.NewLocation("/path/to/")
+	lt.EqualError(err, "non-nil s3.Location pointer is required", "errors returned by NewLocation")
+
+	//test empty path error
+	_, err = loc.NewLocation("")
+	lt.EqualError(err, "non-empty string relativePath is required", "errors returned by NewLocation")
+
+	//test validation error
+	_, err = loc.NewLocation("/absolute/path/to/")
+	lt.EqualError(err, utils.ErrBadRelLocationPath, "errors returned by NewLocation")
 }
 
 func (lt *locationTestSuite) TestStringURI() {
diff --git a/backend/testsuite/backend_integration_test.go b/backend/testsuite/backend_integration_test.go
index 6e3f87a..3ec5921 100644
--- a/backend/testsuite/backend_integration_test.go
+++ b/backend/testsuite/backend_integration_test.go
@@ -1,4 +1,4 @@
-// +build vfsintegration
+//+build vfsintegration
 
 package testsuite
 
@@ -694,46 +694,37 @@ func (s *vfsTestSuite) File(baseLoc vfs.Location) {
 		s.NoError(err)
 	}
 
-	// Test empty reader used to io.Copy
+	// Touch creates a zero-length file on the vfs.File if no File exists.  Update File's last modified timestamp.
+	// Returns error if unable to touch File.
 
-	// TODO: ***************************************************************************************************
-	//  it has yet to be determined what behavior should occur
-	//  likely we will not expect io.Copy to produce a file since that seems to be the general expected behavior
-	//  however, does that mean then that we enforce that it should NOT create a file, like it does in OS.
-	//  Also, Should we ensure that an empty write() doesn't create a file on close()?
-	//  ********************************************************************************************************
-
-	/*
-		setup reader and vfs.File target
-		emptyIOReader, err := srcLoc.NewFile("srcEmptyfile.txt")
-		s.NoError(err)
-		_, err = emptyIOReader.Write([]byte(""))
-		s.NoError(err)
-		s.NoError(emptyIOReader.Close())
-
-		emptyTargetFile, err := srcLoc.NewFile("emptyfile.txt")
-		s.NoError(err)
+	touchedFile, err := srcLoc.NewFile("touch.txt")
+	s.NoError(err)
+	defer func() { _ = touchedFile.Delete() }()
+	exists, err = touchedFile.Exists()
+	s.NoError(err)
+	s.False(exists, "%s shouldn't yet exist", touchedFile)
 
-		fmt.Println(emptyTargetFile)
-		// use io.Copy
-		byteCount, err := io.Copy(emptyTargetFile, emptyIOReader)
-		s.NoError(err, "should have no error copying emtpy file")
-		s.Equal(int64(0), byteCount, "should copy zero bytes")
+	err = touchedFile.Touch()
+	s.NoError(err)
+	exists, err = touchedFile.Exists()
+	s.NoError(err)
+	s.True(exists, "%s now exists", touchedFile)
 
-		//does target exist?
-		exists, err = emptyTargetFile.Exists()
-		s.NoError(err)
-		s.True(exists, "new empty file should exist")
+	size, err := touchedFile.Size()
+	s.NoError(err)
+	s.Equal(uint64(0), size, "%s should be empty", touchedFile)
 
-		//does size match?
-		size, err := emptyTargetFile.Size()
-		s.NoError(err)
-		s.Equal(uint64(0), size, "new empty file should have size of 0")
+	//capture last modified
+	modified, err := touchedFile.LastModified()
+	s.NoError(err)
 
-		//clean up
-		err = emptyTargetFile.Delete()
-		s.NoError(err)
-	*/
+	//wait for eventual constency
+	time.Sleep(1 * time.Second)
+	err = touchedFile.Touch()
+	s.NoError(err)
+	newModified, err := touchedFile.LastModified()
+	s.NoError(err)
+	s.True(newModified.UnixNano() > modified.UnixNano(), "touch updated modified date for %s", touchedFile)
 
 	/*
 		Delete unlinks the File on the file system.