Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions backend/sftp/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 58 additions & 17 deletions backend/sftp/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sftp

import (
"errors"
"fmt"
"io"
"os"
"path"
Expand Down Expand Up @@ -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()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified timer here...technically unrelated to the actual changes

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
Expand All @@ -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)
}

Expand Down Expand Up @@ -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() &&
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -286,6 +283,7 @@ func (f *File) Close() error {
}
f.sftpfile = nil
}

// no op for unopened file
return nil
}
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions backend/sftp/fileSystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading