diff --git a/internal/cmds/root.go b/internal/cmds/root.go index 53b6b31a..cc47de15 100644 --- a/internal/cmds/root.go +++ b/internal/cmds/root.go @@ -14,7 +14,7 @@ import ( ) var debugMode bool = false -var backend phylumfs.PhylumFs +var backend phylumfs.Backend func Setup() { viper.SetEnvPrefix("phylum") @@ -51,9 +51,6 @@ func Setup() { if err != nil { log.Fatal(fmt.Sprintf("Unable to parse db connection String: %v\n", err)) } - if debugMode { - config.Tracer = phylumTracer{} - } conn, err = pgx.ConnectConfig(context.Background(), config) if err != nil { log.Fatal(fmt.Sprintf("Unable to connect to database: %v\n", err)) @@ -71,14 +68,3 @@ func Setup() { rootCmd.AddCommand([]*cobra.Command{setupServeCommand(), setupAdminCommand()}...) rootCmd.Execute() } - -type phylumTracer struct { -} - -func (p phylumTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context { - log.Trace(fmt.Sprintf("%+v\n", data)) - return ctx -} - -func (p phylumTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) { -} diff --git a/internal/cmds/serve.go b/internal/cmds/serve.go index 89bd7cdf..cbe55f8a 100644 --- a/internal/cmds/serve.go +++ b/internal/cmds/serve.go @@ -1,16 +1,13 @@ package cmds import ( - "context" "fmt" - "strings" "time" - webdav "github.com/emersion/go-webdav" "github.com/fvbock/endless" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/shroff/phylum/server/internal/pgfs" + "github.com/shroff/phylum/server/internal/webdav" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -54,41 +51,18 @@ func setupServeCommand() *cobra.Command { } func setupWebdav(r *gin.RouterGroup) { - log.Info(fmt.Sprintf("Setting up WebDAV access at %s", r.BasePath())) - - webdavHandler := webdav.Handler{ - FileSystem: pgfs.New(backend, r.BasePath()), - } - handler := func(c *gin.Context) { - path := c.Params.ByName("path") - path = strings.TrimLeft(path, "/") - if path == "" { - // No path specified - c.Writer.WriteHeader(404) - return - } - index := strings.Index(path, "/") - if index != -1 { - path = path[0:index] - } - _, err := backend.FindRoot(context.Background(), path) - if err != nil { - c.Writer.WriteHeader(404) - return - } - webdavHandler.ServeHTTP(c.Writer, c.Request) - } - r.Handle("OPTIONS", "/*path", handler) - r.Handle("GET", "/*path", handler) - r.Handle("PUT", "/*path", handler) - r.Handle("HEAD", "/*path", handler) - r.Handle("POST", "/*path", handler) - r.Handle("DELETE", "/*path", handler) - r.Handle("MOVE", "/*path", handler) - r.Handle("COPY", "/*path", handler) - r.Handle("MKCOL", "/*path", handler) - r.Handle("PROPFIND", "/*path", handler) - r.Handle("PROPPATCH", "/*path", handler) + handler := webdav.NewHandler(backend, r.BasePath()) + r.Handle("OPTIONS", "/*path", handler.HandleRequest) + r.Handle("GET", "/*path", handler.HandleRequest) + r.Handle("PUT", "/*path", handler.HandleRequest) + r.Handle("HEAD", "/*path", handler.HandleRequest) + r.Handle("POST", "/*path", handler.HandleRequest) + r.Handle("DELETE", "/*path", handler.HandleRequest) + r.Handle("MOVE", "/*path", handler.HandleRequest) + r.Handle("COPY", "/*path", handler.HandleRequest) + r.Handle("MKCOL", "/*path", handler.HandleRequest) + r.Handle("PROPFIND", "/*path", handler.HandleRequest) + r.Handle("PROPPATCH", "/*path", handler.HandleRequest) } func createEngine(corsEnabled bool, corsOrigins []string) *gin.Engine { diff --git a/internal/phylumfs/backend.go b/internal/phylumfs/backend.go new file mode 100644 index 00000000..5d51cd3b --- /dev/null +++ b/internal/phylumfs/backend.go @@ -0,0 +1,56 @@ +package phylumfs + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/shroff/phylum/server/internal/sql" +) + +type Backend interface { + FindRoot(ctx context.Context, name string) (Fs, error) + Mkroot(ctx context.Context, id uuid.UUID, name string) error + Rmroot(ctx context.Context, name string) error +} + +type pgxBackend struct { + queries sql.Queries + cs contentStore +} + +func New(conn *pgx.Conn) Backend { + queries := *sql.New(conn) + cs, err := newLocalContentStore("srv") + if err != nil { + panic(err) + } + return pgxBackend{queries: queries, cs: cs} +} + +func (b pgxBackend) FindRoot(ctx context.Context, name string) (Fs, error) { + // TODO: Permissions checks + root, err := b.queries.FindRoot(ctx, name) + if err != nil { + return nil, err + } + + return pgxFs{queries: b.queries, root: root.ID, cs: b.cs}, nil +} + +func (b pgxBackend) Mkroot(ctx context.Context, id uuid.UUID, name string) error { + if _, err := b.queries.FindRoot(ctx, name); err == nil { + return fmt.Errorf("root directory already exists: %s", name) + } else { + return b.queries.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: nil, Name: name, Dir: true}) + } +} + +func (b pgxBackend) Rmroot(ctx context.Context, name string) error { + if fs, err := b.FindRoot(ctx, name); err == nil { + return fs.DeleteRecursive(ctx, fs.rootId()) + } else { + return fmt.Errorf("root directory does not exist: %s", name) + } +} diff --git a/internal/phylumfs/contentstore.go b/internal/phylumfs/content_store.go similarity index 88% rename from internal/phylumfs/contentstore.go rename to internal/phylumfs/content_store.go index d4bc406c..517e1a68 100644 --- a/internal/phylumfs/contentstore.go +++ b/internal/phylumfs/content_store.go @@ -7,7 +7,7 @@ import ( "github.com/google/uuid" ) -type ContentStore interface { +type contentStore interface { Open(id uuid.UUID) (io.ReadCloser, error) Create(id uuid.UUID, callback func(hash.Hash, int, error)) (io.WriteCloser, error) Delete(id uuid.UUID) error diff --git a/internal/phylumfs/fs.go b/internal/phylumfs/fs.go new file mode 100644 index 00000000..b55f3714 --- /dev/null +++ b/internal/phylumfs/fs.go @@ -0,0 +1,88 @@ +package phylumfs + +import ( + "context" + "encoding/base64" + "hash" + "io" + iofs "io/fs" + "strings" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/shroff/phylum/server/internal/sql" + "github.com/sirupsen/logrus" +) + +type Fs interface { + rootId() uuid.UUID + Open(id uuid.UUID) (io.ReadCloser, error) + ReadDir(ctx context.Context, id uuid.UUID, recursive bool) ([]sql.ReadDirRow, error) + DeleteRecursive(ctx context.Context, id uuid.UUID) error + CreateResource(ctx context.Context, id uuid.UUID, parent uuid.UUID, name string, dir bool) error + UpdateContents(ctx context.Context, id uuid.UUID) (io.WriteCloser, error) + ResourceByPath(ctx context.Context, path string) (*sql.ResourceByPathRow, error) +} + +type pgxFs struct { + queries sql.Queries + root uuid.UUID + cs contentStore +} + +func (fs pgxFs) rootId() uuid.UUID { + return fs.root +} + +func (fs pgxFs) Open(id uuid.UUID) (io.ReadCloser, error) { + return fs.cs.Open(id) +} + +func (fs pgxFs) ReadDir(ctx context.Context, id uuid.UUID, recursive bool) ([]sql.ReadDirRow, error) { + return fs.queries.ReadDir(ctx, sql.ReadDirParams{ID: id, Recursive: recursive}) +} + +func (fs pgxFs) DeleteRecursive(ctx context.Context, id uuid.UUID) error { + _, err := fs.queries.DeleteRecursive(ctx, id) + // TODO: Delete Contents + return err +} + +func (fs pgxFs) CreateResource(ctx context.Context, id uuid.UUID, parent uuid.UUID, name string, dir bool) error { + return fs.queries.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: &parent, Name: name, Dir: dir}) +} + +func (fs pgxFs) UpdateContents(ctx context.Context, id uuid.UUID) (io.WriteCloser, error) { + return fs.cs.Create(id, func(h hash.Hash, len int, err error) { + if err == nil { + etag := base64.StdEncoding.EncodeToString(h.Sum(nil)) + err = fs.queries.UpdateResourceContents(ctx, sql.UpdateResourceContentsParams{ + ID: id, + Size: pgtype.Int4{Int32: int32(len), Valid: true}, + Etag: pgtype.Text{String: string(etag), Valid: true}, + }) + } + if err != nil { + logrus.Warn("Unable to update contents: " + err.Error()) + fs.cs.Delete(id) + } + }) +} + +func (fs pgxFs) ResourceByPath(ctx context.Context, path string) (*sql.ResourceByPathRow, error) { + logrus.Trace("PhylumFs.ResourceByPath called with " + path) + segments := strings.Split(strings.Trim(path, "/"), "/") + if len(segments) == 0 { + return nil, iofs.ErrInvalid + } + + res, err := fs.queries.ResourceByPath(ctx, sql.ResourceByPathParams{Search: segments, Root: fs.root}) + if err != nil { + return nil, iofs.ErrNotExist + } + logrus.Trace(res) + + //TODO: Permissions checks + + return &res, nil +} diff --git a/internal/phylumfs/localcontentstore.go b/internal/phylumfs/local_content_store.go similarity index 72% rename from internal/phylumfs/localcontentstore.go rename to internal/phylumfs/local_content_store.go index 63f1962b..10a5eee8 100644 --- a/internal/phylumfs/localcontentstore.go +++ b/internal/phylumfs/local_content_store.go @@ -10,7 +10,7 @@ import ( "github.com/google/uuid" ) -type LocalContentStore string +type localContentStore string type contentWriter struct { file *os.File @@ -37,19 +37,19 @@ func (c contentWriter) Close() error { return err } -func NewLocalContentStore(root string) (ContentStore, error) { +func newLocalContentStore(root string) (contentStore, error) { err := os.MkdirAll(root, 0750) if err != nil { return nil, err } - return LocalContentStore(root), nil + return localContentStore(root), nil } -func (l LocalContentStore) Open(id uuid.UUID) (io.ReadCloser, error) { +func (l localContentStore) Open(id uuid.UUID) (io.ReadCloser, error) { return os.Open(l.path(id)) } -func (l LocalContentStore) Create(id uuid.UUID, callback func(hash.Hash, int, error)) (io.WriteCloser, error) { +func (l localContentStore) Create(id uuid.UUID, callback func(hash.Hash, int, error)) (io.WriteCloser, error) { file, err := os.Create(l.path(id)) if err != nil { return nil, err @@ -59,10 +59,10 @@ func (l LocalContentStore) Create(id uuid.UUID, callback func(hash.Hash, int, er return contentWriter{file, md5.New(), &addr, callback}, nil } -func (l LocalContentStore) Delete(id uuid.UUID) error { +func (l localContentStore) Delete(id uuid.UUID) error { return os.Remove(filepath.Join(string(l), id.String())) } -func (l LocalContentStore) path(id uuid.UUID) string { +func (l localContentStore) path(id uuid.UUID) string { return filepath.Join(string(l), id.String()) } diff --git a/internal/phylumfs/phylumfs.go b/internal/phylumfs/phylumfs.go deleted file mode 100644 index 37cb714c..00000000 --- a/internal/phylumfs/phylumfs.go +++ /dev/null @@ -1,108 +0,0 @@ -package phylumfs - -import ( - "context" - "encoding/base64" - "fmt" - "hash" - "io" - "io/fs" - "strings" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/shroff/phylum/server/internal/sql" - "github.com/sirupsen/logrus" -) - -type PhylumFs struct { - queries sql.Queries - cs ContentStore -} - -func New(conn *pgx.Conn) PhylumFs { - queries := *sql.New(conn) - cs, err := NewLocalContentStore("srv") - if err != nil { - panic(err) - } - return PhylumFs{queries: queries, cs: cs} -} - -func (p PhylumFs) FindRoot(ctx context.Context, name string) (sql.Resource, error) { - // TODO: Permissions checks - return p.queries.FindRoot(ctx, name) -} - -func (p PhylumFs) Mkroot(ctx context.Context, id uuid.UUID, name string) error { - if _, err := p.queries.FindRoot(ctx, name); err == nil { - return fmt.Errorf("root directory already exists: %s", name) - } else { - return p.queries.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: nil, Name: name, Dir: true}) - } -} - -func (p PhylumFs) Rmroot(ctx context.Context, name string) error { - if root, err := p.queries.FindRoot(ctx, name); err == nil { - return p.DeleteRecursive(ctx, root.ID) - } else { - return fmt.Errorf("root directory does not exist: %s", name) - } -} - -func (p PhylumFs) Open(id uuid.UUID) (io.ReadCloser, error) { - return p.cs.Open(id) -} - -func (p PhylumFs) ReadDir(ctx context.Context, id uuid.UUID, recursive bool) ([]sql.ReadDirRow, error) { - return p.queries.ReadDir(ctx, sql.ReadDirParams{ID: id, Recursive: recursive}) -} - -func (p PhylumFs) DeleteRecursive(ctx context.Context, id uuid.UUID) error { - _, err := p.queries.DeleteRecursive(ctx, id) - // TODO: Delete Contents - return err -} - -func (p PhylumFs) CreateResource(ctx context.Context, id uuid.UUID, parent uuid.UUID, name string, dir bool) error { - return p.queries.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: &parent, Name: name, Dir: dir}) -} - -func (p PhylumFs) UpdateContents(ctx context.Context, id uuid.UUID) (io.WriteCloser, error) { - return p.cs.Create(id, func(h hash.Hash, len int, err error) { - if err == nil { - etag := base64.StdEncoding.EncodeToString(h.Sum(nil)) - err = p.queries.UpdateResourceContents(ctx, sql.UpdateResourceContentsParams{ - ID: id, - Size: pgtype.Int4{Int32: int32(len), Valid: true}, - Etag: pgtype.Text{String: string(etag), Valid: true}, - }) - } - if err != nil { - logrus.Warn(fmt.Sprintf("Unable to update contents: %s", err)) - p.cs.Delete(id) - } - }) -} - -func (p PhylumFs) ResourceByPath(ctx context.Context, path string) (*sql.ResourceByPathRow, error) { - segments := strings.Split(strings.TrimRight(path, "/"), "/") - if len(segments) == 0 { - return nil, fs.ErrInvalid - } - - root, err := p.queries.FindRoot(ctx, segments[0]) - if err != nil { - return nil, fs.ErrNotExist - } - - res, err := p.queries.ResourceByPath(ctx, sql.ResourceByPathParams{Search: segments, Root: root.ID}) - if err != nil { - return nil, fs.ErrNotExist - } - - //TODO: Permissions checks - - return &res, nil -} diff --git a/internal/webdav/handler.go b/internal/webdav/handler.go new file mode 100644 index 00000000..108e39b1 --- /dev/null +++ b/internal/webdav/handler.go @@ -0,0 +1,51 @@ +package webdav + +import ( + "context" + "strings" + + webdav "github.com/emersion/go-webdav" + "github.com/gin-gonic/gin" + "github.com/shroff/phylum/server/internal/phylumfs" + "github.com/sirupsen/logrus" +) + +type handler struct { + backend phylumfs.Backend + prefix string +} + +func NewHandler(backend phylumfs.Backend, prefix string) *handler { + logrus.Info("Setting up WebDAV access at " + prefix) + return &handler{ + backend: backend, + prefix: prefix, + } +} + +func (h *handler) HandleRequest(c *gin.Context) { + path := c.Params.ByName("path") + path = strings.TrimLeft(path, "/") + if path == "" { + // No path specified + c.Writer.WriteHeader(404) + return + } + index := strings.Index(path, "/") + if index != -1 { + path = path[0:index] + } + root, err := h.backend.FindRoot(context.Background(), path) + if err != nil { + c.Writer.WriteHeader(404) + return + } + + webdavHandler := webdav.Handler{ + FileSystem: adapter{ + fs: root, + prefix: h.prefix, + }, + } + webdavHandler.ServeHTTP(c.Writer, c.Request) +} diff --git a/internal/pgfs/pgfs.go b/internal/webdav/webdav_adapter.go similarity index 57% rename from internal/pgfs/pgfs.go rename to internal/webdav/webdav_adapter.go index ecd23ea6..d019e0f2 100644 --- a/internal/pgfs/pgfs.go +++ b/internal/webdav/webdav_adapter.go @@ -1,9 +1,9 @@ -package pgfs +package webdav import ( "context" "io" - "io/fs" + iofs "io/fs" "strings" webdav "github.com/emersion/go-webdav" @@ -12,24 +12,20 @@ import ( "github.com/shroff/phylum/server/internal/sql" ) -type Pgfs struct { - p phylumfs.PhylumFs +type adapter struct { + fs phylumfs.Fs prefix string } -func New(p phylumfs.PhylumFs, prefix string) Pgfs { - return Pgfs{p: p, prefix: prefix} -} - -func (p Pgfs) Open(ctx context.Context, name string) (io.ReadCloser, error) { +func (p adapter) Open(ctx context.Context, name string) (io.ReadCloser, error) { resource, err := p.resourceByPath(ctx, name) if err != nil { return nil, err } - return p.p.Open(resource.ID) + return p.fs.Open(resource.ID) } -func (p Pgfs) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) { +func (p adapter) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) { resource, err := p.resourceByPath(ctx, name) if err != nil { return nil, err @@ -44,15 +40,15 @@ func (p Pgfs) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) { } return val, nil } -func (p Pgfs) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) { +func (p adapter) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) { dir, err := p.resourceByPath(ctx, name) if err != nil { return nil, err } if !dir.Dir { - return nil, fs.ErrInvalid + return nil, iofs.ErrInvalid } - children, err := p.p.ReadDir(ctx, dir.ID, recursive) + children, err := p.fs.ReadDir(ctx, dir.ID, recursive) if err != nil { return nil, err } @@ -71,7 +67,7 @@ func (p Pgfs) ReadDir(ctx context.Context, name string, recursive bool) ([]webda } return result, nil } -func (p Pgfs) Create(ctx context.Context, name string) (io.WriteCloser, error) { +func (p adapter) Create(ctx context.Context, name string) (io.WriteCloser, error) { var id uuid.UUID if resource, err := p.resourceByPath(ctx, name); err == nil { id = resource.ID @@ -81,52 +77,52 @@ func (p Pgfs) Create(ctx context.Context, name string) (io.WriteCloser, error) { parentPath := name[0:index] parent, err := p.resourceByPath(ctx, parentPath) if err != nil { - return nil, fs.ErrNotExist + return nil, iofs.ErrNotExist } fileName := name[index+1:] id = uuid.New() - if err = p.p.CreateResource(ctx, id, parent.ID, fileName, false); err != nil { + if err = p.fs.CreateResource(ctx, id, parent.ID, fileName, false); err != nil { return nil, err } } - return p.p.UpdateContents(ctx, id) + return p.fs.UpdateContents(ctx, id) } -func (p Pgfs) RemoveAll(ctx context.Context, name string) error { +func (p adapter) RemoveAll(ctx context.Context, name string) error { resource, _ := p.resourceByPath(ctx, name) if resource == nil { - return fs.ErrNotExist + return iofs.ErrNotExist } - err := p.p.DeleteRecursive(ctx, resource.ID) + err := p.fs.DeleteRecursive(ctx, resource.ID) return err } -func (p Pgfs) Mkdir(ctx context.Context, name string) error { +func (p adapter) Mkdir(ctx context.Context, name string) error { resource, _ := p.resourceByPath(ctx, name) if resource != nil { - return fs.ErrExist + return iofs.ErrExist } name = strings.TrimRight(name, "/") index := strings.LastIndex(name, "/") parentPath := name[0:index] parent, err := p.resourceByPath(ctx, parentPath) if err != nil { - return fs.ErrNotExist + return iofs.ErrNotExist } dirName := name[index+1:] - err = p.p.CreateResource(ctx, uuid.New(), parent.ID, dirName, true) + err = p.fs.CreateResource(ctx, uuid.New(), parent.ID, dirName, true) return err } -func (p Pgfs) Copy(ctx context.Context, name, dest string, options *webdav.CopyOptions) (created bool, err error) { +func (p adapter) Copy(ctx context.Context, name, dest string, options *webdav.CopyOptions) (created bool, err error) { // TODO: Implement return false, nil } -func (p Pgfs) Move(ctx context.Context, name, dest string, options *webdav.MoveOptions) (created bool, err error) { +func (p adapter) Move(ctx context.Context, name, dest string, options *webdav.MoveOptions) (created bool, err error) { // TODO: Implement return false, nil } -func (p Pgfs) resourceByPath(ctx context.Context, name string) (res *sql.ResourceByPathRow, err error) { - return p.p.ResourceByPath(ctx, strings.TrimPrefix(name, p.prefix+"/")) +func (p adapter) resourceByPath(ctx context.Context, name string) (res *sql.ResourceByPathRow, err error) { + return p.fs.ResourceByPath(ctx, strings.TrimPrefix(name, p.prefix+"/")) }