diff --git a/README.md b/README.md index 3a624ea..1309153 100644 --- a/README.md +++ b/README.md @@ -263,12 +263,17 @@ backed file implementation. This can be used in other memory backed file systems with ease. Plans are to add a radix tree memory stored file system using InMemoryFile. +## SftpFs + +Afero has experimental support for secure file transfer protocol (sftp). Which can +be used to perform file operations over a encrypted channel. + ## Desired/possible backends The following is a short list of possible backends we hope someone will implement: -* SSH/SCP +* SSH * ZIP * TAR * S3 diff --git a/sftp.go b/sftp.go new file mode 100644 index 0000000..a095a54 --- /dev/null +++ b/sftp.go @@ -0,0 +1,128 @@ +// Copyright © 2015 Jerry Jacobs . +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package afero + +import ( + "os" + "time" + + "github.com/spf13/afero/sftp" + + "github.com/pkg/sftp" +) + +// SftpFs is a Fs implementation that uses functions provided by the sftp package. +// +// For details in any method, check the documentation of the sftp package +// (github.com/pkg/sftp). +type SftpFs struct{ + SftpClient *sftp.Client +} + +func (s SftpFs) Name() string { return "SftpFs" } + +func (s SftpFs) Create(name string) (File, error) { + f, err := sftpfs.FileCreate(s.SftpClient, name) + return f, err +} + +func (s SftpFs) Mkdir(name string, perm os.FileMode) error { + err := s.SftpClient.Mkdir(name) + if err != nil { + return err + } + return s.SftpClient.Chmod(name, perm) +} + +func (s SftpFs) MkdirAll(path string, perm os.FileMode) error { + // Fast path: if we can tell whether path is a directory or file, stop with success or error. + dir, err := s.Stat(path) + if err == nil { + if dir.IsDir() { + return nil + } + return err + } + + // Slow path: make sure parent exists and then call Mkdir for path. + i := len(path) + for i > 0 && os.IsPathSeparator(path[i-1]) { // Skip trailing path separator. + i-- + } + + j := i + for j > 0 && !os.IsPathSeparator(path[j-1]) { // Scan backward over element. + j-- + } + + if j > 1 { + // Create parent + err = s.MkdirAll(path[0:j-1], perm) + if err != nil { + return err + } + } + + // Parent now exists; invoke Mkdir and use its result. + err = s.Mkdir(path, perm) + if err != nil { + // Handle arguments like "foo/." by + // double-checking that directory doesn't exist. + dir, err1 := s.Lstat(path) + if err1 == nil && dir.IsDir() { + return nil + } + return err + } + return nil +} + +func (s SftpFs) Open(name string) (File, error) { + f, err := sftpfs.FileOpen(s.SftpClient, name) + return f, err +} + +func (s SftpFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + return nil,nil +} + +func (s SftpFs) Remove(name string) error { + return s.SftpClient.Remove(name) +} + +func (s SftpFs) RemoveAll(path string) error { + // TODO have a look at os.RemoveAll + // https://github.com/golang/go/blob/master/src/os/path.go#L66 + return nil +} + +func (s SftpFs) Rename(oldname, newname string) error { + return s.SftpClient.Rename(oldname, newname) +} + +func (s SftpFs) Stat(name string) (os.FileInfo, error) { + return s.SftpClient.Stat(name) +} + +func (s SftpFs) Lstat(p string) (os.FileInfo, error) { + return s.SftpClient.Lstat(p) +} + +func (s SftpFs) Chmod(name string, mode os.FileMode) error { + return s.SftpClient.Chmod(name, mode) +} + +func (s SftpFs) Chtimes(name string, atime time.Time, mtime time.Time) error { + return s.SftpClient.Chtimes(name, atime, mtime) +} diff --git a/sftp/file.go b/sftp/file.go new file mode 100644 index 0000000..bab4abc --- /dev/null +++ b/sftp/file.go @@ -0,0 +1,95 @@ +// Copyright © 2015 Jerry Jacobs . +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sftpfs + +import ( + "os" + "github.com/pkg/sftp" +) + +type File struct { + fd *sftp.File +} + +func FileOpen(s *sftp.Client, name string) (*File, error) { + fd, err := s.Open(name) + if err != nil { + return &File{}, err + } + return &File{fd: fd}, nil +} + +func FileCreate(s *sftp.Client, name string) (*File, error) { + fd, err := s.Create(name) + if err != nil { + return &File{}, err + } + return &File{fd: fd}, nil +} + +func (f *File) Close() error { + return f.fd.Close() +} + +func (f *File) Name() string { + return f.fd.Name() +} + +func (f *File) Stat() (os.FileInfo, error) { + return f.fd.Stat() +} + +func (f *File) Sync() error { + return nil +} + +func (f *File) Truncate(size int64) error { + return f.fd.Truncate(size) +} + +func (f *File) Read(b []byte) (n int, err error) { + return f.fd.Read(b) +} + +// TODO +func (f *File) ReadAt(b []byte, off int64) (n int, err error) { + return 0,nil +} + +// TODO +func (f *File) Readdir(count int) (res []os.FileInfo, err error) { + return nil,nil +} + +// TODO +func (f *File) Readdirnames(n int) (names []string, err error) { + return nil,nil +} + +func (f *File) Seek(offset int64, whence int) (int64, error) { + return f.fd.Seek(offset, whence) +} + +func (f *File) Write(b []byte) (n int, err error) { + return f.fd.Write(b) +} + +// TODO +func (f *File) WriteAt(b []byte, off int64) (n int, err error) { + return 0,nil +} + +func (f *File) WriteString(s string) (ret int, err error) { + return f.fd.Write([]byte(s)) +} diff --git a/sftp_test.go b/sftp_test.go new file mode 100644 index 0000000..49bdda8 --- /dev/null +++ b/sftp_test.go @@ -0,0 +1,92 @@ +// Copyright © 2015 Jerry Jacobs . +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package afero + +import ( + "os" + "io/ioutil" + "testing" + + "github.com/xor-gate/afero" + + "golang.org/x/crypto/ssh" + "github.com/pkg/sftp" +) + +type SftpFsContext struct { + sshc *ssh.Client + sshcfg *ssh.ClientConfig + sftpc *sftp.Client +} + +func SftpConnect(user string, host string) (*SftpFsContext, error) { + pemBytes, err := ioutil.ReadFile(os.Getenv("HOME") + "/.ssh/id_rsa") + if err != nil { + return nil,err + } + + signer, err := ssh.ParsePrivateKey(pemBytes) + if err != nil { + return nil,err + } + + sshcfg := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + } + + sshc, err := ssh.Dial("tcp", host, sshcfg) + if err != nil { + return nil,err + } + + sftpc, err := sftp.NewClient(sshc) + if err != nil { + return nil,err + } + + ctx := &SftpFsContext{ + sshc: sshc, + sshcfg: sshcfg, + sftpc: sftpc, + } + + return ctx,nil +} + +func (ctx *SftpFsContext) Disconnect() error { + ctx.sftpc.Close() + ctx.sshc.Close() + return nil +} + +func TestSftpCreate(t *testing.T) { + ctx, err := SftpConnect("user", "host:port") + if err != nil { + t.Fatal(err) + } + defer ctx.Disconnect() + + var AppFs afero.Fs = afero.SftpFs{ + SftpClient: ctx.sftpc, + } + + file, err := AppFs.Create("aferoSftpFsTestFile") + if err != nil { + t.Error(err) + } + defer file.Close() +}