Working writes

This commit is contained in:
Quentin 2021-08-20 15:52:08 +02:00
parent 55e9aa7018
commit 36cef67f6b

307
main.go
View file

@ -4,8 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"log" "log"
"mime"
"net/http" "net/http"
"os" "os"
"path" "path"
@ -49,11 +51,13 @@ func main() {
FileSystem: NewGarageFS(), FileSystem: NewGarageFS(),
LockSystem: webdav.NewMemLS(), LockSystem: webdav.NewMemLS(),
Logger: func(r *http.Request, err error) { Logger: func(r *http.Request, err error) {
log.Printf("WEBDAV: %#s, ERROR: %v", r, err) log.Printf("INFO: %s %s %s\n", r.RemoteAddr, r.Method, r.URL)
if err != nil {
log.Printf("ERR: %v", err)
}
}, },
} }
//http.Handle("/", srv)
http.HandleFunc(pathPrefix+"/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc(pathPrefix+"/", func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth() username, password, ok := r.BasicAuth()
@ -167,6 +171,7 @@ func (s *GarageFS) Mkdir(ctx context.Context, name string, perm os.FileMode) err
} }
func (s *GarageFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { func (s *GarageFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
NewGarageStatFromFile(ctx, name)
return NewGarageFile(ctx, name) return NewGarageFile(ctx, name)
} }
@ -179,6 +184,7 @@ func (s *GarageFS) Rename(ctx context.Context, oldName, newName string) error {
} }
func (s *GarageFS) Stat(ctx context.Context, name string) (os.FileInfo, error) { func (s *GarageFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
log.Println("Stat from GarageFS")
return NewGarageStat(ctx, name) return NewGarageStat(ctx, name)
} }
@ -186,36 +192,53 @@ type GarageFile struct {
ctx context.Context ctx context.Context
mc *minio.Client mc *minio.Client
obj *minio.Object obj *minio.Object
stat *GarageStat objw *io.PipeWriter
path string donew chan error
pos int64
path S3Path
} }
func NewGarageFile(ctx context.Context, path string) (webdav.File, error) { func NewGarageFile(ctx context.Context, path string) (webdav.File, error) {
gf := new(GarageFile) gf := new(GarageFile)
gf.ctx = ctx gf.ctx = ctx
gf.pos = 0
gf.mc = ctx.Value(garageEntry).(garageCtx).MC gf.mc = ctx.Value(garageEntry).(garageCtx).MC
gf.path = path gf.path = NewS3Path(path)
stat, err := NewGarageStat(ctx, path)
if err != nil {
return nil, err
}
gf.stat = stat
return gf, nil return gf, nil
} }
func (gf *GarageFile) Close() error { func (gf *GarageFile) Close() error {
if gf.obj == nil { err := make([]error, 0)
return nil
} if gf.obj != nil {
err := gf.obj.Close() err = append(err, gf.obj.Close())
gf.obj = nil gf.obj = nil
return err }
if gf.objw != nil {
// wait that minio completes its transfers in background
err = append(err, gf.objw.Close())
err = append(err, <-gf.donew)
gf.donew = nil
gf.objw = nil
}
count := 0
for _, e := range err {
if e != nil {
count++
log.Println(e)
}
}
if count > 0 {
return errors.New(fmt.Sprintf("%d errors when closing this WebDAV File. Read previous logs to know more.", count))
}
return nil
} }
func (gf *GarageFile) loadObject() error { func (gf *GarageFile) loadObject() error {
if gf.obj == nil { if gf.obj == nil {
log.Println("Called GetObject on", gf.path) obj, err := gf.mc.GetObject(gf.ctx, gf.path.bucket, gf.path.key, minio.GetObjectOptions{})
obj, err := gf.mc.GetObject(gf.ctx, gf.stat.bucket, gf.stat.obj.Key, minio.GetObjectOptions{})
if err != nil { if err != nil {
return err return err
} }
@ -225,9 +248,9 @@ func (gf *GarageFile) loadObject() error {
} }
func (gf *GarageFile) Read(p []byte) (n int, err error) { func (gf *GarageFile) Read(p []byte) (n int, err error) {
if gf.stat.Mode().IsDir() { //if gf.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 err := gf.loadObject(); err != nil { if err := gf.loadObject(); err != nil {
return 0, err return 0, err
} }
@ -236,14 +259,37 @@ func (gf *GarageFile) Read(p []byte) (n int, err error) {
} }
func (gf *GarageFile) Write(p []byte) (n int, err error) { func (gf *GarageFile) Write(p []byte) (n int, err error) {
return 0, errors.New("not implemented Write") /*if gf.path.class != OBJECT {
return 0, os.ErrInvalid
}*/
if gf.objw == nil {
if gf.pos != 0 {
return 0, errors.New("writing with an offset is not implemented")
}
r, w := io.Pipe()
gf.donew = make(chan error, 1)
gf.objw = w
contentType := mime.TypeByExtension(path.Ext(gf.path.key))
go func() {
_, err := gf.mc.PutObject(context.Background(), gf.path.bucket, gf.path.key, r, -1, minio.PutObjectOptions{ContentType: contentType})
gf.donew <- err
}()
}
return gf.objw.Write(p)
} }
func (gf *GarageFile) Seek(offset int64, whence int) (int64, error) { func (gf *GarageFile) Seek(offset int64, whence int) (int64, error) {
if err := gf.loadObject(); err != nil { if err := gf.loadObject(); err != nil {
return 0, err return 0, err
} }
return gf.obj.Seek(offset, whence)
pos, err := gf.obj.Seek(offset, whence)
gf.pos += pos
return pos, err
} }
/* /*
@ -254,13 +300,14 @@ If n > 0, ReadDir returns at most n DirEntry records. In this case, if ReadDir r
If n <= 0, ReadDir returns all the DirEntry records remaining in the directory. When it succeeds, it returns a nil error (not io.EOF). If n <= 0, ReadDir returns all the DirEntry records remaining in the directory. When it succeeds, it returns a nil error (not io.EOF).
*/ */
func (gf *GarageFile) Readdir(count int) ([]fs.FileInfo, error) { func (gf *GarageFile) Readdir(count int) ([]fs.FileInfo, error) {
log.Println("Call Readdir with count", count) if count > 0 {
return nil, errors.New("returning a limited number of directory entry is not supported in readdir")
}
if gf.path == "/" { if gf.path.class == ROOT {
return gf.readDirRoot(count) return gf.readDirRoot(count)
} else { } else {
exploded_path := strings.SplitN(gf.path, "/", 3) return gf.readDirChild(count)
return gf.readDirChild(count, exploded_path[1], exploded_path[2])
} }
} }
@ -282,10 +329,9 @@ func (gf *GarageFile) readDirRoot(count int) ([]fs.FileInfo, error) {
return entries, nil return entries, nil
} }
func (gf *GarageFile) readDirChild(count int, bucket, prefix string) ([]fs.FileInfo, error) { func (gf *GarageFile) readDirChild(count int) ([]fs.FileInfo, error) {
log.Println("call ListObjects with", bucket, prefix) objs_info := gf.mc.ListObjects(gf.ctx, gf.path.bucket, minio.ListObjectsOptions{
objs_info := gf.mc.ListObjects(gf.ctx, bucket, minio.ListObjectsOptions{ Prefix: gf.path.key,
Prefix: prefix,
Recursive: false, Recursive: false,
}) })
@ -294,7 +340,7 @@ func (gf *GarageFile) readDirChild(count int, bucket, prefix string) ([]fs.FileI
if object.Err != nil { if object.Err != nil {
return nil, object.Err return nil, object.Err
} }
ngf, err := NewGarageStatFromObjectInfo(gf.ctx, bucket, object) ngf, err := NewGarageStatFromObjectInfo(gf.ctx, gf.path.bucket, object)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -305,24 +351,40 @@ func (gf *GarageFile) readDirChild(count int, bucket, prefix string) ([]fs.FileI
} }
func (gf *GarageFile) Stat() (fs.FileInfo, error) { func (gf *GarageFile) Stat() (fs.FileInfo, error) {
return NewGarageStat(gf.ctx, gf.path) return NewGarageStatFromFile(gf.ctx, gf.path.path)
} }
/* Implements */ /* Implements */
// StatObject??? // StatObject???
type GarageStat struct { type GarageStat struct {
obj minio.ObjectInfo obj minio.ObjectInfo
bucket string ctx context.Context
path S3Path
} }
func NewGarageStat(ctx context.Context, path string) (*GarageStat, error) { /*
* Stat a path
*/
func NewGarageStatFromFile(ctx context.Context, path string) (*GarageStat, error) {
cache := ctx.Value(garageEntry).(garageCtx).StatCache cache := ctx.Value(garageEntry).(garageCtx).StatCache
// Maybe this file is already in our cache?
if entry, ok := cache[path]; ok { if entry, ok := cache[path]; ok {
return entry, nil return entry, nil
} }
gs, err := newGarageStatFresh(ctx, path) // Create a placeholder in case we are creating the object
if err != nil { gs := new(GarageStat)
gs.ctx = ctx
gs.path = NewS3Path(path)
gs.path.class = OBJECT // known because called from GarageFile
gs.obj.Key = gs.path.key
gs.obj.LastModified = time.Now()
// Maybe this file exists in garage?
err := gs.Refresh()
if err != nil && !os.IsNotExist(err) {
// There is an error and this is not a 404, report it.
return nil, err return nil, err
} }
@ -330,56 +392,92 @@ func NewGarageStat(ctx context.Context, path string) (*GarageStat, error) {
return gs, nil return gs, nil
} }
func newGarageStatFresh(ctx context.Context, path string) (*GarageStat, error) { /*
mc := ctx.Value(garageEntry).(garageCtx).MC * Stat a path knowing its ObjectInfo
gs := new(GarageStat) */
gs.bucket = "/"
gs.obj = minio.ObjectInfo{}
exploded_path := strings.SplitN(path, "/", 3)
// Check if we can extract the bucket name
if len(exploded_path) < 2 {
return gs, nil
}
gs.bucket = exploded_path[1]
// Check if we can extract the prefix
if len(exploded_path) < 3 || exploded_path[2] == "" {
return gs, nil
}
gs.obj.Key = exploded_path[2]
// Check if this is a file or a folder
log.Println("call StatObject with", gs.bucket, gs.obj.Key)
obj, err := mc.StatObject(ctx, gs.bucket, gs.obj.Key, minio.StatObjectOptions{})
if e, ok := err.(minio.ErrorResponse); ok && e.Code == "NoSuchKey" {
return gs, nil
}
if err != nil {
return nil, err
}
// If it is a file, assign its data
gs.obj = obj
return gs, nil
}
func NewGarageStatFromObjectInfo(ctx context.Context, bucket string, obj minio.ObjectInfo) (*GarageStat, error) { func NewGarageStatFromObjectInfo(ctx context.Context, bucket string, obj minio.ObjectInfo) (*GarageStat, error) {
gs := new(GarageStat) gs := new(GarageStat)
gs.bucket = bucket gs.path = NewTrustedS3Path(bucket, obj)
gs.obj = obj gs.obj = obj
cache := ctx.Value(garageEntry).(garageCtx).StatCache cache := ctx.Value(garageEntry).(garageCtx).StatCache
cache[path.Join("/", bucket, obj.Key)] = gs cache[gs.path.path] = gs
return gs, nil return gs, nil
} }
/*
* Stat a path without additional information
*/
func NewGarageStat(ctx context.Context, path string) (*GarageStat, error) {
log.Println("Probe file", path)
cache := ctx.Value(garageEntry).(garageCtx).StatCache
if entry, ok := cache[path]; ok {
return entry, nil
}
gs := new(GarageStat)
gs.ctx = ctx
gs.path = NewS3Path(path)
if err := gs.Refresh(); err != nil {
return nil, err
}
if gs.path.class&OPAQUE_KEY != 0 {
return nil, errors.New("Failed to precisely determine the key type, this a logic error.")
}
cache[path] = gs
cache[gs.path.path] = gs
return gs, nil
}
func (gs *GarageStat) Refresh() error {
if gs.path.class == ROOT || gs.path.class == BUCKET {
return nil
}
mc := gs.ctx.Value(garageEntry).(garageCtx).MC
// Compute the prefix to have the desired behaviour for our stat logic
prefix := gs.path.key
if prefix[len(prefix)-1:] == "/" {
prefix = prefix[:len(prefix)-1]
}
// Get info and check if the key exists
objs_info := mc.ListObjects(gs.ctx, gs.path.bucket, minio.ListObjectsOptions{
Prefix: prefix,
Recursive: false,
})
found := false
for object := range objs_info {
if object.Err != nil {
return object.Err
}
if object.Key == prefix || object.Key == prefix+"/" {
gs.obj = object
gs.path = NewTrustedS3Path(gs.path.bucket, object)
found = true
break
}
}
if !found {
return fs.ErrNotExist
}
return nil
}
func (gs *GarageStat) Name() string { func (gs *GarageStat) Name() string {
if gs.obj.Key != "" { if gs.path.class == ROOT {
return path.Base(gs.obj.Key) return "/"
} else if gs.path.class == BUCKET {
return gs.path.bucket
} else { } else {
return gs.bucket return path.Base(gs.path.key)
} }
} }
@ -388,10 +486,10 @@ func (gs *GarageStat) Size() int64 {
} }
func (gs *GarageStat) Mode() fs.FileMode { func (gs *GarageStat) Mode() fs.FileMode {
if gs.obj.ETag == "" { if gs.path.class == OBJECT {
return fs.ModeDir | fs.ModePerm
} else {
return fs.ModePerm return fs.ModePerm
} else {
return fs.ModeDir | fs.ModePerm
} }
} }
@ -400,9 +498,58 @@ func (gs *GarageStat) ModTime() time.Time {
} }
func (gs *GarageStat) IsDir() bool { func (gs *GarageStat) IsDir() bool {
return gs.Mode().IsDir() return gs.path.class != OBJECT
} }
func (gs *GarageStat) Sys() interface{} { func (gs *GarageStat) Sys() interface{} {
return nil return nil
} }
type S3Class int
const (
ROOT S3Class = 1 << iota
BUCKET
COMMON_PREFIX
OBJECT
OPAQUE_KEY
KEY = COMMON_PREFIX | OBJECT | OPAQUE_KEY
)
type S3Path struct {
path string
class S3Class
bucket string
key string
}
func NewS3Path(path string) S3Path {
exploded_path := strings.SplitN(path, "/", 3)
// If there is no bucket name (eg. "/")
if len(exploded_path) < 2 || exploded_path[1] == "" {
return S3Path{path, ROOT, "", ""}
}
// If there is no key
if len(exploded_path) < 3 || exploded_path[2] == "" {
return S3Path{path, BUCKET, exploded_path[1], ""}
}
return S3Path{path, OPAQUE_KEY, exploded_path[1], exploded_path[2]}
}
func NewTrustedS3Path(bucket string, obj minio.ObjectInfo) S3Path {
cl := OBJECT
if obj.Key[len(obj.Key)-1:] == "/" {
cl = COMMON_PREFIX
}
return S3Path{
path: path.Join("/", bucket, obj.Key),
bucket: bucket,
key: obj.Key,
class: cl,
}
}