From 47f14775d7d9a47e0d123aa174fb1f1f75bce547 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 2 May 2019 14:22:10 -0700 Subject: [PATCH] scaffolding, but seems hard to satisfy mac, windows, and different os versions https://www.reddit.com/r/golang/comments/3wrxo3/webdav_server_that_provide_filesystem_using/ --- weed/command/command.go | 1 + weed/command/webdav.go | 91 +++++++++ weed/server/webdav_server.go | 351 +++++++++++++++++++++++++++++++++++ 3 files changed, 443 insertions(+) create mode 100644 weed/command/webdav.go create mode 100644 weed/server/webdav_server.go diff --git a/weed/command/command.go b/weed/command/command.go index 39a01cc05..79c00d4cd 100644 --- a/weed/command/command.go +++ b/weed/command/command.go @@ -26,6 +26,7 @@ var Commands = []*Command{ cmdVolume, cmdExport, cmdMount, + cmdWebDav, } type Command struct { diff --git a/weed/command/webdav.go b/weed/command/webdav.go new file mode 100644 index 000000000..71933d885 --- /dev/null +++ b/weed/command/webdav.go @@ -0,0 +1,91 @@ +package command + +import ( + "fmt" + "net/http" + "time" + + "github.com/chrislusf/seaweedfs/weed/glog" + "github.com/chrislusf/seaweedfs/weed/security" + "github.com/chrislusf/seaweedfs/weed/server" + "github.com/chrislusf/seaweedfs/weed/util" + "github.com/spf13/viper" +) + +var ( + webDavStandaloneOptions WebDavOption +) + +type WebDavOption struct { + filer *string + port *int + tlsPrivateKey *string + tlsCertificate *string +} + +func init() { + cmdWebDav.Run = runWebDav // break init cycle + webDavStandaloneOptions.filer = cmdWebDav.Flag.String("filer", "localhost:8888", "filer server address") + webDavStandaloneOptions.port = cmdWebDav.Flag.Int("port", 7333, "webdav server http listen port") + webDavStandaloneOptions.tlsPrivateKey = cmdWebDav.Flag.String("key.file", "", "path to the TLS private key file") + webDavStandaloneOptions.tlsCertificate = cmdWebDav.Flag.String("cert.file", "", "path to the TLS certificate file") +} + +var cmdWebDav = &Command{ + UsageLine: "webdav -port=7333 -filer=", + Short: "start a webdav server that is backed by a filer", + Long: `start a webdav server that is backed by a filer. + +`, +} + +func runWebDav(cmd *Command, args []string) bool { + + weed_server.LoadConfiguration("security", false) + + glog.V(0).Infof("Starting Seaweed WebDav Server %s at https port %d", util.VERSION, *webDavStandaloneOptions.port) + + return webDavStandaloneOptions.startWebDav() + +} + +func (wo *WebDavOption) startWebDav() bool { + + filerGrpcAddress, err := parseFilerGrpcAddress(*wo.filer) + if err != nil { + glog.Fatal(err) + return false + } + + ws, webdavServer_err := weed_server.NewWebDavServer(&weed_server.WebDavOption{ + Filer: *wo.filer, + FilerGrpcAddress: filerGrpcAddress, + GrpcDialOption: security.LoadClientTLS(viper.Sub("grpc"), "client"), + }) + if webdavServer_err != nil { + glog.Fatalf("WebDav Server startup error: %v", webdavServer_err) + } + + httpS := &http.Server{Handler: ws.Handler} + + listenAddress := fmt.Sprintf(":%d", *wo.port) + webDavListener, err := util.NewListener(listenAddress, time.Duration(10)*time.Second) + if err != nil { + glog.Fatalf("WebDav Server listener on %s error: %v", listenAddress, err) + } + + if *wo.tlsPrivateKey != "" { + glog.V(0).Infof("Start Seaweed WebDav Server %s at https port %d", util.VERSION, *wo.port) + if err = httpS.ServeTLS(webDavListener, *wo.tlsCertificate, *wo.tlsPrivateKey); err != nil { + glog.Fatalf("WebDav Server Fail to serve: %v", err) + } + } else { + glog.V(0).Infof("Start Seaweed WebDav Server %s at http port %d", util.VERSION, *wo.port) + if err = httpS.Serve(webDavListener); err != nil { + glog.Fatalf("WebDav Server Fail to serve: %v", err) + } + } + + return true + +} diff --git a/weed/server/webdav_server.go b/weed/server/webdav_server.go new file mode 100644 index 000000000..b06c055e2 --- /dev/null +++ b/weed/server/webdav_server.go @@ -0,0 +1,351 @@ +package weed_server + +import ( + "context" + "io" + "net/http" + "net/url" + "os" + "path" + "strings" + "time" + + "golang.org/x/net/webdav" + "google.golang.org/grpc" + + "github.com/chrislusf/seaweedfs/weed/filer2" + "github.com/chrislusf/seaweedfs/weed/glog" + "github.com/chrislusf/seaweedfs/weed/security" + "github.com/spf13/viper" +) + +type WebDavOption struct { + Filer string + FilerGrpcAddress string + DomainName string + BucketsPath string + GrpcDialOption grpc.DialOption +} + +type WebDavServer struct { + option *WebDavOption + secret security.SigningKey + filer *filer2.Filer + grpcDialOption grpc.DialOption + Handler *webdav.Handler +} + +func NewWebDavServer(option *WebDavOption) (ws *WebDavServer, err error) { + + fs, _ := NewWebDavFileSystem() + + ws = &WebDavServer{ + option: option, + grpcDialOption: security.LoadClientTLS(viper.Sub("grpc"), "filer"), + Handler: &webdav.Handler{ + FileSystem: fs, + LockSystem: webdav.NewMemLS(), + Logger: func(r *http.Request, err error) { + litmus := r.Header.Get("X-Litmus") + if len(litmus) > 19 { + litmus = litmus[:16] + "..." + } + + switch r.Method { + case "COPY", "MOVE": + dst := "" + if u, err := url.Parse(r.Header.Get("Destination")); err == nil { + dst = u.Path + } + glog.Infof("%-18s %s %s %v", + r.Method, + r.URL.Path, + dst, + err) + default: + glog.Infof("%-18s %s %v", + r.Method, + r.URL.Path, + err) + } + }, + }, + } + + return ws, nil +} + +// adapted from https://github.com/mattn/davfs/blob/master/plugin/mysql/mysql.go + +type WebDavFileSystem struct { +} + +type FileInfo struct { + name string + size int64 + mode os.FileMode + mod_time time.Time +} + +func (fi *FileInfo) Name() string { return fi.name } +func (fi *FileInfo) Size() int64 { return fi.size } +func (fi *FileInfo) Mode() os.FileMode { return fi.mode } +func (fi *FileInfo) ModTime() time.Time { return fi.mod_time } +func (fi *FileInfo) IsDir() bool { return fi.mode.IsDir() } +func (fi *FileInfo) Sys() interface{} { return nil } + +type WebDavFile struct { + fs *WebDavFileSystem + name string + isDirectory bool + off int64 +} + +func NewWebDavFileSystem() (webdav.FileSystem, error) { + return &WebDavFileSystem{}, nil +} + +func clearName(name string) (string, error) { + slashed := strings.HasSuffix(name, "/") + name = path.Clean(name) + if !strings.HasSuffix(name, "/") && slashed { + name += "/" + } + if !strings.HasPrefix(name, "/") { + return "", os.ErrInvalid + } + return name, nil +} + +func (fs *WebDavFileSystem) Mkdir(ctx context.Context, name string, perm os.FileMode) error { + + glog.V(2).Infof("WebDavFileSystem.Mkdir %v", name) + + if !strings.HasSuffix(name, "/") { + name += "/" + } + + var err error + if name, err = clearName(name); err != nil { + return err + } + + _, err = fs.stat(name) + if err == nil { + return os.ErrExist + } + + base := "/" + for _, elem := range strings.Split(strings.Trim(name, "/"), "/") { + base += elem + "/" + _, err = fs.stat(base) + if err != os.ErrNotExist { + return err + } + // _, err = fs.db.Exec(`insert into filesystem(name, content, mode, mod_time) values(?, '', ?, now())`, base, perm.Perm()|os.ModeDir) + if err != nil { + return err + } + } + return nil +} + +func (fs *WebDavFileSystem) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { + + glog.V(2).Infof("WebDavFileSystem.OpenFile %v", name) + + var err error + if name, err = clearName(name); err != nil { + return nil, err + } + + if flag&os.O_CREATE != 0 { + // file should not have / suffix. + if strings.HasSuffix(name, "/") { + return nil, os.ErrInvalid + } + // based directory should be exists. + dir, _ := path.Split(name) + _, err := fs.stat(dir) + if err != nil { + return nil, os.ErrInvalid + } + _, err = fs.stat(name) + if err == nil { + if flag&os.O_EXCL != 0 { + return nil, os.ErrExist + } + fs.removeAll(name) + } + // _, err = fs.db.Exec(`insert into filesystem(name, content, mode, mod_time) values(?, '', ?, now())`, name, perm.Perm()) + if err != nil { + return nil, err + } + return &WebDavFile{fs, name, false, 0}, nil + } + + fi, err := fs.stat(name) + if err != nil { + return nil, os.ErrNotExist + } + if !strings.HasSuffix(name, "/") && fi.IsDir() { + name += "/" + } + + return &WebDavFile{fs, name, true, 0}, nil + +} + +func (fs *WebDavFileSystem) removeAll(name string) error { + var err error + if name, err = clearName(name); err != nil { + return err + } + + fi, err := fs.stat(name) + if err != nil { + return err + } + + if fi.IsDir() { + //_, err = fs.db.Exec(`delete from filesystem where name like $1 escape '\'`, strings.Replace(name, `%`, `\%`, -1)+`%`) + } else { + //_, err = fs.db.Exec(`delete from filesystem where name = ?`, name) + } + return err +} + +func (fs *WebDavFileSystem) RemoveAll(ctx context.Context, name string) error { + + glog.V(2).Infof("WebDavFileSystem.RemoveAll %v", name) + + return fs.removeAll(name) +} + +func (fs *WebDavFileSystem) Rename(ctx context.Context, oldName, newName string) error { + + glog.V(2).Infof("WebDavFileSystem.Rename %v to %v", oldName, newName) + + var err error + if oldName, err = clearName(oldName); err != nil { + return err + } + if newName, err = clearName(newName); err != nil { + return err + } + + of, err := fs.stat(oldName) + if err != nil { + return os.ErrExist + } + if of.IsDir() && !strings.HasSuffix(oldName, "/") { + oldName += "/" + newName += "/" + } + + _, err = fs.stat(newName) + if err == nil { + return os.ErrExist + } + + //_, err = fs.db.Exec(`update filesystem set name = ? where name = ?`, newName, oldName) + return err +} + +func (fs *WebDavFileSystem) stat(name string) (os.FileInfo, error) { + var err error + if name, err = clearName(name); err != nil { + return nil, err + } + + //rows, err := fs.db.Query(`select name, format(length(content)/2, 0), mode, mod_time from filesystem where name = ?`, name) + if err != nil { + return nil, err + } + var fi FileInfo + // err = rows.Scan(&fi.name, &fi.size, &fi.mode, &fi.mod_time) + if err != nil { + return nil, err + } + _, fi.name = path.Split(path.Clean(fi.name)) + if fi.name == "" { + fi.name = "/" + fi.mod_time = time.Now() + } + return &fi, nil +} + +func (fs *WebDavFileSystem) Stat(ctx context.Context, name string) (os.FileInfo, error) { + + glog.V(2).Infof("WebDavFileSystem.Stat %v", name) + + return fs.stat(name) +} + +func (f *WebDavFile) Write(p []byte) (int, error) { + + glog.V(2).Infof("WebDavFileSystem.Write %v", f.name) + + var err error + // _, err := f.fs.db.Exec(`update filesystem set content = substr(content, 1, ?) || ? where name = ?`, f.off*2, hex.EncodeToString(p), f.name) + if err != nil { + return 0, err + } + //f.off += int64(len(p)) + return len(p), err +} + +func (f *WebDavFile) Close() error { + + glog.V(2).Infof("WebDavFileSystem.Close %v", f.name) + + return nil +} + +func (f *WebDavFile) Read(p []byte) (int, error) { + + glog.V(2).Infof("WebDavFileSystem.Read %v", f.name) + + var err error + //rows, err := f.fs.db.Query(`select mode, substr(content, ?, ?) from filesystem where name = ?`, 1+f.off*2, len(p)*2, f.name) + if err != nil { + return 0, err + } + //defer rows.Close() + + return 0, io.EOF +} + +func (f *WebDavFile) Readdir(count int) ([]os.FileInfo, error) { + + glog.V(2).Infof("WebDavFileSystem.Readdir %v", f.name) + + // return f.children[old:f.off], nil + return nil, nil +} + +func (f *WebDavFile) Seek(offset int64, whence int) (int64, error) { + + glog.V(2).Infof("WebDavFile.Seek %v %v %v", f.name, offset, whence) + + var err error + switch whence { + case 0: + f.off = 0 + case 2: + if fi, err := f.fs.stat(f.name); err != nil { + return 0, err + } else { + f.off = fi.Size() + } + } + f.off += offset + return f.off, err +} + +func (f *WebDavFile) Stat() (os.FileInfo, error) { + + glog.V(2).Infof("WebDavFile.Stat %v", f.name) + + return f.fs.stat(f.name) +}