-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 08f2ad2
Showing
10 changed files
with
491 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
## SFTP Server with Google Cloud Storage backend | ||
|
||
Runs an isolated, sandboxed SFTP server that only interacts with virtual backing storage on Google Cloud Storage (GCS). | ||
|
||
|
||
Set the following environment variables, e.g. | ||
|
||
``` | ||
SFTP_USERNAME=user123 | ||
SFTP_PASSWORD=kl5dfqpw3NXCZX0 | ||
SFTP_PORT=2022 | ||
SFTP_SERVER_KEY_PATH=/id_rsa | ||
GCS_CREDENTIALS_FILE=/credentials.json | ||
GCS_BUCKET=my-sftp-bucket | ||
``` | ||
|
||
GET, PUT, STAT, LIST & MKDIR are currently the only methods implemented. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package main | ||
|
||
import ( | ||
"os" | ||
"log" | ||
) | ||
|
||
func mustGetenv(k string) string { | ||
v := os.Getenv(k) | ||
if v == "" { | ||
log.Fatalf("%s environment variable not set.", k) | ||
} | ||
return v | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package gsftp | ||
|
||
import ( | ||
"io" | ||
"os" | ||
|
||
"cloud.google.com/go/storage" | ||
) | ||
|
||
type gcsHandler struct { | ||
client *storage.Client | ||
bucket *storage.BucketHandle | ||
} | ||
|
||
type listerat []os.FileInfo | ||
|
||
// Modeled after strings.Reader's ReadAt() implementation | ||
func (f listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) { | ||
var n int | ||
if offset >= int64(len(f)) { | ||
return 0, io.EOF | ||
} | ||
n = copy(ls, f[offset:]) | ||
if n < len(ls) { | ||
return n, io.EOF | ||
} | ||
return n, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package gsftp | ||
|
||
import ( | ||
"os" | ||
"time" | ||
|
||
"cloud.google.com/go/storage" | ||
) | ||
|
||
type SyntheticFileInfo struct { | ||
objAttr *storage.ObjectAttrs | ||
prefix string | ||
} | ||
|
||
func (f *SyntheticFileInfo) Name() string { // base name of the file | ||
if f.objAttr.Prefix == "" { | ||
return f.objAttr.Name[len(f.prefix):] | ||
} else { | ||
return f.objAttr.Prefix[len(f.prefix) : len(f.objAttr.Prefix)-1] | ||
} | ||
} | ||
|
||
func (f *SyntheticFileInfo) Size() int64 { // length in bytes for regular files; system-dependent for others | ||
return f.objAttr.Size | ||
} | ||
|
||
func (f *SyntheticFileInfo) Mode() os.FileMode { // file mode bits | ||
if f.objAttr.Prefix == "" { | ||
return 0777 | ||
} else { | ||
return os.ModeDir | 0777 | ||
} | ||
} | ||
|
||
func (f *SyntheticFileInfo) ModTime() time.Time { // modification time | ||
if f.objAttr.Prefix == "" { | ||
return f.objAttr.Updated | ||
} else { | ||
return time.Now() | ||
} | ||
} | ||
|
||
func (f *SyntheticFileInfo) IsDir() bool { // abbreviation for Mode().IsDir() | ||
return f.objAttr.Prefix == "" | ||
} | ||
|
||
func (f *SyntheticFileInfo) Sys() interface{} { // underlying data source (can return nil) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package gsftp | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"io/ioutil" | ||
) | ||
|
||
func NewReadAtBuffer(r io.ReadCloser) (io.ReaderAt, error) { | ||
buf, err := ioutil.ReadAll(r) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
err = r.Close() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return bytes.NewReader(buf), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package gsftp | ||
|
||
import ( | ||
"io" | ||
"sync" | ||
) | ||
|
||
type WriteAtBuffer struct { | ||
buf []byte | ||
m sync.Mutex | ||
|
||
// GrowthCoeff defines the growth rate of the internal buffer. By | ||
// default, the growth rate is 1, where expanding the internal | ||
// buffer will allocate only enough capacity to fit the new expected | ||
// length. | ||
GrowthCoeff float64 | ||
|
||
Writer io.WriteCloser | ||
} | ||
|
||
// NewWriteAtBuffer creates a WriteAtBuffer with an internal buffer | ||
// provided by buf. | ||
func NewWriteAtBuffer(w io.WriteCloser, buf []byte) *WriteAtBuffer { | ||
return &WriteAtBuffer{ | ||
Writer: w, | ||
buf: buf, | ||
} | ||
} | ||
|
||
// WriteAt writes a slice of bytes to a buffer starting at the position provided | ||
// The number of bytes written will be returned, or error. Can overwrite previous | ||
// written slices if the write ats overlap. | ||
func (b *WriteAtBuffer) WriteAt(p []byte, pos int64) (n int, err error) { | ||
pLen := len(p) | ||
expLen := pos + int64(pLen) | ||
b.m.Lock() | ||
defer b.m.Unlock() | ||
if int64(len(b.buf)) < expLen { | ||
if int64(cap(b.buf)) < expLen { | ||
if b.GrowthCoeff < 1 { | ||
b.GrowthCoeff = 1 | ||
} | ||
newBuf := make([]byte, expLen, int64(b.GrowthCoeff*float64(expLen))) | ||
copy(newBuf, b.buf) | ||
b.buf = newBuf | ||
} | ||
b.buf = b.buf[:expLen] | ||
} | ||
copy(b.buf[pos:], p) | ||
return pLen, nil | ||
} | ||
|
||
// Bytes returns a slice of bytes written to the buffer. | ||
func (b *WriteAtBuffer) Bytes() []byte { | ||
b.m.Lock() | ||
defer b.m.Unlock() | ||
return b.buf | ||
} | ||
|
||
func (b *WriteAtBuffer) Close() error { | ||
b.m.Lock() | ||
defer b.m.Unlock() | ||
|
||
_, err := b.Writer.Write(b.buf) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return b.Writer.Close() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package gsftp | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"log" | ||
"os" | ||
|
||
"cloud.google.com/go/storage" | ||
"github.com/pkg/sftp" | ||
"google.golang.org/api/iterator" | ||
) | ||
|
||
// Example Handlers | ||
func (fs *gcsHandler) Fileread(r *sftp.Request) (io.ReaderAt, error) { | ||
object := fs.bucket.Object(r.Filepath[1:]) | ||
|
||
log.Printf("Reading file %s", r.Filepath) | ||
|
||
reader, err := object.NewReader(r.Context()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return NewReadAtBuffer(reader) | ||
} | ||
|
||
func (fs *gcsHandler) Filewrite(r *sftp.Request) (io.WriterAt, error) { | ||
object := fs.bucket.Object(r.Filepath[1:]) | ||
|
||
log.Printf("Writing file %s", r.Filepath) | ||
|
||
writer := object.NewWriter(r.Context()) | ||
|
||
return NewWriteAtBuffer(writer, []byte{}), nil | ||
} | ||
|
||
func (fs *gcsHandler) Filecmd(r *sftp.Request) error { | ||
switch r.Method { | ||
case "Setstat": | ||
return nil | ||
case "Rename": | ||
return fmt.Errorf("not implemented") | ||
case "Rmdir", "Remove": | ||
return fmt.Errorf("not implemented") | ||
case "Mkdir": | ||
object := fs.bucket.Object(r.Filepath[1:] + "/") | ||
|
||
log.Printf("Creating directory %s", r.Filepath) | ||
|
||
writer := object.NewWriter(r.Context()) | ||
|
||
err := writer.Close() | ||
return err | ||
case "Symlink": | ||
return fmt.Errorf("not implemented") | ||
} | ||
return nil | ||
} | ||
|
||
func (fs *gcsHandler) Filelist(r *sftp.Request) (sftp.ListerAt, error) { | ||
switch r.Method { | ||
case "List": | ||
log.Printf("Listing directory for path %s", r.Filepath) | ||
|
||
prefix := r.Filepath[1:] | ||
if prefix != "" { | ||
prefix += "/" | ||
} | ||
|
||
objects := fs.bucket.Objects(r.Context(), &storage.Query{ | ||
Delimiter: "/", | ||
Prefix: prefix, | ||
}) | ||
|
||
list := []os.FileInfo{} | ||
|
||
for { | ||
objAttrs, err := objects.Next() | ||
if err == iterator.Done { | ||
break | ||
} | ||
if err != nil { | ||
log.Printf("Error iterating directory %s: %s", r.Filepath, err) | ||
|
||
return nil, err | ||
} | ||
|
||
// Don't include self. | ||
if ((prefix != "") && (objAttrs.Prefix == prefix)) || ((objAttrs.Prefix == "") && (objAttrs.Name == prefix)) { | ||
continue | ||
} | ||
|
||
list = append(list, &SyntheticFileInfo{ | ||
prefix: prefix, | ||
objAttr: objAttrs, | ||
}) | ||
} | ||
|
||
return listerat(list), nil | ||
case "Stat": | ||
if r.Filepath == "/" { | ||
return listerat([]os.FileInfo{ | ||
&SyntheticFileInfo{ | ||
objAttr: &storage.ObjectAttrs{ | ||
Prefix: "/", | ||
}, | ||
}, | ||
}), nil | ||
} | ||
|
||
object := fs.bucket.Object(r.Filepath[1:]) | ||
|
||
log.Printf("Getting file info for %s", r.Filepath) | ||
|
||
attrs, err := object.Attrs(r.Context()) | ||
if err == storage.ErrObjectNotExist { | ||
object := fs.bucket.Object(r.Filepath[1:] + "/") | ||
|
||
log.Printf("Retrying file info for %s", r.Filepath+"/") | ||
|
||
attrs, err = object.Attrs(r.Context()) | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
file := &SyntheticFileInfo{ | ||
objAttr: attrs, | ||
} | ||
return listerat([]os.FileInfo{file}), nil | ||
case "Readlink": | ||
return nil, fmt.Errorf("not implemented") | ||
} | ||
return nil, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package gsftp | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"cloud.google.com/go/storage" | ||
"github.com/pkg/sftp" | ||
"google.golang.org/api/option" | ||
) | ||
|
||
func GoogleCloudStorageHandler(ctx context.Context, credentialsFile string, bucketName string) (*sftp.Handlers, error) { | ||
client, err := storage.NewClient(ctx, option.WithCredentialsFile(credentialsFile)) | ||
if err != nil { | ||
return nil, fmt.Errorf("Storage Client Error: %s", err) | ||
} | ||
|
||
bucket := client.Bucket(bucketName) | ||
|
||
handler := &gcsHandler{ | ||
client: client, | ||
bucket: bucket, | ||
} | ||
|
||
return &sftp.Handlers{handler, handler, handler, handler}, nil | ||
} |
Oops, something went wrong.