Repository: C2FO/vfs
Pull request #203 (2024-09-13 20:55:46)

Add ability to set file permissions after writing an SFTP file. Resol…
…ves #202.

codecov-commenter: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/C2FO/vfs/pull/203?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) Report
Attention: Patch coverage is `28.75000%` with `342 lines` in your changes missing coverage. Please review.
> Project coverage is 21.03%. Comparing base [(`8b7451b`)](https://app.codecov.io/gh/C2FO/vfs/commit/8b7451bf3eed473aec89ece29e6b3769c7e60680?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) to head [(`089b625`)](https://app.codecov.io/gh/C2FO/vfs/commit/089b62560176c7b11785a8a159a24787190fa730?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/C2FO/vfs/pull/203?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) | Patch % | Lines |
|---|---|---|
| [backend/sftp/mocks/Client.go](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&filepath=backend%2Fsftp%2Fmocks%2FClient.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO#diff-YmFja2VuZC9zZnRwL21vY2tzL0NsaWVudC5nbw==) | 25.52% | [127 Missing and 16 partials :warning: ](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) |
| [backend/sftp/mocks/FileInfo.go](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&filepath=backend%2Fsftp%2Fmocks%2FFileInfo.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO#diff-YmFja2VuZC9zZnRwL21vY2tzL0ZpbGVJbmZvLmdv) | 5.88% | [92 Missing and 4 partials :warning: ](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) |
| [backend/sftp/mocks/ReadWriteSeekCloser.go](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&filepath=backend%2Fsftp%2Fmocks%2FReadWriteSeekCloser.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO#diff-YmFja2VuZC9zZnRwL21vY2tzL1JlYWRXcml0ZVNlZWtDbG9zZXIuZ28=) | 29.62% | [80 Missing and 15 partials :warning: ](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) |
| [backend/sftp/file.go](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&filepath=backend%2Fsftp%2Ffile.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO#diff-YmFja2VuZC9zZnRwL2ZpbGUuZ28=) | 84.61% | [4 Missing and 2 partials :warning: ](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) |
| [utils/utils.go](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&filepath=utils%2Futils.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO#diff-dXRpbHMvdXRpbHMuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/C2FO/vfs/pull/203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO) |

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #203      +/-   ##
==========================================
- Coverage   21.71%   21.03%   -0.69%     
==========================================
  Files          59       61       +2     
  Lines       19301    20341    +1040     
==========================================
+ Hits         4192     4278      +86     
- Misses      14648    15565     +917     
- Partials      461      498      +37     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/C2FO/vfs/pull/203?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C2FO).


Patch:
diff --git a/.mockery.yaml b/.mockery.yaml
index ee6c276..481e548 100644
--- a/.mockery.yaml
+++ b/.mockery.yaml
@@ -10,3 +10,6 @@ packages:
   github.com/c2fo/vfs/v6/backend/s3:
     config:
       all: true
+  github.com/c2fo/vfs/v6/backend/sftp:
+    config:
+      all: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index edd1e08..74af6d4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 ## [Unreleased]
 
+## [6.19.0] - 2024-09-13
+### Added
+- Add ability to set file permissions after writing an SFTP file. Resolves #202.
+
 ## [6.18.0] - 2024-09-12
 ### Added
 - Updated mocks to use mockery Expecter.  Resolves #200.
diff --git a/backend/sftp/doc.go b/backend/sftp/doc.go
index d179980..2908c34 100644
--- a/backend/sftp/doc.go
+++ b/backend/sftp/doc.go
@@ -138,6 +138,23 @@ Example:
 		},
 	  )
 
+# FilePermissions
+
+The `FilePermissions` option allows you to specify the file permissions for files created or modified using the SFTP backend.
+These permissions will override the sftp server or underlying filesystem's umask (default permissions). Permissions should
+be specified using an octal literal (e.g., `0777` for full read, write, and execute permissions for all users).
+
+Example:
+
+		fs = fs.WithOptions(
+	 		sftp.Options{
+	 			FilePermissions: "0777", // Correctly specify permissions as octal (in string form)
+				// other settings
+			},
+		)
+
+When a file is opened for Write() or Touch()'d, the specified `FilePermissions` will be applied to the file.
+
 # AutoDisconnect
 
 When dialing a TCP connection, Go doesn't disconnect for you.  This is true even when the connection falls out of scope, and even when
diff --git a/backend/sftp/file.go b/backend/sftp/file.go
index 7665f01..cc80032 100644
--- a/backend/sftp/file.go
+++ b/backend/sftp/file.go
@@ -2,6 +2,7 @@ package sftp
 
 import (
 	"errors"
+	"fmt"
 	"io"
 	"os"
 	"path"
@@ -78,15 +79,16 @@ func (f *File) Exists() (bool, 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.
 func (f *File) Touch() error {
+	// restart timer once action is completed
+	f.fileSystem.connTimerStop()
+	defer f.fileSystem.connTimerStart()
+
 	exists, err := f.Exists()
 	if err != nil {
 		return err
 	}
 
 	if !exists {
-		// restart timer once action is completed
-		f.fileSystem.connTimerStop()
-		defer f.fileSystem.connTimerStart()
 		file, err := f.openFile(os.O_WRONLY | os.O_CREATE)
 		if err != nil {
 			return err
@@ -99,10 +101,15 @@ func (f *File) Touch() error {
 	if err != nil {
 		return err
 	}
-	// start timer once action is completed
-	defer f.fileSystem.connTimerStart()
-	now := time.Now()
 
+	// set permissions if default permissions are set
+	err = f.setPermissions(client, f.fileSystem.options)
+	if err != nil {
+		return err
+	}
+
+	// update last accessed and last modified times
+	now := time.Now()
 	return client.Chtimes(f.Path(), now, now)
 }
 
@@ -140,11 +147,6 @@ func (f *File) Location() vfs.Location {
 // If the given location is also sftp AND for the same user and host, the sftp Rename method is used, otherwise
 // we'll do a an io.Copy to the destination file then delete source file.
 func (f *File) MoveToFile(t vfs.File) error {
-	// validate seek is at 0,0 before doing copy
-	// TODO: Fix this later
-	// if err := backend.ValidateCopySeekPosition(f); err != nil {
-	//	  return err
-	// }
 	// sftp rename if vfs is sftp and for the same user/host
 	if f.fileSystem.Scheme() == t.Location().FileSystem().Scheme() &&
 		f.Authority.UserInfo().Username() == t.(*File).Authority.UserInfo().Username() &&
@@ -204,11 +206,6 @@ func (f *File) MoveToLocation(location vfs.Location) (vfs.File, error) {
 
 // CopyToFile puts the contents of File into the targetFile passed.
 func (f *File) CopyToFile(file vfs.File) (err error) {
-	// validate seek is at 0,0 before doing copy
-	// TODO: Fix this later
-	// if err := backend.ValidateCopySeekPosition(f); err != nil {
-	//  	return err
-	// }
 
 	// Close file (f) reader regardless of an error
 	defer func() {
@@ -286,6 +283,7 @@ func (f *File) Close() error {
 		}
 		f.sftpfile = nil
 	}
+
 	// no op for unopened file
 	return nil
 }
@@ -490,8 +488,51 @@ func (f *File) _open(flags int) (ReadWriteSeekCloser, error) {
 		opener = defaultOpenFile
 	}
 
-	return opener(client, f.Path(), flags)
+	rwsc, err := opener(client, f.Path(), flags)
+	if err != nil {
+		return nil, err
+	}
 
+	// chmod file if default permissions are set and opening for write
+	if flags&os.O_WRONLY != 0 {
+		err = f.setPermissions(client, f.fileSystem.options)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return rwsc, nil
+}
+
+// setPermissions sets the file permissions if they are set in the options
+func (f *File) setPermissions(client Client, opts vfs.Options) error {
+	if opts == nil {
+		return nil
+	}
+
+	// ensure we're dealing with pointer to Options
+	ptrOpts, ok := opts.(*Options)
+	if !ok {
+		p := opts.(Options)
+		ptrOpts = &p
+	}
+
+	// if file permissions are not set, return early
+	if ptrOpts.FilePermissions == nil {
+		return nil
+	}
+
+	// get file mode
+	perms, err := ptrOpts.GetFileMode()
+	if err != nil {
+		return fmt.Errorf("get file mode err: %w", err)
+	}
+
+	if err := client.Chmod(f.Path(), *perms); err != nil {
+		return fmt.Errorf("chmod err: %w", err)
+	}
+
+	return nil
 }
 
 // defaultOpenFile uses sftp.Client to open a file and returns an sftp.File
diff --git a/backend/sftp/fileSystem.go b/backend/sftp/fileSystem.go
index aa6c696..3c2fdbe 100644
--- a/backend/sftp/fileSystem.go
+++ b/backend/sftp/fileSystem.go
@@ -187,6 +187,7 @@ func init() {
 
 // Client is an interface to make it easier to test
 type Client interface {
+	Chmod(path string, mode os.FileMode) error
 	Chtimes(path string, atime, mtime time.Time) error
 	Create(path string) (*_sftp.File, error)
 	MkdirAll(path string) error
diff --git a/backend/sftp/mocks/Client.go b/backend/sftp/mocks/Client.go
index 013916f..ddaee97 100644
--- a/backend/sftp/mocks/Client.go
+++ b/backend/sftp/mocks/Client.go
@@ -1,13 +1,12 @@
-// Code generated by mockery v0.0.0-dev. DO NOT EDIT.
+// Code generated by mockery v2.43.2. DO NOT EDIT.
 
 package mocks
 
 import (
-	os "os"
-
-	mock "github.com/stretchr/testify/mock"
+	fs "io/fs"
 
 	pkgsftp "github.com/pkg/sftp"
+	mock "github.com/stretchr/testify/mock"
 
 	time "time"
 )
@@ -17,10 +16,69 @@ type Client struct {
 	mock.Mock
 }
 
+type Client_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *Client) EXPECT() *Client_Expecter {
+	return &Client_Expecter{mock: &_m.Mock}
+}
+
+// Chmod provides a mock function with given fields: path, mode
+func (_m *Client) Chmod(path string, mode fs.FileMode) error {
+	ret := _m.Called(path, mode)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Chmod")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(string, fs.FileMode) error); ok {
+		r0 = rf(path, mode)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// Client_Chmod_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Chmod'
+type Client_Chmod_Call struct {
+	*mock.Call
+}
+
+// Chmod is a helper method to define mock.On call
+//   - path string
+//   - mode fs.FileMode
+func (_e *Client_Expecter) Chmod(path interface{}, mode interface{}) *Client_Chmod_Call {
+	return &Client_Chmod_Call{Call: _e.mock.On("Chmod", path, mode)}
+}
+
+func (_c *Client_Chmod_Call) Run(run func(path string, mode fs.FileMode)) *Client_Chmod_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].(fs.FileMode))
+	})
+	return _c
+}
+
+func (_c *Client_Chmod_Call) Return(_a0 error) *Client_Chmod_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *Client_Chmod_Call) RunAndReturn(run func(string, fs.FileMode) error) *Client_Chmod_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Chtimes provides a mock function with given fields: path, atime, mtime
 func (_m *Client) Chtimes(path string, atime time.Time, mtime time.Time) error {
 	ret := _m.Called(path, atime, mtime)
 
+	if len(ret) == 0 {
+		panic("no return value specified for Chtimes")
+	}
+
 	var r0 error
 	if rf, ok := ret.Get(0).(func(string, time.Time, time.Time) error); ok {
 		r0 = rf(path, atime, mtime)
@@ -31,10 +89,44 @@ func (_m *Client) Chtimes(path string, atime time.Time, mtime time.Time) error {
 	return r0
 }
 
+// Client_Chtimes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Chtimes'
+type Client_Chtimes_Call struct {
+	*mock.Call
+}
+
+// Chtimes is a helper method to define mock.On call
+//   - path string
+//   - atime time.Time
+//   - mtime time.Time
+func (_e *Client_Expecter) Chtimes(path interface{}, atime interface{}, mtime interface{}) *Client_Chtimes_Call {
+	return &Client_Chtimes_Call{Call: _e.mock.On("Chtimes", path, atime, mtime)}
+}
+
+func (_c *Client_Chtimes_Call) Run(run func(path string, atime time.Time, mtime time.Time)) *Client_Chtimes_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].(time.Time), args[2].(time.Time))
+	})
+	return _c
+}
+
+func (_c *Client_Chtimes_Call) Return(_a0 error) *Client_Chtimes_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *Client_Chtimes_Call) RunAndReturn(run func(string, time.Time, time.Time) error) *Client_Chtimes_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Close provides a mock function with given fields:
 func (_m *Client) Close() error {
 	ret := _m.Called()
 
+	if len(ret) == 0 {
+		panic("no return value specified for Close")
+	}
+
 	var r0 error
 	if rf, ok := ret.Get(0).(func() error); ok {
 		r0 = rf()
@@ -45,11 +137,46 @@ func (_m *Client) Close() error {
 	return r0
 }
 
+// Client_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
+type Client_Close_Call struct {
+	*mock.Call
+}
+
+// Close is a helper method to define mock.On call
+func (_e *Client_Expecter) Close() *Client_Close_Call {
+	return &Client_Close_Call{Call: _e.mock.On("Close")}
+}
+
+func (_c *Client_Close_Call) Run(run func()) *Client_Close_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *Client_Close_Call) Return(_a0 error) *Client_Close_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *Client_Close_Call) RunAndReturn(run func() error) *Client_Close_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Create provides a mock function with given fields: path
 func (_m *Client) Create(path string) (*pkgsftp.File, error) {
 	ret := _m.Called(path)
 
+	if len(ret) == 0 {
+		panic("no return value specified for Create")
+	}
+
 	var r0 *pkgsftp.File
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) (*pkgsftp.File, error)); ok {
+		return rf(path)
+	}
 	if rf, ok := ret.Get(0).(func(string) *pkgsftp.File); ok {
 		r0 = rf(path)
 	} else {
@@ -58,7 +185,6 @@ func (_m *Client) Create(path string) (*pkgsftp.File, error) {
 		}
 	}
 
-	var r1 error
 	if rf, ok := ret.Get(1).(func(string) error); ok {
 		r1 = rf(path)
 	} else {
@@ -68,10 +194,42 @@ func (_m *Client) Create(path string) (*pkgsftp.File, error) {
 	return r0, r1
 }
 
+// Client_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
+type Client_Create_Call struct {
+	*mock.Call
+}
+
+// Create is a helper method to define mock.On call
+//   - path string
+func (_e *Client_Expecter) Create(path interface{}) *Client_Create_Call {
+	return &Client_Create_Call{Call: _e.mock.On("Create", path)}
+}
+
+func (_c *Client_Create_Call) Run(run func(path string)) *Client_Create_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *Client_Create_Call) Return(_a0 *pkgsftp.File, _a1 error) *Client_Create_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *Client_Create_Call) RunAndReturn(run func(string) (*pkgsftp.File, error)) *Client_Create_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // MkdirAll provides a mock function with given fields: path
 func (_m *Client) MkdirAll(path string) error {
 	ret := _m.Called(path)
 
+	if len(ret) == 0 {
+		panic("no return value specified for MkdirAll")
+	}
+
 	var r0 error
 	if rf, ok := ret.Get(0).(func(string) error); ok {
 		r0 = rf(path)
@@ -82,11 +240,47 @@ func (_m *Client) MkdirAll(path string) error {
 	return r0
 }
 
+// Client_MkdirAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MkdirAll'
+type Client_MkdirAll_Call struct {
+	*mock.Call
+}
+
+// MkdirAll is a helper method to define mock.On call
+//   - path string
+func (_e *Client_Expecter) MkdirAll(path interface{}) *Client_MkdirAll_Call {
+	return &Client_MkdirAll_Call{Call: _e.mock.On("MkdirAll", path)}
+}
+
+func (_c *Client_MkdirAll_Call) Run(run func(path string)) *Client_MkdirAll_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *Client_MkdirAll_Call) Return(_a0 error) *Client_MkdirAll_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *Client_MkdirAll_Call) RunAndReturn(run func(string) error) *Client_MkdirAll_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // OpenFile provides a mock function with given fields: path, f
 func (_m *Client) OpenFile(path string, f int) (*pkgsftp.File, error) {
 	ret := _m.Called(path, f)
 
+	if len(ret) == 0 {
+		panic("no return value specified for OpenFile")
+	}
+
 	var r0 *pkgsftp.File
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string, int) (*pkgsftp.File, error)); ok {
+		return rf(path, f)
+	}
 	if rf, ok := ret.Get(0).(func(string, int) *pkgsftp.File); ok {
 		r0 = rf(path, f)
 	} else {
@@ -95,7 +289,6 @@ func (_m *Client) OpenFile(path string, f int) (*pkgsftp.File, error) {
 		}
 	}
 
