It seems to work

This commit is contained in:
Quentin 2021-11-20 13:42:20 +01:00
parent 87fff9843d
commit e10f04c5e3
9 changed files with 151 additions and 83 deletions

View file

@ -14,6 +14,7 @@ type Config struct {
UserNameAttr string `env:"BAGAGE_LDAP_USERNAME_ATTR" default:"cn"` UserNameAttr string `env:"BAGAGE_LDAP_USERNAME_ATTR" default:"cn"`
Endpoint string `env:"BAGAGE_S3_ENDPOINT" default:"garage.deuxfleurs.fr"` Endpoint string `env:"BAGAGE_S3_ENDPOINT" default:"garage.deuxfleurs.fr"`
UseSSL bool `env:"BAGAGE_S3_SSL" default:"true"` UseSSL bool `env:"BAGAGE_S3_SSL" default:"true"`
S3Cache string `env:"BAGAGE_S3_CACHE" default:"./s3_cache"`
SSHKey string `env:"BAGAGE_SSH_KEY" default:"id_rsa"` SSHKey string `env:"BAGAGE_SSH_KEY" default:"id_rsa"`
} }

View file

@ -13,16 +13,19 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"os"
) )
func main() { func main() {
log.Println("=== Starting Bagage ===") log.Println("=== Starting Bagage ===")
config := (&Config{}).LoadWithDefault().LoadWithEnv() config := (&Config{}).LoadWithDefault().LoadWithEnv()
log.Println(config) log.Println(config)
done := make(chan error) // Some init
os.MkdirAll(config.S3Cache, 0755)
// Launch our submodules
done := make(chan error)
go httpServer(config, done) go httpServer(config, done)
go sshServer(config, done) go sshServer(config, done)
@ -148,7 +151,7 @@ func handleSSHConn(nConn net.Conn, dconfig *Config, config *ssh.ServerConfig) {
return return
} }
fs := s3.NewS3FS(mc) fs := s3.NewS3FS(mc, dconfig.S3Cache)
server, err := sftp.NewServer(ctx, channel, &fs) server, err := sftp.NewServer(ctx, channel, &fs)
if err != nil { if err != nil {

View file

@ -1,13 +1,13 @@
package s3 package s3
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"log" "log"
"mime" "mime"
"os"
"path" "path"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
@ -17,9 +17,9 @@ type S3File struct {
fs *S3FS fs *S3FS
obj *minio.Object obj *minio.Object
objw *io.PipeWriter objw *io.PipeWriter
cache *os.File
donew chan error donew chan error
pos int64 pos int64
eof bool
entries []fs.FileInfo entries []fs.FileInfo
Path S3Path Path S3Path
} }
@ -49,6 +49,11 @@ func (f *S3File) Close() error {
f.objw = nil f.objw = nil
} }
if f.cache != nil {
err = append(err, f.writeFlush())
f.cache = nil
}
count := 0 count := 0
for _, e := range err { for _, e := range err {
if e != nil { if e != nil {
@ -57,7 +62,7 @@ func (f *S3File) Close() error {
} }
} }
if count > 0 { if count > 0 {
return errors.New(fmt.Sprintf("%d errors when closing this WebDAV File. Read previous logs to know more.", count)) return errors.New(fmt.Sprintf("%d errors when closing this S3 File. Read previous logs to know more.", count))
} }
return nil return nil
} }
@ -74,10 +79,15 @@ func (f *S3File) loadObject() error {
} }
func (f *S3File) Read(p []byte) (n int, err error) { func (f *S3File) Read(p []byte) (n int, err error) {
log.Printf("s3 Read\n") //log.Printf("s3 Read\n")
//if f.Stat() & OBJECT == 0 { /* @FIXME Ideally we would check against OBJECT but we need a non OPAQUE_KEY */ //if f.Stat() & OBJECT == 0 { /* @FIXME Ideally we would check against OBJECT but we need a non OPAQUE_KEY */
// return 0, os.ErrInvalid // return 0, os.ErrInvalid
//} //}
if f.cache != nil {
return f.cache.Read(p)
}
if err := f.loadObject(); err != nil { if err := f.loadObject(); err != nil {
return 0, err return 0, err
} }
@ -86,6 +96,10 @@ func (f *S3File) Read(p []byte) (n int, err error) {
} }
func (f *S3File) ReadAt(p []byte, off int64) (n int, err error) { func (f *S3File) ReadAt(p []byte, off int64) (n int, err error) {
if f.cache != nil {
return f.cache.ReadAt(p, off)
}
stat, err := f.Stat() stat, err := f.Stat()
if err != nil { if err != nil {
return 0, err return 0, err
@ -93,7 +107,7 @@ func (f *S3File) ReadAt(p []byte, off int64) (n int, err error) {
return 0, io.EOF return 0, io.EOF
} }
log.Printf("s3 ReadAt %v\n", off) //log.Printf("s3 ReadAt %v\n", off)
if err := f.loadObject(); err != nil { if err := f.loadObject(); err != nil {
return 0, err return 0, err
} }
@ -101,45 +115,101 @@ func (f *S3File) ReadAt(p []byte, off int64) (n int, err error) {
return f.obj.ReadAt(p, off) return f.obj.ReadAt(p, off)
} }
func (f *S3File) WriteAt(p []byte, off int64) (n int, err error) { func (f *S3File) initCache() error {
return 0, errors.New("not implemented") // We use a locally cached file instead of writing directly to S3
// When the user calls close, the file is flushed on S3.
// Check writeFlush below.
if f.cache == nil {
// We create a temp file in the configured folder
// We do not use the default tmp file as files can be very large
// and could fillup the RAM (often /tmp is mounted in RAM)
tmp, err := os.CreateTemp(f.fs.local, "bagage-cache")
if err != nil {
return err
}
f.cache = tmp
// Problem: WriteAt override the existing file, if it exists
// So if when we stat the file, its size is greater than zero,
// we download it in our cache
file, err := f.Stat()
if err != nil {
return err
} else if file.Size() != 0 {
// We get a Reader on our object
object, err := f.fs.mc.GetObject(f.fs.ctx, f.Path.Bucket, f.Path.Key, minio.GetObjectOptions{})
if err != nil {
return err
}
// We inject it in our cache file
if _, err = io.Copy(f.cache, object); err != nil {
return err
}
}
}
return nil
}
func (f *S3File) WriteAt(p []byte, off int64) (n int, err error) {
f.initCache()
// And now we simply apply the command on our cache
return f.cache.WriteAt(p, off)
} }
func (f *S3File) Write(p []byte) (n int, err error) { func (f *S3File) Write(p []byte) (n int, err error) {
/*if f.path.class != OBJECT { f.initCache()
return 0, os.ErrInvalid
}*/
if f.objw == nil { return f.cache.Write(p)
if f.pos != 0 {
return 0, errors.New("writing with an offset is not implemented")
} }
r, w := io.Pipe() func (f *S3File) writeFlush() error {
f.donew = make(chan error, 1) // Only needed if we used a write cache
f.objw = w if f.cache == nil {
return nil
}
// Rewind the file to copy from the start
_, err := f.cache.Seek(0, 0)
if err != nil {
return err
}
// Get a FileInfo object as minio needs its size (ideally)
stat, err := f.cache.Stat()
if err != nil {
return err
}
// Send the file to minio
contentType := mime.TypeByExtension(path.Ext(f.Path.Key)) contentType := mime.TypeByExtension(path.Ext(f.Path.Key))
go func() { _, err = f.fs.mc.PutObject(f.fs.ctx, f.Path.Bucket, f.Path.Key, f.cache, stat.Size(), minio.PutObjectOptions{
/* @FIXME ContentType: contentType,
PutObject has a strange behaviour when used with unknown size, it supposes the final size will be 5TiB. })
Then it computes that, following the final size of the file, each part of the multipart upload should be 512MiB, which leads to big allocations. if err != nil {
The culprit is OptimalPartInfo: https://github.com/minio/minio-go/blob/62beca8cd87e9960d88793320220ad2c159bb5e5/api-put-object-common.go#L70 return err
We set this value to the minimum allowed one, 5MiB.
The minimum value is set here: https://github.com/minio/minio-go/blob/62beca8cd87e9960d88793320220ad2c159bb5e5/constants.go#L24
Because Multipart uploads seems to be limited to 10 000 parts, it might be possible that we are limited to 50 GiB files, which is still good enough.
Ref: https://github.com/minio/minio-go/blob/62beca8cd87e9960d88793320220ad2c159bb5e5/api-put-object-common.go#L110-L112
*/
_, err := f.fs.mc.PutObject(context.Background(), f.Path.Bucket, f.Path.Key, r, -1, minio.PutObjectOptions{ContentType: contentType, PartSize: 5*1024*1024})
f.donew <- err
}()
} }
return f.objw.Write(p) // Close the cache file and remove it
err = f.cache.Close()
if err != nil {
return err
}
err = os.Remove(f.cache.Name())
if err != nil {
return err
}
return nil
} }
func (f *S3File) Seek(offset int64, whence int) (int64, error) { func (f *S3File) Seek(offset int64, whence int) (int64, error) {
if f.cache != nil {
return f.cache.Seek(offset, whence)
}
if err := f.loadObject(); err != nil { if err := f.loadObject(); err != nil {
return 0, err return 0, err
} }

View file

@ -22,13 +22,15 @@ import (
type S3FS struct { type S3FS struct {
cache map[string]*S3Stat cache map[string]*S3Stat
mc *minio.Client mc *minio.Client
local string
ctx context.Context ctx context.Context
} }
func NewS3FS(mc *minio.Client) S3FS { func NewS3FS(mc *minio.Client, local string) S3FS {
return S3FS{ return S3FS{
cache: make(map[string]*S3Stat), cache: make(map[string]*S3Stat),
mc: mc, mc: mc,
local: local,
} }
} }

View file

@ -1,7 +1,6 @@
package s3 package s3
import ( import (
"log"
"errors" "errors"
"io/fs" "io/fs"
"path" "path"
@ -105,7 +104,6 @@ func (s *S3Stat) Name() string {
} }
func (s *S3Stat) Size() int64 { func (s *S3Stat) Size() int64 {
log.Println("stat size: ", s.obj.Size)
return s.obj.Size return s.obj.Size
} }

View file

@ -98,4 +98,3 @@ func (a *allocator) isRequestOrderIDUsed(requestOrderID uint32) bool {
_, ok := a.used[requestOrderID] _, ok := a.used[requestOrderID]
return ok return ok
} }

View file

@ -218,4 +218,3 @@ func (s *packetManager) maybeSendPackets() {
// } // }
// return res // return res
// } // }

View file

@ -4,12 +4,12 @@ package sftp
import ( import (
"context" "context"
"log"
"encoding" "encoding"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"os" "os"
"strconv" "strconv"
"sync" "sync"
@ -21,7 +21,7 @@ import (
const ( const (
// SftpServerWorkerCount defines the number of workers for the SFTP server // SftpServerWorkerCount defines the number of workers for the SFTP server
SftpServerWorkerCount = 8 SftpServerWorkerCount = 1
) )
// Server is an SSH File Transfer Protocol (sftp) server. // Server is an SSH File Transfer Protocol (sftp) server.
@ -194,7 +194,7 @@ func handlePacket(s *Server, p orderedRequest) error {
case *sshFxpLstatPacket: case *sshFxpLstatPacket:
log.Println("pkt: lstat: ", p.Path) log.Println("pkt: lstat: ", p.Path)
// stat the requested file // stat the requested file
info, err := os.Lstat(toLocalPath(p.Path)) info, err := s.fs.Stat(s.ctx, p.Path)
rpkt = &sshFxpStatResponse{ rpkt = &sshFxpStatResponse{
ID: p.ID, ID: p.ID,
info: info, info: info,
@ -219,43 +219,39 @@ func handlePacket(s *Server, p orderedRequest) error {
} }
case *sshFxpMkdirPacket: case *sshFxpMkdirPacket:
log.Println("pkt: mkdir: ", p.Path) log.Println("pkt: mkdir: ", p.Path)
err := os.Mkdir(toLocalPath(p.Path), 0755) err := s.fs.Mkdir(s.ctx, p.Path, 0755)
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpRmdirPacket: case *sshFxpRmdirPacket:
log.Println("pkt: rmdir: ", p.Path) log.Println("pkt: rmdir: ", p.Path)
err := os.Remove(toLocalPath(p.Path)) err := s.fs.RemoveAll(s.ctx, p.Path)
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpRemovePacket: case *sshFxpRemovePacket:
log.Println("pkt: rm: ", p.Filename) log.Println("pkt: rm: ", p.Filename)
err := os.Remove(toLocalPath(p.Filename)) err := s.fs.RemoveAll(s.ctx, p.Filename)
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpRenamePacket: case *sshFxpRenamePacket:
log.Println("pkt: rename: ", p.Oldpath, ", ", p.Newpath) log.Println("pkt: rename: ", p.Oldpath, ", ", p.Newpath)
err := os.Rename(toLocalPath(p.Oldpath), toLocalPath(p.Newpath)) err := s.fs.Rename(s.ctx, p.Oldpath, p.Newpath)
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpSymlinkPacket: case *sshFxpSymlinkPacket:
log.Println("pkt: ln -s: ", p.Targetpath, ", ", p.Linkpath) log.Println("pkt: ln -s: ", p.Targetpath, ", ", p.Linkpath)
err := os.Symlink(toLocalPath(p.Targetpath), toLocalPath(p.Linkpath)) err := s.fs.Rename(s.ctx, p.Targetpath, p.Linkpath)
rpkt = statusFromError(p.ID, err) rpkt = statusFromError(p.ID, err)
case *sshFxpClosePacket: case *sshFxpClosePacket:
log.Println("pkt: close handle: ", p.Handle) log.Println("pkt: close handle: ", p.Handle)
rpkt = statusFromError(p.ID, s.closeHandle(p.Handle)) rpkt = statusFromError(p.ID, s.closeHandle(p.Handle))
case *sshFxpReadlinkPacket: case *sshFxpReadlinkPacket:
log.Println("pkt: read: ", p.Path) log.Println("pkt: readlink: ", p.Path)
f, err := os.Readlink(toLocalPath(p.Path))
rpkt = &sshFxpNamePacket{ rpkt = &sshFxpNamePacket{
ID: p.ID, ID: p.ID,
NameAttrs: []*sshFxpNameAttr{ NameAttrs: []*sshFxpNameAttr{
{ {
Name: f, Name: p.Path,
LongName: f, LongName: p.Path,
Attrs: emptyFileStat, Attrs: emptyFileStat,
}, },
}, },
} }
if err != nil {
rpkt = statusFromError(p.ID, err)
}
case *sshFxpRealpathPacket: case *sshFxpRealpathPacket:
log.Println("pkt: absolute path: ", p.Path) log.Println("pkt: absolute path: ", p.Path)
f := s3.NewS3Path(p.Path).Path f := s3.NewS3Path(p.Path).Path
@ -288,7 +284,7 @@ func handlePacket(s *Server, p orderedRequest) error {
case *sshFxpReadPacket: case *sshFxpReadPacket:
var err error = EBADF var err error = EBADF
f, ok := s.getHandle(p.Handle) f, ok := s.getHandle(p.Handle)
log.Println("pkt: read handle: ", p.Handle, f.Path.Path) //log.Println("pkt: read handle: ", p.Handle, f.Path.Path)
if ok { if ok {
err = nil err = nil
data := p.getDataSlice(s.pktMgr.alloc, orderID) data := p.getDataSlice(s.pktMgr.alloc, orderID)
@ -309,7 +305,7 @@ func handlePacket(s *Server, p orderedRequest) error {
} }
case *sshFxpWritePacket: case *sshFxpWritePacket:
log.Println("pkt: write handle: ", p.Handle, ", Offset: ", p.Offset) //log.Println("pkt: write handle: ", p.Handle, ", Offset: ", p.Offset)
f, ok := s.getHandle(p.Handle) f, ok := s.getHandle(p.Handle)
var err error = EBADF var err error = EBADF
if ok { if ok {
@ -324,7 +320,7 @@ func handlePacket(s *Server, p orderedRequest) error {
rpkt = p.respond(s) rpkt = p.respond(s)
} }
case serverRespondablePacket: case serverRespondablePacket:
log.Println("pkt: respondable") //log.Println("pkt: respondable")
rpkt = p.respond(s) rpkt = p.respond(s)
default: default:
return fmt.Errorf("unexpected packet type %T", p) return fmt.Errorf("unexpected packet type %T", p)
@ -477,7 +473,7 @@ func (p *sshFxpOpenPacket) respond(svr *Server) responsePacket {
} }
func (p *sshFxpReaddirPacket) respond(svr *Server) responsePacket { func (p *sshFxpReaddirPacket) respond(svr *Server) responsePacket {
log.Println("pkt: readdir: ", p.Handle) //log.Println("pkt: readdir: ", p.Handle)
f, ok := svr.getHandle(p.Handle) f, ok := svr.getHandle(p.Handle)
if !ok { if !ok {
return statusFromError(p.ID, EBADF) return statusFromError(p.ID, EBADF)

View file

@ -16,7 +16,7 @@ func (wd WebDav) WithMC(mc *minio.Client) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
(&webdav.Handler{ (&webdav.Handler{
Prefix: wd.WithConfig.DavPath, Prefix: wd.WithConfig.DavPath,
FileSystem: s3.NewS3FS(mc), FileSystem: s3.NewS3FS(mc, wd.WithConfig.S3Cache),
LockSystem: webdav.NewMemLS(), LockSystem: webdav.NewMemLS(),
Logger: func(r *http.Request, err error) { Logger: func(r *http.Request, err error) {
log.Printf("INFO: %s %s %s\n", r.RemoteAddr, r.Method, r.URL) log.Printf("INFO: %s %s %s\n", r.RemoteAddr, r.Method, r.URL)