|
|
@ -1,567 +1,35 @@ |
|
|
|
package main |
|
|
|
|
|
|
|
import ( |
|
|
|
"context" |
|
|
|
"errors" |
|
|
|
"fmt" |
|
|
|
"io" |
|
|
|
"io/fs" |
|
|
|
"log" |
|
|
|
"mime" |
|
|
|
"net/http" |
|
|
|
"os" |
|
|
|
"path" |
|
|
|
"strings" |
|
|
|
"time" |
|
|
|
|
|
|
|
"golang.org/x/net/webdav" |
|
|
|
|
|
|
|
"github.com/go-ldap/ldap/v3" |
|
|
|
|
|
|
|
"github.com/minio/minio-go/v7" |
|
|
|
"github.com/minio/minio-go/v7/pkg/credentials" |
|
|
|
) |
|
|
|
|
|
|
|
type bagageCtxKey string |
|
|
|
|
|
|
|
const garageEntry = bagageCtxKey("garage") |
|
|
|
|
|
|
|
type garageCtx struct { |
|
|
|
MC *minio.Client |
|
|
|
StatCache map[string]*GarageStat |
|
|
|
} |
|
|
|
|
|
|
|
func EnvOrDefault(key, def string) string { |
|
|
|
if val, ok := os.LookupEnv(key); ok { |
|
|
|
return val |
|
|
|
} |
|
|
|
return def |
|
|
|
} |
|
|
|
|
|
|
|
func main() { |
|
|
|
log.Println("=== Starting Bagage ===") |
|
|
|
HttpListen := EnvOrDefault("BAGAGE_HTTP_LISTEN", ":8080") |
|
|
|
pathPrefix := EnvOrDefault("BAGAGE_WEBDAV_PREFIX", "/webdav") |
|
|
|
LdapServer := EnvOrDefault("BAGAGE_LDAP_ENDPOINT", "127.0.0.1:1389") |
|
|
|
UserBaseDN := EnvOrDefault("BAGAGE_LDAP_USER_BASE_DN", "ou=users,dc=deuxfleurs,dc=fr") |
|
|
|
UserNameAttr := EnvOrDefault("BAGAGE_LDAP_USERNAME_ATTR", "cn") |
|
|
|
Endpoint := EnvOrDefault("BAGAGE_S3_ENDPOINT", "garage.deuxfleurs.fr") |
|
|
|
UseSSL := EnvOrDefault("BAGAGE_S3_SSL", "true") == "true" |
|
|
|
|
|
|
|
srv := &webdav.Handler{ |
|
|
|
Prefix: pathPrefix, |
|
|
|
FileSystem: NewGarageFS(), |
|
|
|
LockSystem: webdav.NewMemLS(), |
|
|
|
Logger: func(r *http.Request, err error) { |
|
|
|
log.Printf("INFO: %s %s %s\n", r.RemoteAddr, r.Method, r.URL) |
|
|
|
if err != nil { |
|
|
|
log.Printf("ERR: %v", err) |
|
|
|
} |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
http.HandleFunc(pathPrefix+"/", func(w http.ResponseWriter, r *http.Request) { |
|
|
|
username, password, ok := r.BasicAuth() |
|
|
|
|
|
|
|
if !ok { |
|
|
|
NotAuthorized(w, r) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
ldapSock, err := ldap.Dial("tcp", LdapServer) |
|
|
|
if err != nil { |
|
|
|
log.Println(err) |
|
|
|
InternalError(w, r) |
|
|
|
return |
|
|
|
} |
|
|
|
defer ldapSock.Close() |
|
|
|
|
|
|
|
// Check credential
|
|
|
|
userDn := fmt.Sprintf("%s=%s,%s", UserNameAttr, username, UserBaseDN) |
|
|
|
err = ldapSock.Bind(userDn, password) |
|
|
|
if err != nil { |
|
|
|
log.Println(err) |
|
|
|
NotAuthorized(w, r) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
// Get S3 creds garage_s3_access_key garage_s3_secret_key
|
|
|
|
searchRequest := ldap.NewSearchRequest( |
|
|
|
userDn, |
|
|
|
ldap.ScopeBaseObject, |
|
|
|
ldap.NeverDerefAliases, |
|
|
|
0, |
|
|
|
0, |
|
|
|
false, |
|
|
|
"(objectClass=*)", |
|
|
|
[]string{"garage_s3_access_key", "garage_s3_secret_key"}, |
|
|
|
nil) |
|
|
|
|
|
|
|
sr, err := ldapSock.Search(searchRequest) |
|
|
|
if err != nil { |
|
|
|
log.Println(err) |
|
|
|
InternalError(w, r) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if len(sr.Entries) != 1 { |
|
|
|
log.Println("Wrong number of LDAP entries, expected 1, got", len(sr.Entries)) |
|
|
|
InternalError(w, r) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
access_key := sr.Entries[0].GetAttributeValue("garage_s3_access_key") |
|
|
|
secret_key := sr.Entries[0].GetAttributeValue("garage_s3_secret_key") |
|
|
|
|
|
|
|
if access_key == "" || secret_key == "" { |
|
|
|
log.Println("Either access key or secret key is missing in LDAP for ", userDn) |
|
|
|
InternalError(w, r) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
mc, err := minio.New(Endpoint, &minio.Options{ |
|
|
|
Creds: credentials.NewStaticV4(access_key, secret_key, ""), |
|
|
|
Secure: UseSSL, |
|
|
|
config := (&Config{}).LoadWithDefault().LoadWithEnv() |
|
|
|
|
|
|
|
log.Println(config) |
|
|
|
|
|
|
|
// Assemble components to handle WebDAV requests
|
|
|
|
http.Handle(config.DavPath+"/", |
|
|
|
BasicAuthExtract{ |
|
|
|
OnNotFound: NotAuthorized{}, |
|
|
|
OnCreds: LdapPreAuth{ |
|
|
|
WithConfig: config, |
|
|
|
OnWrongPassword: NotAuthorized{}, |
|
|
|
OnFailure: InternalError{}, |
|
|
|
OnCreds: S3Auth{ |
|
|
|
WithConfig: config, |
|
|
|
OnFailure: InternalError{}, |
|
|
|
OnMinioClient: WebDav{ |
|
|
|
WithConfig: config, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}) |
|
|
|
if err != nil { |
|
|
|
log.Println(err) |
|
|
|
InternalError(w, r) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
nctx := context.WithValue(r.Context(), garageEntry, garageCtx{MC: mc, StatCache: make(map[string]*GarageStat)}) |
|
|
|
srv.ServeHTTP(w, r.WithContext(nctx)) |
|
|
|
return |
|
|
|
}) |
|
|
|
|
|
|
|
if err := http.ListenAndServe(HttpListen, nil); err != nil { |
|
|
|
if err := http.ListenAndServe(config.HttpListen, nil); err != nil { |
|
|
|
log.Fatalf("Error with WebDAV server: %v", err) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func NotAuthorized(w http.ResponseWriter, r *http.Request) { |
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="Pour accéder à Bagage, veuillez entrer vos identifiants Deuxfleurs"`) |
|
|
|
w.WriteHeader(401) |
|
|
|
w.Write([]byte("401 Unauthorized\n")) |
|
|
|
} |
|
|
|
|
|
|
|
func InternalError(w http.ResponseWriter, r *http.Request) { |
|
|
|
w.WriteHeader(500) |
|
|
|
w.Write([]byte("500 Internal Server Error\n")) |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
/////// Select Action
|
|
|
|
If no slash or one trailing slash |
|
|
|
return ListBuckets |
|
|
|
Else |
|
|
|
obj := ListObjects |
|
|
|
If obj.Length == 1 |
|
|
|
return GetObject |
|
|
|
Else |
|
|
|
return obj |
|
|
|
*/ |
|
|
|
type GarageFS struct{} |
|
|
|
|
|
|
|
func NewGarageFS() *GarageFS { |
|
|
|
grg := new(GarageFS) |
|
|
|
return grg |
|
|
|
} |
|
|
|
|
|
|
|
func (s *GarageFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error { |
|
|
|
return errors.New("Not implemented Mkdir") |
|
|
|
} |
|
|
|
|
|
|
|
func (s *GarageFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { |
|
|
|
//log.Println("Stat from GarageFS.OpenFile()", name)
|
|
|
|
NewGarageStatFromFile(ctx, name) |
|
|
|
return NewGarageFile(ctx, name) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *GarageFS) RemoveAll(ctx context.Context, name string) error { |
|
|
|
return errors.New("Not implemented RemoveAll") |
|
|
|
} |
|
|
|
|
|
|
|
func (s *GarageFS) Rename(ctx context.Context, oldName, newName string) error { |
|
|
|
return errors.New("Not implemented Rename") |
|
|
|
} |
|
|
|
|
|
|
|
func (s *GarageFS) Stat(ctx context.Context, name string) (os.FileInfo, error) { |
|
|
|
//log.Println("Stat from GarageFS.Stat()", name)
|
|
|
|
return NewGarageStat(ctx, name) |
|
|
|
} |
|
|
|
|
|
|
|
type GarageFile struct { |
|
|
|
ctx context.Context |
|
|
|
mc *minio.Client |
|
|
|
obj *minio.Object |
|
|
|
objw *io.PipeWriter |
|
|
|
donew chan error |
|
|
|
pos int64 |
|
|
|
path S3Path |
|
|
|
} |
|
|
|
|
|
|
|
func NewGarageFile(ctx context.Context, path string) (webdav.File, error) { |
|
|
|
gf := new(GarageFile) |
|
|
|
gf.ctx = ctx |
|
|
|
gf.pos = 0 |
|
|
|
gf.mc = ctx.Value(garageEntry).(garageCtx).MC |
|
|
|
gf.path = NewS3Path(path) |
|
|
|
return gf, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (gf *GarageFile) Close() error { |
|
|
|
err := make([]error, 0) |
|
|
|
|
|
|
|
if gf.obj != nil { |
|
|
|
err = append(err, gf.obj.Close()) |
|
|
|
gf.obj = nil |
|
|
|
} |
|
|
|
|
|
|
|
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 { |
|
|
|
if gf.obj == nil { |
|
|
|
obj, err := gf.mc.GetObject(gf.ctx, gf.path.bucket, gf.path.key, minio.GetObjectOptions{}) |
|
|
|
if err != nil { |
|
|
|
return err |
|
|
|
} |
|
|
|
gf.obj = obj |
|
|
|
} |
|
|
|
return nil |
|
|
|
} |
|
|
|
|
|
|
|
func (gf *GarageFile) Read(p []byte) (n int, err error) { |
|
|
|
//if gf.Stat() & OBJECT == 0 { /* @FIXME Ideally we would check against OBJECT but we need a non OPAQUE_KEY */
|
|
|
|
// return 0, os.ErrInvalid
|
|
|
|
//}
|
|
|
|
if err := gf.loadObject(); err != nil { |
|
|
|
return 0, err |
|
|
|
} |
|
|
|
|
|
|
|
return gf.obj.Read(p) |
|
|
|
} |
|
|
|
|
|
|
|
func (gf *GarageFile) Write(p []byte) (n int, err error) { |
|
|
|
/*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) { |
|
|
|
if err := gf.loadObject(); err != nil { |
|
|
|
return 0, err |
|
|
|
} |
|
|
|
|
|
|
|
pos, err := gf.obj.Seek(offset, whence) |
|
|
|
gf.pos += pos |
|
|
|
return pos, err |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
ReadDir reads the contents of the directory associated with the file f and returns a slice of DirEntry values in directory order. Subsequent calls on the same file will yield later DirEntry records in the directory. |
|
|
|
|
|
|
|
If n > 0, ReadDir returns at most n DirEntry records. In this case, if ReadDir returns an empty slice, it will return an error explaining why. At the end of a directory, the error is 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) { |
|
|
|
if count > 0 { |
|
|
|
return nil, errors.New("returning a limited number of directory entry is not supported in readdir") |
|
|
|
} |
|
|
|
|
|
|
|
if gf.path.class == ROOT { |
|
|
|
return gf.readDirRoot(count) |
|
|
|
} else { |
|
|
|
return gf.readDirChild(count) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (gf *GarageFile) readDirRoot(count int) ([]fs.FileInfo, error) { |
|
|
|
buckets, err := gf.mc.ListBuckets(gf.ctx) |
|
|
|
if err != nil { |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
|
|
|
|
entries := make([]fs.FileInfo, 0, len(buckets)) |
|
|
|
for _, bucket := range buckets { |
|
|
|
//log.Println("Stat from GarageFile.readDirRoot()", "/"+bucket.Name)
|
|
|
|
ngf, err := NewGarageStat(gf.ctx, "/"+bucket.Name) |
|
|
|
if err != nil { |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
entries = append(entries, ngf) |
|
|
|
} |
|
|
|
|
|
|
|
return entries, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (gf *GarageFile) readDirChild(count int) ([]fs.FileInfo, error) { |
|
|
|
prefix := gf.path.key |
|
|
|
if len(prefix) > 0 && prefix[len(prefix)-1:] != "/" { |
|
|
|
prefix = prefix + "/" |
|
|
|
} |
|
|
|
|
|
|
|
objs_info := gf.mc.ListObjects(gf.ctx, gf.path.bucket, minio.ListObjectsOptions{ |
|
|
|
Prefix: prefix, |
|
|
|
Recursive: false, |
|
|
|
}) |
|
|
|
|
|
|
|
entries := make([]fs.FileInfo, 0) |
|
|
|
for object := range objs_info { |
|
|
|
if object.Err != nil { |
|
|
|
return nil, object.Err |
|
|
|
} |
|
|
|
//log.Println("Stat from GarageFile.readDirChild()", path.Join("/", gf.path.bucket, object.Key))
|
|
|
|
ngf, err := NewGarageStatFromObjectInfo(gf.ctx, gf.path.bucket, object) |
|
|
|
if err != nil { |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
entries = append(entries, ngf) |
|
|
|
} |
|
|
|
|
|
|
|
return entries, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (gf *GarageFile) Stat() (fs.FileInfo, error) { |
|
|
|
//log.Println("Stat from GarageFile.Stat()", gf.path.path)
|
|
|
|
return NewGarageStatFromFile(gf.ctx, gf.path.path) |
|
|
|
} |
|
|
|
|
|
|
|
/* Implements */ |
|
|
|
// StatObject???
|
|
|
|
type GarageStat struct { |
|
|
|
obj minio.ObjectInfo |
|
|
|
ctx context.Context |
|
|
|
path S3Path |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* Stat a path |
|
|
|
*/ |
|
|
|
func NewGarageStatFromFile(ctx context.Context, path string) (*GarageStat, error) { |
|
|
|
cache := ctx.Value(garageEntry).(garageCtx).StatCache |
|
|
|
|
|
|
|
// Maybe this file is already in our cache?
|
|
|
|
if entry, ok := cache[path]; ok { |
|
|
|
return entry, nil |
|
|
|
} |
|
|
|
|
|
|
|
// Create a placeholder in case we are creating the object
|
|
|
|
gs := new(GarageStat) |
|
|
|
gs.ctx = ctx |
|
|
|
gs.path = NewS3Path(path) |
|
|
|
if gs.path.class == OPAQUE_KEY { |
|
|
|
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 |
|
|
|
} |
|
|
|
|
|
|
|
cache[path] = gs |
|
|
|
return gs, nil |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* Stat a path knowing its ObjectInfo |
|
|
|
*/ |
|
|
|
func NewGarageStatFromObjectInfo(ctx context.Context, bucket string, obj minio.ObjectInfo) (*GarageStat, error) { |
|
|
|
gs := new(GarageStat) |
|
|
|
gs.path = NewTrustedS3Path(bucket, obj) |
|
|
|
gs.obj = obj |
|
|
|
|
|
|
|
cache := ctx.Value(garageEntry).(garageCtx).StatCache |
|
|
|
cache[gs.path.path] = gs |
|
|
|
return gs, nil |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* Stat a path without additional information |
|
|
|
*/ |
|
|
|
func NewGarageStat(ctx context.Context, path string) (*GarageStat, error) { |
|
|
|
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 { |
|
|
|
if gs.path.class == ROOT { |
|
|
|
return "/" |
|
|
|
} else if gs.path.class == BUCKET { |
|
|
|
return gs.path.bucket |
|
|
|
} else { |
|
|
|
return path.Base(gs.path.key) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (gs *GarageStat) Size() int64 { |
|
|
|
return gs.obj.Size |
|
|
|
} |
|
|
|
|
|
|
|
func (gs *GarageStat) Mode() fs.FileMode { |
|
|
|
if gs.path.class == OBJECT { |
|
|
|
return fs.ModePerm |
|
|
|
} else { |
|
|
|
return fs.ModeDir | fs.ModePerm |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (gs *GarageStat) ModTime() time.Time { |
|
|
|
return gs.obj.LastModified |
|
|
|
} |
|
|
|
|
|
|
|
func (gs *GarageStat) IsDir() bool { |
|
|
|
return gs.path.class != OBJECT |
|
|
|
} |
|
|
|
|
|
|
|
func (gs *GarageStat) Sys() interface{} { |
|
|
|
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, |
|
|
|
} |
|
|
|
} |
|
|
|