-	var r1 error
 	if rf, ok := ret.Get(1).(func(string, int) error); ok {
 		r1 = rf(path, f)
 	} else {
@@ -105,20 +298,56 @@ func (_m *Client) OpenFile(path string, f int) (*pkgsftp.File, error) {
 	return r0, r1
 }
 
+// Client_OpenFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenFile'
+type Client_OpenFile_Call struct {
+	*mock.Call
+}
+
+// OpenFile is a helper method to define mock.On call
+//   - path string
+//   - f int
+func (_e *Client_Expecter) OpenFile(path interface{}, f interface{}) *Client_OpenFile_Call {
+	return &Client_OpenFile_Call{Call: _e.mock.On("OpenFile", path, f)}
+}
+
+func (_c *Client_OpenFile_Call) Run(run func(path string, f int)) *Client_OpenFile_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].(int))
+	})
+	return _c
+}
+
+func (_c *Client_OpenFile_Call) Return(_a0 *pkgsftp.File, _a1 error) *Client_OpenFile_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *Client_OpenFile_Call) RunAndReturn(run func(string, int) (*pkgsftp.File, error)) *Client_OpenFile_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // ReadDir provides a mock function with given fields: p
-func (_m *Client) ReadDir(p string) ([]os.FileInfo, error) {
+func (_m *Client) ReadDir(p string) ([]fs.FileInfo, error) {
 	ret := _m.Called(p)
 
-	var r0 []os.FileInfo
-	if rf, ok := ret.Get(0).(func(string) []os.FileInfo); ok {
+	if len(ret) == 0 {
+		panic("no return value specified for ReadDir")
+	}
+
+	var r0 []fs.FileInfo
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) ([]fs.FileInfo, error)); ok {
+		return rf(p)
+	}
+	if rf, ok := ret.Get(0).(func(string) []fs.FileInfo); ok {
 		r0 = rf(p)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]os.FileInfo)
