forked from Deuxfleurs/bagage
Format the source file
This commit is contained in:
parent
d69640894d
commit
8f7e8d2c73
1 changed files with 198 additions and 197 deletions
395
main.go
395
main.go
|
@ -1,42 +1,43 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"io/fs"
|
|
||||||
"time"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/webdav"
|
"golang.org/x/net/webdav"
|
||||||
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
"github.com/go-ldap/ldap/v3"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
type bagageCtxKey string
|
type bagageCtxKey string
|
||||||
|
|
||||||
const garageEntry = bagageCtxKey("garage")
|
const garageEntry = bagageCtxKey("garage")
|
||||||
|
|
||||||
type garageCtx struct {
|
type garageCtx struct {
|
||||||
MC *minio.Client
|
MC *minio.Client
|
||||||
StatCache map[string]*GarageStat
|
StatCache map[string]*GarageStat
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
pathPrefix := "/webdav"
|
pathPrefix := "/webdav"
|
||||||
UserBaseDN := "ou=users,dc=deuxfleurs,dc=fr"
|
UserBaseDN := "ou=users,dc=deuxfleurs,dc=fr"
|
||||||
UserNameAttr := "cn"
|
UserNameAttr := "cn"
|
||||||
Endpoint := "garage.deuxfleurs.fr"
|
Endpoint := "garage.deuxfleurs.fr"
|
||||||
UseSSL := true
|
UseSSL := true
|
||||||
|
|
||||||
srv := &webdav.Handler{
|
srv := &webdav.Handler{
|
||||||
Prefix: pathPrefix,
|
Prefix: pathPrefix,
|
||||||
FileSystem: NewGarageFS(),
|
FileSystem: NewGarageFS(),
|
||||||
LockSystem: webdav.NewMemLS(),
|
LockSystem: webdav.NewMemLS(),
|
||||||
Logger: func(r *http.Request, err error) {
|
Logger: func(r *http.Request, err error) {
|
||||||
|
@ -45,79 +46,79 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
//http.Handle("/", srv)
|
//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()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
NotAuthorized(w,r)
|
NotAuthorized(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ldapSock, err := ldap.Dial("tcp", "127.0.0.1:1389")
|
ldapSock, err := ldap.Dial("tcp", "127.0.0.1:1389")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
InternalError(w,r)
|
InternalError(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer ldapSock.Close()
|
defer ldapSock.Close()
|
||||||
|
|
||||||
// Check credential
|
// Check credential
|
||||||
userDn := fmt.Sprintf("%s=%s,%s", UserNameAttr, username, UserBaseDN)
|
userDn := fmt.Sprintf("%s=%s,%s", UserNameAttr, username, UserBaseDN)
|
||||||
err = ldapSock.Bind(userDn, password)
|
err = ldapSock.Bind(userDn, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
NotAuthorized(w,r)
|
NotAuthorized(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get S3 creds garage_s3_access_key garage_s3_secret_key
|
// Get S3 creds garage_s3_access_key garage_s3_secret_key
|
||||||
searchRequest := ldap.NewSearchRequest(
|
searchRequest := ldap.NewSearchRequest(
|
||||||
userDn,
|
userDn,
|
||||||
ldap.ScopeBaseObject,
|
ldap.ScopeBaseObject,
|
||||||
ldap.NeverDerefAliases,
|
ldap.NeverDerefAliases,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
"(objectClass=*)",
|
"(objectClass=*)",
|
||||||
[]string{"garage_s3_access_key", "garage_s3_secret_key"},
|
[]string{"garage_s3_access_key", "garage_s3_secret_key"},
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
sr, err := ldapSock.Search(searchRequest)
|
sr, err := ldapSock.Search(searchRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
InternalError(w,r)
|
InternalError(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(sr.Entries) != 1 {
|
if len(sr.Entries) != 1 {
|
||||||
log.Println("Wrong number of LDAP entries, expected 1, got", len(sr.Entries))
|
log.Println("Wrong number of LDAP entries, expected 1, got", len(sr.Entries))
|
||||||
InternalError(w,r)
|
InternalError(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
access_key := sr.Entries[0].GetAttributeValue("garage_s3_access_key")
|
access_key := sr.Entries[0].GetAttributeValue("garage_s3_access_key")
|
||||||
secret_key := sr.Entries[0].GetAttributeValue("garage_s3_secret_key")
|
secret_key := sr.Entries[0].GetAttributeValue("garage_s3_secret_key")
|
||||||
|
|
||||||
if access_key == "" || secret_key == "" {
|
if access_key == "" || secret_key == "" {
|
||||||
log.Println("Either access key or secret key is missing in LDAP for ", userDn)
|
log.Println("Either access key or secret key is missing in LDAP for ", userDn)
|
||||||
InternalError(w,r)
|
InternalError(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mc, err := minio.New(Endpoint, &minio.Options{
|
mc, err := minio.New(Endpoint, &minio.Options{
|
||||||
Creds: credentials.NewStaticV4(access_key, secret_key, ""),
|
Creds: credentials.NewStaticV4(access_key, secret_key, ""),
|
||||||
Secure: UseSSL,
|
Secure: UseSSL,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
InternalError(w,r)
|
InternalError(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nctx := context.WithValue(r.Context(), garageEntry, garageCtx{MC: mc, StatCache: make(map[string]*GarageStat)})
|
nctx := context.WithValue(r.Context(), garageEntry, garageCtx{MC: mc, StatCache: make(map[string]*GarageStat)})
|
||||||
srv.ServeHTTP(w, r.WithContext(nctx))
|
srv.ServeHTTP(w, r.WithContext(nctx))
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := http.ListenAndServe(":8080", nil); err != nil {
|
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||||
log.Fatalf("Error with WebDAV server: %v", err)
|
log.Fatalf("Error with WebDAV server: %v", err)
|
||||||
|
@ -132,7 +133,7 @@ func NotAuthorized(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func InternalError(w http.ResponseWriter, r *http.Request) {
|
func InternalError(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
w.Write([]byte("500 Internal Server Error\n"))
|
w.Write([]byte("500 Internal Server Error\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -146,7 +147,7 @@ func InternalError(w http.ResponseWriter, r *http.Request) {
|
||||||
Else
|
Else
|
||||||
return obj
|
return obj
|
||||||
*/
|
*/
|
||||||
type GarageFS struct {}
|
type GarageFS struct{}
|
||||||
|
|
||||||
func NewGarageFS() *GarageFS {
|
func NewGarageFS() *GarageFS {
|
||||||
grg := new(GarageFS)
|
grg := new(GarageFS)
|
||||||
|
@ -154,53 +155,53 @@ func NewGarageFS() *GarageFS {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GarageFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
func (s *GarageFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||||
return errors.New("Not implemented Mkdir")
|
return errors.New("Not implemented Mkdir")
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
return NewGarageFile(ctx, name)
|
return NewGarageFile(ctx, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GarageFS) RemoveAll(ctx context.Context, name string) error {
|
func (s *GarageFS) RemoveAll(ctx context.Context, name string) error {
|
||||||
return errors.New("Not implemented RemoveAll")
|
return errors.New("Not implemented RemoveAll")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GarageFS) Rename(ctx context.Context, oldName, newName string) error {
|
func (s *GarageFS) Rename(ctx context.Context, oldName, newName string) error {
|
||||||
return errors.New("Not implemented Rename")
|
return errors.New("Not implemented Rename")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GarageFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
func (s *GarageFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
|
||||||
return NewGarageStat(ctx, name)
|
return NewGarageStat(ctx, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GarageFile struct {
|
type GarageFile struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
mc *minio.Client
|
mc *minio.Client
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
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.mc = ctx.Value(garageEntry).(garageCtx).MC
|
gf.mc = ctx.Value(garageEntry).(garageCtx).MC
|
||||||
gf.path = path
|
gf.path = path
|
||||||
return gf, nil
|
return gf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gf *GarageFile) Close() error {
|
func (gf *GarageFile) Close() error {
|
||||||
return errors.New("not implemented Close")
|
return errors.New("not implemented Close")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gf *GarageFile) Read(p []byte) (n int, err error) {
|
func (gf *GarageFile) Read(p []byte) (n int, err error) {
|
||||||
return 0, errors.New("not implemented Read")
|
return 0, errors.New("not implemented Read")
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
return 0, errors.New("not implemented Write")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gf *GarageFile) Seek(offset int64, whence int) (int64, error) {
|
func (gf *GarageFile) Seek(offset int64, whence int) (int64, error) {
|
||||||
return 0, errors.New("not implemented Seek")
|
return 0, errors.New("not implemented Seek")
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -211,155 +212,155 @@ 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)
|
log.Println("Call Readdir with count", count)
|
||||||
|
|
||||||
if gf.path == "/" {
|
if gf.path == "/" {
|
||||||
return gf.readDirRoot(count)
|
return gf.readDirRoot(count)
|
||||||
} else {
|
} else {
|
||||||
exploded_path := strings.SplitN(gf.path, "/", 3)
|
exploded_path := strings.SplitN(gf.path, "/", 3)
|
||||||
return gf.readDirChild(count, exploded_path[1], exploded_path[2])
|
return gf.readDirChild(count, exploded_path[1], exploded_path[2])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gf *GarageFile) readDirRoot(count int) ([]fs.FileInfo, error) {
|
func (gf *GarageFile) readDirRoot(count int) ([]fs.FileInfo, error) {
|
||||||
buckets, err := gf.mc.ListBuckets(gf.ctx)
|
buckets, err := gf.mc.ListBuckets(gf.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
entries := make([]fs.FileInfo, 0, len(buckets))
|
entries := make([]fs.FileInfo, 0, len(buckets))
|
||||||
for _, bucket := range buckets {
|
for _, bucket := range buckets {
|
||||||
ngf, err := NewGarageStat(gf.ctx, "/"+bucket.Name)
|
ngf, err := NewGarageStat(gf.ctx, "/"+bucket.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entries = append(entries, ngf)
|
entries = append(entries, ngf)
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gf *GarageFile) readDirChild(count int, bucket, prefix string) ([]fs.FileInfo, error) {
|
func (gf *GarageFile) readDirChild(count int, bucket, prefix string) ([]fs.FileInfo, error) {
|
||||||
log.Println("call ListObjects with", bucket, prefix)
|
log.Println("call ListObjects with", bucket, prefix)
|
||||||
objs_info := gf.mc.ListObjects(gf.ctx, bucket, minio.ListObjectsOptions{
|
objs_info := gf.mc.ListObjects(gf.ctx, bucket, minio.ListObjectsOptions{
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
Recursive: false,
|
Recursive: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
entries := make([]fs.FileInfo,0)
|
entries := make([]fs.FileInfo, 0)
|
||||||
for object := range objs_info {
|
for object := range objs_info {
|
||||||
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, bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
entries = append(entries, ngf)
|
entries = append(entries, ngf)
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gf *GarageFile) Stat() (fs.FileInfo, error) {
|
func (gf *GarageFile) Stat() (fs.FileInfo, error) {
|
||||||
return NewGarageStat(gf.ctx, gf.path)
|
return NewGarageStat(gf.ctx, gf.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Implements */
|
/* Implements */
|
||||||
// StatObject???
|
// StatObject???
|
||||||
type GarageStat struct {
|
type GarageStat struct {
|
||||||
obj minio.ObjectInfo
|
obj minio.ObjectInfo
|
||||||
bucket string
|
bucket string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGarageStat(ctx context.Context, path string) (*GarageStat, error) {
|
func NewGarageStat(ctx context.Context, path string) (*GarageStat, error) {
|
||||||
cache := ctx.Value(garageEntry).(garageCtx).StatCache
|
cache := ctx.Value(garageEntry).(garageCtx).StatCache
|
||||||
if entry, ok := cache[path]; ok {
|
if entry, ok := cache[path]; ok {
|
||||||
return entry, nil
|
return entry, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gs, err := newGarageStatFresh(ctx, path)
|
gs, err := newGarageStatFresh(ctx, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cache[path] = gs
|
cache[path] = gs
|
||||||
return gs, nil
|
return gs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGarageStatFresh(ctx context.Context, path string) (*GarageStat, error) {
|
func newGarageStatFresh(ctx context.Context, path string) (*GarageStat, error) {
|
||||||
mc := ctx.Value(garageEntry).(garageCtx).MC
|
mc := ctx.Value(garageEntry).(garageCtx).MC
|
||||||
gs := new(GarageStat)
|
gs := new(GarageStat)
|
||||||
gs.bucket = "/"
|
gs.bucket = "/"
|
||||||
gs.obj = minio.ObjectInfo{}
|
gs.obj = minio.ObjectInfo{}
|
||||||
|
|
||||||
exploded_path := strings.SplitN(path, "/", 3)
|
exploded_path := strings.SplitN(path, "/", 3)
|
||||||
|
|
||||||
// Check if we can extract the bucket name
|
// Check if we can extract the bucket name
|
||||||
if len(exploded_path) < 2 {
|
if len(exploded_path) < 2 {
|
||||||
return gs, nil
|
return gs, nil
|
||||||
}
|
}
|
||||||
gs.bucket = exploded_path[1]
|
gs.bucket = exploded_path[1]
|
||||||
|
|
||||||
// Check if we can extract the prefix
|
// Check if we can extract the prefix
|
||||||
if len(exploded_path) < 3 || exploded_path[2] == "" {
|
if len(exploded_path) < 3 || exploded_path[2] == "" {
|
||||||
return gs, nil
|
return gs, nil
|
||||||
}
|
}
|
||||||
gs.obj.Key = exploded_path[2]
|
gs.obj.Key = exploded_path[2]
|
||||||
|
|
||||||
// Check if this is a file or a folder
|
// Check if this is a file or a folder
|
||||||
log.Println("call StatObject with", gs.bucket, gs.obj.Key)
|
log.Println("call StatObject with", gs.bucket, gs.obj.Key)
|
||||||
obj, err := mc.StatObject(ctx, gs.bucket, gs.obj.Key, minio.StatObjectOptions{})
|
obj, err := mc.StatObject(ctx, gs.bucket, gs.obj.Key, minio.StatObjectOptions{})
|
||||||
if e, ok := err.(minio.ErrorResponse); ok && e.Code == "NoSuchKey" {
|
if e, ok := err.(minio.ErrorResponse); ok && e.Code == "NoSuchKey" {
|
||||||
return gs, nil
|
return gs, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it is a file, assign its data
|
// If it is a file, assign its data
|
||||||
gs.obj = obj
|
gs.obj = obj
|
||||||
return gs, nil
|
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.bucket = bucket
|
||||||
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[path.Join("/", bucket, obj.Key)] = gs
|
||||||
return gs, nil
|
return gs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GarageStat) Name() string {
|
func (gs *GarageStat) Name() string {
|
||||||
if gs.obj.Key != "" {
|
if gs.obj.Key != "" {
|
||||||
return path.Base(gs.obj.Key)
|
return path.Base(gs.obj.Key)
|
||||||
} else {
|
} else {
|
||||||
return gs.bucket
|
return gs.bucket
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GarageStat) Size() int64 {
|
func (gs *GarageStat) Size() int64 {
|
||||||
return gs.obj.Size
|
return gs.obj.Size
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GarageStat) Mode() fs.FileMode {
|
func (gs *GarageStat) Mode() fs.FileMode {
|
||||||
if gs.obj.ETag == "" {
|
if gs.obj.ETag == "" {
|
||||||
return fs.ModeDir | fs.ModePerm
|
return fs.ModeDir | fs.ModePerm
|
||||||
} else {
|
} else {
|
||||||
return fs.ModePerm
|
return fs.ModePerm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GarageStat) ModTime() time.Time {
|
func (gs *GarageStat) ModTime() time.Time {
|
||||||
return gs.obj.LastModified
|
return gs.obj.LastModified
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GarageStat) IsDir() bool {
|
func (gs *GarageStat) IsDir() bool {
|
||||||
return gs.Mode().IsDir()
|
return gs.Mode().IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GarageStat) Sys() interface{} {
|
func (gs *GarageStat) Sys() interface{} {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue