From a405c69aaf742c4abeb015e92e738035a1f72165 Mon Sep 17 00:00:00 2001 From: John Judd Date: Mon, 16 Sep 2024 13:40:13 -0500 Subject: [PATCH] =?UTF-8?q?Add=20ability=20to=20set=20file=20permissions?= =?UTF-8?q?=20after=20writing=20an=20SFTP=20file.=20Resol=E2=80=A6=20(#203?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add ability to set file permissions after writing an SFTP file. Resolves #202. * fixed version in changelog * updated Options to use string for file permissions * removed unused helper funcs * correct doc --- .mockery.yaml | 3 + CHANGELOG.md | 4 + backend/sftp/doc.go | 17 + backend/sftp/file.go | 75 +++- backend/sftp/fileSystem.go | 1 + backend/sftp/file_test.go | 402 ++++++++++++++------- backend/sftp/mocks/Client.go | 403 +++++++++++++++++++++- backend/sftp/mocks/FileInfo.go | 228 +++++++++++- backend/sftp/mocks/ReadWriteSeekCloser.go | 246 +++++++++++++ backend/sftp/mocks/SFTPFile.go | 87 ----- backend/sftp/options.go | 19 +- backend/sftp/options_test.go | 55 +++ utils/utils.go | 5 + 13 files changed, 1286 insertions(+), 259 deletions(-) create mode 100644 backend/sftp/mocks/ReadWriteSeekCloser.go delete mode 100644 backend/sftp/mocks/SFTPFile.go diff --git a/.mockery.yaml b/.mockery.yaml index ee6c2760..481e5484 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 edd1e08e..74af6d48 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 d1799807..2908c34e 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 7665f011..cc800328 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 aa6c6961..3c2fdbee 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/file_test.go b/backend/sftp/file_test.go index 676073a0..f071557c 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/mocks/Client.go b/backend/sftp/mocks/Client.go index 013916f9..ddaee971 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 7d429aa5..1047aaef 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 00000000..6e96bfd7 --- /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 fb485f94..00000000 --- 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 7d9c9757..071495e9 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/backend/sftp/options_test.go b/backend/sftp/options_test.go index 5fe360c7..67ec2726 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)) } diff --git a/utils/utils.go b/utils/utils.go index 87cef302..a1954583 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 +}