+			r0 = ret.Get(0).([]fs.FileInfo)
 		}
 	}
 
-	var r1 error
 	if rf, ok := ret.Get(1).(func(string) error); ok {
 		r1 = rf(p)
 	} else {
@@ -128,10 +357,42 @@ func (_m *Client) ReadDir(p string) ([]os.FileInfo, error) {
 	return r0, r1
 }
 
+// Client_ReadDir_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadDir'
+type Client_ReadDir_Call struct {
+	*mock.Call
+}
+
+// ReadDir is a helper method to define mock.On call
+//   - p string
+func (_e *Client_Expecter) ReadDir(p interface{}) *Client_ReadDir_Call {
+	return &Client_ReadDir_Call{Call: _e.mock.On("ReadDir", p)}
+}
+
+func (_c *Client_ReadDir_Call) Run(run func(p string)) *Client_ReadDir_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *Client_ReadDir_Call) Return(_a0 []fs.FileInfo, _a1 error) *Client_ReadDir_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *Client_ReadDir_Call) RunAndReturn(run func(string) ([]fs.FileInfo, error)) *Client_ReadDir_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Remove provides a mock function with given fields: path
 func (_m *Client) Remove(path string) error {
 	ret := _m.Called(path)
 
+	if len(ret) == 0 {
+		panic("no return value specified for Remove")
+	}
+
 	var r0 error
 	if rf, ok := ret.Get(0).(func(string) error); ok {
 		r0 = rf(path)
@@ -142,10 +403,42 @@ func (_m *Client) Remove(path string) error {
 	return r0
 }
 
+// Client_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
+type Client_Remove_Call struct {
+	*mock.Call
+}
+
+// Remove is a helper method to define mock.On call
+//   - path string
+func (_e *Client_Expecter) Remove(path interface{}) *Client_Remove_Call {
+	return &Client_Remove_Call{Call: _e.mock.On("Remove", path)}
+}
+
+func (_c *Client_Remove_Call) Run(run func(path string)) *Client_Remove_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *Client_Remove_Call) Return(_a0 error) *Client_Remove_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *Client_Remove_Call) RunAndReturn(run func(string) error) *Client_Remove_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Rename provides a mock function with given fields: oldname, newname
 func (_m *Client) Rename(oldname string, newname string) error {
 	ret := _m.Called(oldname, newname)
 
+	if len(ret) == 0 {
+		panic("no return value specified for Rename")
+	}
+
 	var r0 error
 	if rf, ok := ret.Get(0).(func(string, string) error); ok {
 		r0 = rf(oldname, newname)
@@ -156,20 +449,56 @@ func (_m *Client) Rename(oldname string, newname string) error {
 	return r0
 }
 
+// Client_Rename_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Rename'
+type Client_Rename_Call struct {
+	*mock.Call
+}
+
+// Rename is a helper method to define mock.On call
+//   - oldname string
+//   - newname string
+func (_e *Client_Expecter) Rename(oldname interface{}, newname interface{}) *Client_Rename_Call {
+	return &Client_Rename_Call{Call: _e.mock.On("Rename", oldname, newname)}
+}
+
+func (_c *Client_Rename_Call) Run(run func(oldname string, newname string)) *Client_Rename_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].(string))
+	})
+	return _c
+}
+
+func (_c *Client_Rename_Call) Return(_a0 error) *Client_Rename_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *Client_Rename_Call) RunAndReturn(run func(string, string) error) *Client_Rename_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Stat provides a mock function with given fields: p
-func (_m *Client) Stat(p string) (os.FileInfo, error) {
+func (_m *Client) Stat(p string) (fs.FileInfo, error) {
 	ret := _m.Called(p)
 
-	var r0 os.FileInfo
-	if rf, ok := ret.Get(0).(func(string) os.FileInfo); ok {
+	if len(ret) == 0 {
+		panic("no return value specified for Stat")
+	}
+
+	var r0 fs.FileInfo
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) (fs.FileInfo, error)); ok {
+		return rf(p)
+	}
+	if rf, ok := ret.Get(0).(func(string) fs.FileInfo); ok {
 		r0 = rf(p)
 	} else {
 		if ret.Get(0) != nil {
-			r0 = ret.Get(0).(os.FileInfo)
+			r0 = ret.Get(0).(fs.FileInfo)
 		}
 	}
 
-	var r1 error
 	if rf, ok := ret.Get(1).(func(string) error); ok {
 		r1 = rf(p)
 	} else {
@@ -178,3 +507,45 @@ func (_m *Client) Stat(p string) (os.FileInfo, error) {
 
 	return r0, r1
 }
+
+// Client_Stat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Stat'
+type Client_Stat_Call struct {
+	*mock.Call
+}
+
+// Stat is a helper method to define mock.On call
+//   - p string
+func (_e *Client_Expecter) Stat(p interface{}) *Client_Stat_Call {
+	return &Client_Stat_Call{Call: _e.mock.On("Stat", p)}
+}
+
+func (_c *Client_Stat_Call) Run(run func(p string)) *Client_Stat_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *Client_Stat_Call) Return(_a0 fs.FileInfo, _a1 error) *Client_Stat_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *Client_Stat_Call) RunAndReturn(run func(string) (fs.FileInfo, error)) *Client_Stat_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewClient(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *Client {
+	mock := &Client{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/backend/sftp/mocks/FileInfo.go b/backend/sftp/mocks/FileInfo.go
index 7d429aa..1047aae 100644
--- a/backend/sftp/mocks/FileInfo.go
+++ b/backend/sftp/mocks/FileInfo.go
@@ -1,20 +1,36 @@
-// Code generated by mockery v1.0.0. DO NOT EDIT.
+// Code generated by mockery v2.43.2. DO NOT EDIT.
 
 package mocks
 
-import "github.com/stretchr/testify/mock"
-import "os"
-import "time"
+import (
+	fs "io/fs"
+
+	mock "github.com/stretchr/testify/mock"
+
+	time "time"
+)
 
 // FileInfo is an autogenerated mock type for the FileInfo type
 type FileInfo struct {
 	mock.Mock
 }
 
+type FileInfo_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *FileInfo) EXPECT() *FileInfo_Expecter {
+	return &FileInfo_Expecter{mock: &_m.Mock}
+}
+
 // IsDir provides a mock function with given fields:
 func (_m *FileInfo) IsDir() bool {
 	ret := _m.Called()
 
+	if len(ret) == 0 {
+		panic("no return value specified for IsDir")
+	}
+
 	var r0 bool
 	if rf, ok := ret.Get(0).(func() bool); ok {
 		r0 = rf()
@@ -25,10 +41,41 @@ func (_m *FileInfo) IsDir() bool {
 	return r0
 }
 
+// FileInfo_IsDir_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsDir'
+type FileInfo_IsDir_Call struct {
+	*mock.Call
+}
+
+// IsDir is a helper method to define mock.On call
+func (_e *FileInfo_Expecter) IsDir() *FileInfo_IsDir_Call {
+	return &FileInfo_IsDir_Call{Call: _e.mock.On("IsDir")}
+}
+
+func (_c *FileInfo_IsDir_Call) Run(run func()) *FileInfo_IsDir_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *FileInfo_IsDir_Call) Return(_a0 bool) *FileInfo_IsDir_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *FileInfo_IsDir_Call) RunAndReturn(run func() bool) *FileInfo_IsDir_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // ModTime provides a mock function with given fields:
 func (_m *FileInfo) ModTime() time.Time {
 	ret := _m.Called()
 
+	if len(ret) == 0 {
+		panic("no return value specified for ModTime")
+	}
+
 	var r0 time.Time
 	if rf, ok := ret.Get(0).(func() time.Time); ok {
 		r0 = rf()
@@ -39,24 +86,86 @@ func (_m *FileInfo) ModTime() time.Time {
 	return r0
 }
 
+// FileInfo_ModTime_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ModTime'
+type FileInfo_ModTime_Call struct {
+	*mock.Call
+}
+
+// ModTime is a helper method to define mock.On call
+func (_e *FileInfo_Expecter) ModTime() *FileInfo_ModTime_Call {
+	return &FileInfo_ModTime_Call{Call: _e.mock.On("ModTime")}
+}
+
+func (_c *FileInfo_ModTime_Call) Run(run func()) *FileInfo_ModTime_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *FileInfo_ModTime_Call) Return(_a0 time.Time) *FileInfo_ModTime_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *FileInfo_ModTime_Call) RunAndReturn(run func() time.Time) *FileInfo_ModTime_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Mode provides a mock function with given fields:
-func (_m *FileInfo) Mode() os.FileMode {
+func (_m *FileInfo) Mode() fs.FileMode {
 	ret := _m.Called()
 
-	var r0 os.FileMode
-	if rf, ok := ret.Get(0).(func() os.FileMode); ok {
+	if len(ret) == 0 {
+		panic("no return value specified for Mode")
+	}
+
+	var r0 fs.FileMode
+	if rf, ok := ret.Get(0).(func() fs.FileMode); ok {
 		r0 = rf()
 	} else {
-		r0 = ret.Get(0).(os.FileMode)
+		r0 = ret.Get(0).(fs.FileMode)
 	}
 
 	return r0
 }
 
+// FileInfo_Mode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Mode'
+type FileInfo_Mode_Call struct {
+	*mock.Call
+}
+
+// Mode is a helper method to define mock.On call
+func (_e *FileInfo_Expecter) Mode() *FileInfo_Mode_Call {
+	return &FileInfo_Mode_Call{Call: _e.mock.On("Mode")}
+}
+
+func (_c *FileInfo_Mode_Call) Run(run func()) *FileInfo_Mode_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *FileInfo_Mode_Call) Return(_a0 fs.FileMode) *FileInfo_Mode_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *FileInfo_Mode_Call) RunAndReturn(run func() fs.FileMode) *FileInfo_Mode_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Name provides a mock function with given fields:
 func (_m *FileInfo) Name() string {
 	ret := _m.Called()
 
+	if len(ret) == 0 {
+		panic("no return value specified for Name")
+	}
+
 	var r0 string
 	if rf, ok := ret.Get(0).(func() string); ok {
 		r0 = rf()
@@ -67,10 +176,41 @@ func (_m *FileInfo) Name() string {
 	return r0
 }
 
+// FileInfo_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
+type FileInfo_Name_Call struct {
+	*mock.Call
+}
+
+// Name is a helper method to define mock.On call
+func (_e *FileInfo_Expecter) Name() *FileInfo_Name_Call {
+	return &FileInfo_Name_Call{Call: _e.mock.On("Name")}
+}
+
+func (_c *FileInfo_Name_Call) Run(run func()) *FileInfo_Name_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *FileInfo_Name_Call) Return(_a0 string) *FileInfo_Name_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *FileInfo_Name_Call) RunAndReturn(run func() string) *FileInfo_Name_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Size provides a mock function with given fields:
 func (_m *FileInfo) Size() int64 {
 	ret := _m.Called()
 
+	if len(ret) == 0 {
+		panic("no return value specified for Size")
+	}
+
 	var r0 int64
 	if rf, ok := ret.Get(0).(func() int64); ok {
 		r0 = rf()
@@ -81,10 +221,41 @@ func (_m *FileInfo) Size() int64 {
 	return r0
 }
 
+// FileInfo_Size_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Size'
+type FileInfo_Size_Call struct {
+	*mock.Call
+}
+
+// Size is a helper method to define mock.On call
+func (_e *FileInfo_Expecter) Size() *FileInfo_Size_Call {
+	return &FileInfo_Size_Call{Call: _e.mock.On("Size")}
+}
+
+func (_c *FileInfo_Size_Call) Run(run func()) *FileInfo_Size_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *FileInfo_Size_Call) Return(_a0 int64) *FileInfo_Size_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *FileInfo_Size_Call) RunAndReturn(run func() int64) *FileInfo_Size_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Sys provides a mock function with given fields:
 func (_m *FileInfo) Sys() interface{} {
 	ret := _m.Called()
 
+	if len(ret) == 0 {
+		panic("no return value specified for Sys")
+	}
+
 	var r0 interface{}
 	if rf, ok := ret.Get(0).(func() interface{}); ok {
 		r0 = rf()
@@ -96,3 +267,44 @@ func (_m *FileInfo) Sys() interface{} {
 
 	return r0
 }
+
+// FileInfo_Sys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sys'
+type FileInfo_Sys_Call struct {
+	*mock.Call
+}
+
+// Sys is a helper method to define mock.On call
+func (_e *FileInfo_Expecter) Sys() *FileInfo_Sys_Call {
+	return &FileInfo_Sys_Call{Call: _e.mock.On("Sys")}
+}
+
+func (_c *FileInfo_Sys_Call) Run(run func()) *FileInfo_Sys_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *FileInfo_Sys_Call) Return(_a0 interface{}) *FileInfo_Sys_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *FileInfo_Sys_Call) RunAndReturn(run func() interface{}) *FileInfo_Sys_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewFileInfo creates a new instance of FileInfo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewFileInfo(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *FileInfo {
+	mock := &FileInfo{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/backend/sftp/mocks/ReadWriteSeekCloser.go b/backend/sftp/mocks/ReadWriteSeekCloser.go
new file mode 100644
index 0000000..6e96bfd
--- /dev/null
+++ b/backend/sftp/mocks/ReadWriteSeekCloser.go
@@ -0,0 +1,246 @@
+// Code generated by mockery v2.43.2. DO NOT EDIT.
+
+package mocks
+
+import mock "github.com/stretchr/testify/mock"
+
+// ReadWriteSeekCloser is an autogenerated mock type for the ReadWriteSeekCloser type
+type ReadWriteSeekCloser struct {
+	mock.Mock
+}
+
+type ReadWriteSeekCloser_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *ReadWriteSeekCloser) EXPECT() *ReadWriteSeekCloser_Expecter {
+	return &ReadWriteSeekCloser_Expecter{mock: &_m.Mock}
+}
+
+// Close provides a mock function with given fields:
+func (_m *ReadWriteSeekCloser) Close() error {
+	ret := _m.Called()
+
+	if len(ret) == 0 {
+		panic("no return value specified for Close")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func() error); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// ReadWriteSeekCloser_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
+type ReadWriteSeekCloser_Close_Call struct {
+	*mock.Call
+}
+
+// Close is a helper method to define mock.On call
+func (_e *ReadWriteSeekCloser_Expecter) Close() *ReadWriteSeekCloser_Close_Call {
+	return &ReadWriteSeekCloser_Close_Call{Call: _e.mock.On("Close")}
+}
+
+func (_c *ReadWriteSeekCloser_Close_Call) Run(run func()) *ReadWriteSeekCloser_Close_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *ReadWriteSeekCloser_Close_Call) Return(_a0 error) *ReadWriteSeekCloser_Close_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *ReadWriteSeekCloser_Close_Call) RunAndReturn(run func() error) *ReadWriteSeekCloser_Close_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Read provides a mock function with given fields: p
+func (_m *ReadWriteSeekCloser) Read(p []byte) (int, error) {
+	ret := _m.Called(p)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Read")
+	}
+
+	var r0 int
+	var r1 error
+	if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
+		return rf(p)
+	}
+	if rf, ok := ret.Get(0).(func([]byte) int); ok {
+		r0 = rf(p)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	if rf, ok := ret.Get(1).(func([]byte) error); ok {
+		r1 = rf(p)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// ReadWriteSeekCloser_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
+type ReadWriteSeekCloser_Read_Call struct {
+	*mock.Call
+}
+
+// Read is a helper method to define mock.On call
+//   - p []byte
+func (_e *ReadWriteSeekCloser_Expecter) Read(p interface{}) *ReadWriteSeekCloser_Read_Call {
+	return &ReadWriteSeekCloser_Read_Call{Call: _e.mock.On("Read", p)}
+}
+
+func (_c *ReadWriteSeekCloser_Read_Call) Run(run func(p []byte)) *ReadWriteSeekCloser_Read_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].([]byte))
+	})
+	return _c
+}
+
+func (_c *ReadWriteSeekCloser_Read_Call) Return(n int, err error) *ReadWriteSeekCloser_Read_Call {
+	_c.Call.Return(n, err)
+	return _c
+}
+
+func (_c *ReadWriteSeekCloser_Read_Call) RunAndReturn(run func([]byte) (int, error)) *ReadWriteSeekCloser_Read_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Seek provides a mock function with given fields: offset, whence
+func (_m *ReadWriteSeekCloser) Seek(offset int64, whence int) (int64, error) {
+	ret := _m.Called(offset, whence)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Seek")
+	}
+
+	var r0 int64
+	var r1 error
+	if rf, ok := ret.Get(0).(func(int64, int) (int64, error)); ok {
+		return rf(offset, whence)
+	}
+	if rf, ok := ret.Get(0).(func(int64, int) int64); ok {
+		r0 = rf(offset, whence)
+	} else {
+		r0 = ret.Get(0).(int64)
+	}
+
+	if rf, ok := ret.Get(1).(func(int64, int) error); ok {
+		r1 = rf(offset, whence)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// ReadWriteSeekCloser_Seek_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Seek'
+type ReadWriteSeekCloser_Seek_Call struct {
+	*mock.Call
+}
+
+// Seek is a helper method to define mock.On call
+//   - offset int64
+//   - whence int
+func (_e *ReadWriteSeekCloser_Expecter) Seek(offset interface{}, whence interface{}) *ReadWriteSeekCloser_Seek_Call {
+	return &ReadWriteSeekCloser_Seek_Call{Call: _e.mock.On("Seek", offset, whence)}
+}
+
+func (_c *ReadWriteSeekCloser_Seek_Call) Run(run func(offset int64, whence int)) *ReadWriteSeekCloser_Seek_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(int64), args[1].(int))
+	})
+	return _c
+}
+
+func (_c *ReadWriteSeekCloser_Seek_Call) Return(_a0 int64, _a1 error) *ReadWriteSeekCloser_Seek_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *ReadWriteSeekCloser_Seek_Call) RunAndReturn(run func(int64, int) (int64, error)) *ReadWriteSeekCloser_Seek_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Write provides a mock function with given fields: p
+func (_m *ReadWriteSeekCloser) Write(p []byte) (int, error) {
+	ret := _m.Called(p)
+
+	if len(ret) == 0 {
+		panic("no return value specified for Write")
+	}
+
+	var r0 int
+	var r1 error
+	if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok {
+		return rf(p)
+	}
+	if rf, ok := ret.Get(0).(func([]byte) int); ok {
+		r0 = rf(p)
+	} else {
+		r0 = ret.Get(0).(int)
+	}
+
+	if rf, ok := ret.Get(1).(func([]byte) error); ok {
+		r1 = rf(p)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// ReadWriteSeekCloser_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write'
+type ReadWriteSeekCloser_Write_Call struct {
+	*mock.Call
+}
+
+// Write is a helper method to define mock.On call
+//   - p []byte
+func (_e *ReadWriteSeekCloser_Expecter) Write(p interface{}) *ReadWriteSeekCloser_Write_Call {
+	return &ReadWriteSeekCloser_Write_Call{Call: _e.mock.On("Write", p)}
+}
+
+func (_c *ReadWriteSeekCloser_Write_Call) Run(run func(p []byte)) *ReadWriteSeekCloser_Write_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].([]byte))
+	})
+	return _c
+}
+
+func (_c *ReadWriteSeekCloser_Write_Call) Return(n int, err error) *ReadWriteSeekCloser_Write_Call {
+	_c.Call.Return(n, err)
+	return _c
+}
+
+func (_c *ReadWriteSeekCloser_Write_Call) RunAndReturn(run func([]byte) (int, error)) *ReadWriteSeekCloser_Write_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewReadWriteSeekCloser creates a new instance of ReadWriteSeekCloser. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewReadWriteSeekCloser(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *ReadWriteSeekCloser {
+	mock := &ReadWriteSeekCloser{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}
diff --git a/backend/sftp/mocks/SFTPFile.go b/backend/sftp/mocks/SFTPFile.go
deleted file mode 100644
index fb485f9..0000000
--- a/backend/sftp/mocks/SFTPFile.go
+++ /dev/null
@@ -1,87 +0,0 @@
-// Code generated by mockery v1.0.0. DO NOT EDIT.
-
-package mocks
-
-import mock "github.com/stretchr/testify/mock"
-
-// SFTPFile is an autogenerated mock type for the SFTPFile type
-type SFTPFile struct {
-	mock.Mock
-}
-
-// Close provides a mock function with given fields:
-func (_m *SFTPFile) Close() error {
-	ret := _m.Called()
-
-	var r0 error
-	if rf, ok := ret.Get(0).(func() error); ok {
-		r0 = rf()
-	} else {
-		r0 = ret.Error(0)
-	}
-
-	return r0
-}
-
-// Read provides a mock function with given fields: p
-func (_m *SFTPFile) Read(p []byte) (int, error) {
-	ret := _m.Called(p)
-
-	var r0 int
-	if rf, ok := ret.Get(0).(func([]byte) int); ok {
-		r0 = rf(p)
-	} else {
-		r0 = ret.Get(0).(int)
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func([]byte) error); ok {
-		r1 = rf(p)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
-// Seek provides a mock function with given fields: offset, whence
-func (_m *SFTPFile) Seek(offset int64, whence int) (int64, error) {
-	ret := _m.Called(offset, whence)
-
-	var r0 int64
-	if rf, ok := ret.Get(0).(func(int64, int) int64); ok {
-		r0 = rf(offset, whence)
-	} else {
-		r0 = ret.Get(0).(int64)
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func(int64, int) error); ok {
-		r1 = rf(offset, whence)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
-
-// Write provides a mock function with given fields: p
-func (_m *SFTPFile) Write(p []byte) (int, error) {
-	ret := _m.Called(p)
-
-	var r0 int
-	if rf, ok := ret.Get(0).(func([]byte) int); ok {
-		r0 = rf(p)
-	} else {
-		r0 = ret.Get(0).(int)
-	}
-
-	var r1 error
-	if rf, ok := ret.Get(1).(func([]byte) error); ok {
-		r1 = rf(p)
-	} else {
-		r1 = ret.Error(1)
-	}
-
-	return r0, r1
-}
diff --git a/backend/sftp/options.go b/backend/sftp/options.go
index 7d9c975..071495e 100644
--- a/backend/sftp/options.go
+++ b/backend/sftp/options.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"path"
 	"runtime"
+	"strconv"
 
 	"github.com/mitchellh/go-homedir"
 	_sftp "github.com/pkg/sftp"
@@ -31,7 +32,23 @@ type Options struct {
 	HostKeyAlgorithms  []string            `json:"hostKeyAlgorithms,omitempty"`
 	AutoDisconnect     int                 `json:"autoDisconnect,omitempty"` // seconds before disconnecting. default: 10
 	KnownHostsCallback ssh.HostKeyCallback // env var VFS_SFTP_INSECURE_KNOWN_HOSTS
-	FileBufferSize     int                 // Buffer Size In Bytes Used with utils.TouchCopyBuffered
+	FileBufferSize     int                 `json:"fileBufferSize,omitempty"`  // Buffer Size In Bytes Used with utils.TouchCopyBuffered
+	FilePermissions    *string             `json:"filePermissions,omitempty"` // Default File Permissions for new files
+}
+
+// GetFileMode converts the FilePermissions string to os.FileMode.
+func (o *Options) GetFileMode() (*os.FileMode, error) {
+	if o.FilePermissions == nil {
+		return nil, nil
+	}
+
+	// Convert the string to an unsigned integer, interpreting it as an octal value
+	parsed, err := strconv.ParseUint(*o.FilePermissions, 0, 32)
+	if err != nil {
+		return nil, fmt.Errorf("invalid file mode: %v", err)
+	}
+	mode := os.FileMode(parsed)
+	return &mode, nil
 }
 
 var defaultSSHConfig = &ssh.ClientConfig{
diff --git a/utils/utils.go b/utils/utils.go
index 87cef30..a195458 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -257,3 +257,8 @@ func EncodeURI(scheme, username, hostport, path string) string {
 
 	return u.String()
 }
+
+// Ptr returns a pointer to the given value.
+func Ptr[T any](value T) *T {
+	return &value
+}


Test patch:
diff --git a/backend/sftp/file_test.go b/backend/sftp/file_test.go
index 676073a..f071557 100644
--- a/backend/sftp/file_test.go
+++ b/backend/sftp/file_test.go
@@ -122,65 +122,97 @@ func (ts *fileTestSuite) TestSeek() {
 }
 
 func (ts *fileTestSuite) Test_openFile() {
+	type testCase struct {
+		name           string
+		flags          int
+		setupMocks     func(client *mocks.Client)
+		expectedError  bool
+		expectedErrMsg string
+	}
 
-	// set up sftpfile
-	filepath := "/some/path.txt"
-	client := &mocks.Client{}
-
-	file1 := &nopWriteCloser{strings.NewReader("file 1")}
-
-	auth, err := utils.NewAuthority("user@host1.com:22")
-	ts.NoError(err)
-
-	file := &File{
-		fileSystem: &FileSystem{
-			sftpclient: client,
-			options:    Options{},
+	tests := []testCase{
+		{
+			name:  "Open file for read",
+			flags: os.O_RDONLY,
+			setupMocks: func(client *mocks.Client) {
+				client.EXPECT().OpenFile("/some/path.txt", os.O_RDONLY).Return(&sftp.File{}, nil)
+			},
+			expectedError: false,
 		},
-		Authority: auth,
-		path:      filepath,
-		sftpfile:  file1,
-	}
-
-	// file already opened
-	f, err := file.openFile(os.O_RDONLY)
-	ts.NoError(err, "no error expected")
-	b, err := io.ReadAll(f)
-	ts.NoError(err, "no error expected")
-	ts.Equal(string(b), "file 1", "mock returned")
-
-	// file not open, open for read
-	file.sftpfile = nil
-	file.opener = func(c Client, p string, f int) (ReadWriteSeekCloser, error) { return file1, nil }
-	_, err = file1.Seek(0, 0) // reset file
-	ts.NoError(err, "no error expected")
-	f, err = file.openFile(os.O_RDONLY)
-	ts.NoError(err, "no error expected")
-	b, err = io.ReadAll(f)
-	ts.NoError(err, "no error expected")
-	ts.Equal(string(b), "file 1", "mock returned")
-
-	// file not open, user default opener
-	file.sftpfile = nil
-	file.opener = nil
-	client.On("OpenFile", filepath, os.O_RDONLY).Return(&sftp.File{}, nil)
-	f, err = file.openFile(os.O_RDONLY)
-	ts.NoError(err, "no error expected")
-	ts.IsType(&sftp.File{}, f, "type check")
-
-	// file not open, open for create/write
-	file.sftpfile = nil
-	file.opener = func(c Client, p string, f int) (ReadWriteSeekCloser, error) { return file1, nil }
-	_, err = file1.Seek(0, 0) // reset file
-	ts.NoError(err, "no error expected")
-	client.On("MkdirAll", path.Dir(filepath)).Return(nil)
-	f, err = file.openFile(os.O_RDWR | os.O_CREATE)
-	ts.NoError(err, "no error expected")
-	b, err = io.ReadAll(f)
-	ts.NoError(err, "no error expected")
-	ts.Equal(string(b), "file 1", "mock returned")
+		{
+			name:  "Open file for write",
+			flags: os.O_WRONLY | os.O_CREATE,
+			setupMocks: func(client *mocks.Client) {
+				client.EXPECT().MkdirAll("/some").Return(nil)
+				client.EXPECT().Chmod("/some/path.txt", os.FileMode(0644)).Return(nil)
+				client.EXPECT().OpenFile("/some/path.txt", os.O_WRONLY|os.O_CREATE).Return(&sftp.File{}, nil)
+			},
+			expectedError: false,
+		},
+		{
+			name:  "Open file for create",
+			flags: os.O_RDWR | os.O_CREATE,
+			setupMocks: func(client *mocks.Client) {
+				client.EXPECT().MkdirAll(path.Dir("/some/path.txt")).Return(nil)
+				client.EXPECT().OpenFile("/some/path.txt", os.O_RDWR|os.O_CREATE).Return(&sftp.File{}, nil)
+			},
+			expectedError: false,
+		},
+		{
+			name:  "Open file for create with error",
+			flags: os.O_RDWR | os.O_CREATE,
+			setupMocks: func(client *mocks.Client) {
+				client.EXPECT().MkdirAll(path.Dir("/some/path.txt")).Return(errors.New("mkdir error"))
+			},
+			expectedError:  true,
+			expectedErrMsg: "mkdir error",
+		},
+		{
+			name:  "Open file with default permissions",
+			flags: os.O_WRONLY,
+			setupMocks: func(client *mocks.Client) {
+				client.EXPECT().OpenFile("/some/path.txt", os.O_WRONLY).Return(&sftp.File{}, nil)
+				client.EXPECT().Chmod("/some/path.txt", os.FileMode(0644)).Return(nil)
+			},
+			expectedError: false,
+		},
+		{
+			name:  "Open file with default permissions error",
+			flags: os.O_WRONLY,
+			setupMocks: func(client *mocks.Client) {
+				client.EXPECT().OpenFile("/some/path.txt", os.O_WRONLY).Return(&sftp.File{}, nil)
+				client.EXPECT().Chmod("/some/path.txt", os.FileMode(0644)).Return(errors.New("chmod error"))
+			},
+			expectedError:  true,
+			expectedErrMsg: "chmod error",
+		},
+	}
 
-	client.AssertExpectations(ts.T())
+	for _, tt := range tests {
+		ts.Run(tt.name, func() {
+			client := mocks.NewClient(ts.T())
+			tt.setupMocks(client)
+
+			authority, err := utils.NewAuthority("sftp://user@host:22")
+			ts.NoError(err)
+			file := &File{
+				path:      "/some/path.txt",
+				Authority: authority,
+				fileSystem: &FileSystem{
+					sftpclient: client,
+					options:    Options{FilePermissions: utils.Ptr("0644")},
+				},
+			}
+
+			_, err = file._open(tt.flags)
+			if tt.expectedError {
+				ts.Error(err)
+				ts.Contains(err.Error(), tt.expectedErrMsg)
+			} else {
+				ts.NoError(err)
+			}
+		})
+	}
 }
 
 func (ts *fileTestSuite) TestExists() {
@@ -216,7 +248,7 @@ func (ts *fileTestSuite) TestCopyToFile() {
 	// set up source
 	sourceClient := &mocks.Client{}
 
-	sourceSftpFile := &mocks.SFTPFile{}
+	sourceSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 
 	sourceSftpFile.On("Read", mock.Anything).Return(len(content), nil).Once()
 	sourceSftpFile.On("Read", mock.Anything).Return(0, io.EOF).Once()
@@ -238,7 +270,7 @@ func (ts *fileTestSuite) TestCopyToFile() {
 	// set up target
 	targetClient := &mocks.Client{}
 
-	targetSftpFile := &mocks.SFTPFile{}
+	targetSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	targetSftpFile.On("Write", mock.Anything).Return(len(content), nil).Once()
 	targetSftpFile.On("Close").Return(nil).Once()
 
@@ -272,7 +304,7 @@ func (ts *fileTestSuite) TestCopyToFileBuffered() {
 	// set up source
 	sourceClient := &mocks.Client{}
 
-	sourceSftpFile := &mocks.SFTPFile{}
+	sourceSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 
 	sourceSftpFile.On("Read", mock.Anything).Return(len(content), nil).Once()
 	sourceSftpFile.On("Read", mock.Anything).Return(0, io.EOF).Once()
@@ -294,7 +326,7 @@ func (ts *fileTestSuite) TestCopyToFileBuffered() {
 	// set up target
 	targetClient := &mocks.Client{}
 
-	targetSftpFile := &mocks.SFTPFile{}
+	targetSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	targetSftpFile.On("Write", mock.Anything).Return(len(content), nil).Once()
 	targetSftpFile.On("Close").Return(nil).Once()
 
@@ -331,7 +363,7 @@ func (ts *fileTestSuite) TestCopyToFileEmpty() {
 	// set up source
 	sourceClient := &mocks.Client{}
 
-	sourceSftpFile := &mocks.SFTPFile{}
+	sourceSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	sourceSftpFile.On("Read", mock.Anything).Return(0, io.EOF).Once()
 	sourceSftpFile.On("Close").Return(nil).Once()
 
@@ -351,7 +383,7 @@ func (ts *fileTestSuite) TestCopyToFileEmpty() {
 	// set up target
 	targetClient := &mocks.Client{}
 
-	targetSftpFile := &mocks.SFTPFile{}
+	targetSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	targetSftpFile.On("Write", mock.Anything).Return(len(content), nil).Once()
 	targetSftpFile.On("Close").Return(nil).Once()
 
@@ -388,7 +420,7 @@ func (ts *fileTestSuite) TestCopyToFileEmptyBuffered() {
 	// set up source
 	sourceClient := &mocks.Client{}
 
-	sourceSftpFile := &mocks.SFTPFile{}
+	sourceSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	sourceSftpFile.On("Read", mock.Anything).Return(0, io.EOF).Once()
 	sourceSftpFile.On("Close").Return(nil).Once()
 
@@ -408,7 +440,7 @@ func (ts *fileTestSuite) TestCopyToFileEmptyBuffered() {
 	// set up target
 	targetClient := &mocks.Client{}
 
-	targetSftpFile := &mocks.SFTPFile{}
+	targetSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	targetSftpFile.On("Write", mock.Anything).Return(len(content), nil).Once()
 	targetSftpFile.On("Close").Return(nil).Once()
 
@@ -445,7 +477,7 @@ func (ts *fileTestSuite) TestCopyToLocation() {
 	// set up source
 	sourceClient := &mocks.Client{}
 
-	sourceSftpFile := &mocks.SFTPFile{}
+	sourceSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	sourceSftpFile.On("Read", mock.Anything).Return(len(content), nil).Once()
 	sourceSftpFile.On("Read", mock.Anything).Return(0, io.EOF).Once()
 	sourceSftpFile.On("Close").Return(nil).Once()
@@ -466,7 +498,7 @@ func (ts *fileTestSuite) TestCopyToLocation() {
 	// set up target
 	targetClient := &mocks.Client{}
 
-	targetSftpFile := &mocks.SFTPFile{}
+	targetSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	targetSftpFile.On("Write", mock.Anything).Return(len(content), nil).Once()
 	targetSftpFile.On("Close").Return(nil).Once()
 
@@ -506,7 +538,7 @@ func (ts *fileTestSuite) TestMoveToFile_differentAuthority() {
 	sourceClient := &mocks.Client{}
 	sourceClient.On("Remove", mock.Anything).Return(nil).Once()
 
-	sourceSftpFile := &mocks.SFTPFile{}
+	sourceSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	sourceSftpFile.On("Read", mock.Anything).Return(len(content), nil).Once()
 	sourceSftpFile.On("Read", mock.Anything).Return(0, io.EOF).Once()
 	sourceSftpFile.On("Close").Return(nil).Once()
@@ -527,7 +559,7 @@ func (ts *fileTestSuite) TestMoveToFile_differentAuthority() {
 	// set up target
 	targetClient := &mocks.Client{}
 
-	targetSftpFile := &mocks.SFTPFile{}
+	targetSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	targetSftpFile.On("Write", mock.Anything).Return(len(content), nil).Once()
 	targetSftpFile.On("Close").Return(nil).Once()
 
@@ -573,8 +605,7 @@ func (ts *fileTestSuite) TestMoveToFile_sameAuthority() {
 		path:      "/some/path.txt",
 	}
 
-	rws := &mocks.SFTPFile{}
-	rws.On("Seek", int64(0), 1).Return(int64(0), nil)
+	rws := mocks.NewReadWriteSeekCloser(ts.T())
 	sourceFile.opener = func(c Client, p string, f int) (ReadWriteSeekCloser, error) { return rws, nil }
 
 	// set up target
@@ -623,8 +654,7 @@ func (ts *fileTestSuite) TestMoveToFile_fileExists() {
 		path:      "/some/path.txt",
 	}
 
-	rws := &mocks.SFTPFile{}
-	rws.On("Seek", int64(0), 1).Return(int64(0), nil)
+	rws := mocks.NewReadWriteSeekCloser(ts.T())
 	sourceFile.opener = func(c Client, p string, f int) (ReadWriteSeekCloser, error) { return rws, nil }
 
 	// set up target
@@ -663,7 +693,7 @@ func (ts *fileTestSuite) TestMoveToLocation() {
 	sourceClient := &mocks.Client{}
 	sourceClient.On("Remove", mock.Anything).Return(nil).Once()
 
-	sourceSftpFile := &mocks.SFTPFile{}
+	sourceSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	sourceSftpFile.On("Read", mock.Anything).Return(len(content), nil).Once()
 	sourceSftpFile.On("Read", mock.Anything).Return(0, io.EOF).Once()
 	sourceSftpFile.On("Close").Return(nil).Once()
@@ -684,7 +714,7 @@ func (ts *fileTestSuite) TestMoveToLocation() {
 	// set up target
 	targetClient := &mocks.Client{}
 
-	targetSftpFile := &mocks.SFTPFile{}
+	targetSftpFile := mocks.NewReadWriteSeekCloser(ts.T())
 	targetSftpFile.On("Write", mock.Anything).Return(len(content), nil).Once()
 	targetSftpFile.On("Close").Return(nil).Once()
 
@@ -718,66 +748,112 @@ func (ts *fileTestSuite) TestMoveToLocation() {
 	targetMockLocation.AssertExpectations(ts.T())
 }
 
-func (ts *fileTestSuite) TestTouch_exists() {
-	filepath := "/some/path.txt"
-	// set up source
-	sourceFileInfo := &mocks.FileInfo{}
-
-	client := &mocks.Client{}
-	client.On("Stat", filepath).Return(sourceFileInfo, nil).Once()
-	client.On("Chtimes", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
-
-	sftpFile := &mocks.SFTPFile{}
-
-	auth, err := utils.NewAuthority("user@host1.com:22")
-	ts.NoError(err)
-
-	file := &File{
-		fileSystem: &FileSystem{
-			sftpclient: client,
-			options:    Options{},
-		},
-		Authority: auth,
-		path:      filepath,
-		sftpfile:  sftpFile,
+func (ts *fileTestSuite) TestTouch() {
+	type testCase struct {
+		name           string
+		filePath       string
+		fileExists     bool
+		setPermissions bool
+		expectedError  error
+		setupMocks     func(client *mocks.Client, sftpFile *mocks.ReadWriteSeekCloser, fileInfo *mocks.FileInfo)
 	}
-
-	ts.NoError(file.Touch())
-
-	client.AssertExpectations(ts.T())
-	sftpFile.AssertExpectations(ts.T())
-	sourceFileInfo.AssertExpectations(ts.T())
-}
-
-func (ts *fileTestSuite) TestTouch_notExists() {
-	filepath := "/some/path.txt"
-	// set up source
-	sourceFileInfo := &mocks.FileInfo{}
-
-	client := &mocks.Client{}
-	client.On("Stat", filepath).Return(sourceFileInfo, os.ErrNotExist).Once()
-
-	sftpFile := &mocks.SFTPFile{}
-	sftpFile.On("Close").Return(nil).Once()
-
-	auth, err := utils.NewAuthority("user@host1.com:22")
-	ts.NoError(err)
-
-	file := &File{
-		fileSystem: &FileSystem{
-			sftpclient: client,
-			options:    Options{},
+	err := errors.New("some error")
+	testCases := []testCase{
+		{
+			name:       "file exists",
+			filePath:   "/some/path.txt",
+			fileExists: true,
+			setupMocks: func(client *mocks.Client, sftpFile *mocks.ReadWriteSeekCloser, fileInfo *mocks.FileInfo) {
+				client.EXPECT().Stat("/some/path.txt").Return(fileInfo, nil).Once()
+				client.EXPECT().Chtimes("/some/path.txt", mock.Anything, mock.Anything).Return(nil).Once()
+			},
+		},
+		{
+			name:       "file does not exist",
+			filePath:   "/some/path.txt",
+			fileExists: false,
+			setupMocks: func(client *mocks.Client, sftpFile *mocks.ReadWriteSeekCloser, fileInfo *mocks.FileInfo) {
+				client.EXPECT().Stat("/some/path.txt").Return(nil, os.ErrNotExist).Once()
+				sftpFile.EXPECT().Close().Return(nil).Once()
+			},
+		},
+		{
+			name:           "set default permissions",
+			filePath:       "/some/path.txt",
+			fileExists:     true,
+			setPermissions: true,
+			setupMocks: func(client *mocks.Client, sftpFile *mocks.ReadWriteSeekCloser, fileInfo *mocks.FileInfo) {
+				client.EXPECT().Stat("/some/path.txt").Return(fileInfo, nil).Once()
+				client.EXPECT().Chmod("/some/path.txt", os.FileMode(0666)).Return(nil).Once()
+				client.EXPECT().Chtimes("/some/path.txt", mock.Anything, mock.Anything).Return(nil).Once()
+			},
+		},
+		{
+			name:          "error on stat",
+			filePath:      "/some/path.txt",
+			expectedError: err,
+			setupMocks: func(client *mocks.Client, sftpFile *mocks.ReadWriteSeekCloser, fileInfo *mocks.FileInfo) {
+				client.EXPECT().Stat("/some/path.txt").Return(nil, err).Once()
+			},
+		},
+		{
+			name:          "error on chtimes",
+			filePath:      "/some/path.txt",
+			fileExists:    true,
+			expectedError: err,
+			setupMocks: func(client *mocks.Client, sftpFile *mocks.ReadWriteSeekCloser, fileInfo *mocks.FileInfo) {
+				client.EXPECT().Stat("/some/path.txt").Return(fileInfo, nil).Once()
+				client.EXPECT().Chtimes("/some/path.txt", mock.Anything, mock.Anything).Return(err).Once()
+			},
+		},
+		{
+			name:     "setPermissions returns error",
+			filePath: "/some/path.txt",
+			setupMocks: func(client *mocks.Client, sftpFile *mocks.ReadWriteSeekCloser, fileInfo *mocks.FileInfo) {
+				client.EXPECT().Stat("/some/path.txt").Return(fileInfo, nil).Once()
+				client.EXPECT().Chmod("/some/path.txt", os.FileMode(0666)).Return(err).Once()
+			},
+			expectedError:  err,
+			setPermissions: true,
 		},
-		Authority: auth,
-		path:      filepath,
-		sftpfile:  sftpFile,
 	}
 
-	ts.NoError(file.Touch())
-
-	client.AssertExpectations(ts.T())
-	sftpFile.AssertExpectations(ts.T())
-	sourceFileInfo.AssertExpectations(ts.T())
+	for _, tc := range testCases {
+		ts.Run(tc.name, func() {
+			client := mocks.NewClient(ts.T())
+			sftpFile := mocks.NewReadWriteSeekCloser(ts.T())
+			fileInfo := mocks.NewFileInfo(ts.T())
+
+			auth, err := utils.NewAuthority("user@host1.com:22")
+			ts.NoError(err)
+
+			file := &File{
+				fileSystem: &FileSystem{
+					sftpclient: client,
+					options: Options{
+						FilePermissions: func() *string {
+							if tc.setPermissions {
+								return utils.Ptr("0666")
+							}
+							return nil
+						}(),
+					},
+				},
+				Authority: auth,
+				path:      tc.filePath,
+				sftpfile:  sftpFile,
+			}
+
+			tc.setupMocks(client, sftpFile, fileInfo)
+
+			err = file.Touch()
+			if tc.expectedError != nil {
+				ts.ErrorIs(err, tc.expectedError)
+			} else {
+				ts.NoError(err)
+			}
+		})
+	}
 }
 
 func (ts *fileTestSuite) TestDelete() {
@@ -870,6 +946,72 @@ func (ts *fileTestSuite) TestNewFile() {
 	ts.Equal(key, sftpFile.Path())
 }
 
+func (ts *fileTestSuite) TestSetDefaultPermissions() {
+	type testCase struct {
+		name           string
+		client         *mocks.Client
+		options        vfs.Options
+		expectedError  bool
+		expectedErrMsg string
+	}
+
+	tests := []testCase{
+		{
+			name: "No options provided",
+			client: func() *mocks.Client {
+				client := mocks.NewClient(ts.T())
+				return client
+			}(),
+			options:       nil,
+			expectedError: false,
+		},
+		{
+			name: "Default permissions set",
+			client: func() *mocks.Client {
+				client := mocks.NewClient(ts.T())
+				client.EXPECT().Chmod("/some/path.txt", os.FileMode(0644)).Return(nil)
+				return client
+			}(),
+			options: func() vfs.Options {
+				opts := Options{FilePermissions: utils.Ptr("0644")}
+				return opts
+			}(),
+			expectedError: false,
+		},
+		{
+			name: "Chmod returns error",
+			client: func() *mocks.Client {
+				client := mocks.NewClient(ts.T())
+				client.EXPECT().Chmod("/some/path.txt", os.FileMode(0644)).Return(errors.New("chmod error"))
+				return client
+			}(),
+			options: func() vfs.Options {
+				opts := Options{FilePermissions: utils.Ptr("0644")}
+				return opts
+			}(),
+			expectedError:  true,
+			expectedErrMsg: "chmod error",
+		},
+	}
+
+	for _, tt := range tests {
+		ts.Run(tt.name, func() {
+			file := &File{
+				path:       "/some/path.txt",
+				fileSystem: &FileSystem{options: tt.options},
+			}
+
+			err := file.setPermissions(tt.client, tt.options)
+			if tt.expectedError {
+				ts.Error(err)
+				ts.Contains(err.Error(), tt.expectedErrMsg)
+			} else {
+				ts.NoError(err)
+			}
+		})
+	}
+}
+
 func TestFile(t *testing.T) {
 	suite.Run(t, new(fileTestSuite))
 }
diff --git a/backend/sftp/options_test.go b/backend/sftp/options_test.go
index 5fe360c..67ec272 100644
--- a/backend/sftp/options_test.go
+++ b/backend/sftp/options_test.go
@@ -553,6 +553,61 @@ func (o *optionsSuite) TestGetSSHConfig() {
 	}
 }
 
+func (o *optionsSuite) TestGetFileMode() {
+	tests := []struct {
+		name            string
+		filePermissions *string
+		expectedMode    *os.FileMode
+		expectError     bool
+	}{
+		{
+			name:            "NilFilePermissions",
+			filePermissions: nil,
+			expectedMode:    nil,
+			expectError:     false,
+		},
+		{
+			name:            "ValidOctalString",
+			filePermissions: utils.Ptr("0755"),
+			expectedMode:    utils.Ptr(os.FileMode(0755)),
+			expectError:     false,
+		},
+		{
+			name:            "InvalidString",
+			filePermissions: utils.Ptr("invalid"),
+			expectedMode:    nil,
+			expectError:     true,
+		},
+		{
+			name:            "EmptyString",
+			filePermissions: utils.Ptr(""),
+			expectedMode:    nil,
+			expectError:     true,
+		},
+		{
+			name:            "ValidDecimalString",
+			filePermissions: utils.Ptr("493"), // 0755 in decimal
+			expectedMode:    utils.Ptr(os.FileMode(0755)),
+			expectError:     false,
+		},
+	}
+
+	for _, tt := range tests {
+		o.Run(tt.name, func() {
+			opts := &Options{
+				FilePermissions: tt.filePermissions,
+			}
+			mode, err := opts.GetFileMode()
+			if tt.expectError {
+				o.Error(err)
+			} else {
+				o.NoError(err)
+				o.Equal(tt.expectedMode, mode)
+			}
+		})
+	}
+}
+
 func TestUtils(t *testing.T) {
 	suite.Run(t, new(optionsSuite))
 }