From dcbb581bc1bffff48a16fb43f2f042838bc6522c Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Fri, 25 Oct 2024 16:14:08 +0530 Subject: [PATCH] [server] Serve public resources --- server/internal/api/public/public.go | 31 ++++++++++ server/internal/api/serve/serve_resource.go | 12 ++-- server/internal/api/v1/fs/cat.go | 10 +++- server/internal/api/webdav/impl/webdav.go | 8 +-- server/internal/command/serve/cmd.go | 2 + server/internal/core/fs/public/filesystem.go | 42 ------------- server/internal/core/fs/public/public.go | 18 +++--- server/internal/core/fs/public/resource.go | 62 +++++++++++++++++--- 8 files changed, 113 insertions(+), 72 deletions(-) create mode 100644 server/internal/api/public/public.go diff --git a/server/internal/api/public/public.go b/server/internal/api/public/public.go new file mode 100644 index 00000000..1a03230c --- /dev/null +++ b/server/internal/api/public/public.go @@ -0,0 +1,31 @@ +package public + +import ( + "github.com/gin-gonic/gin" + "github.com/shroff/phylum/server/internal/api/auth" + "github.com/shroff/phylum/server/internal/api/serve" + "github.com/shroff/phylum/server/internal/core/fs/public" +) + +func requirePassword(c *gin.Context, id string) string { + if _, pass, ok := c.Request.BasicAuth(); ok { + return pass + } else { + c.Header("WWW-Authenticate", "Basic realm=\""+id+"\"") + panic(auth.ErrRequired) + } +} + +func Setup(r *gin.RouterGroup) { + // r.Handle("OPTIONS", "/:id/*path", handler.HandleRequest) + r.Handle("GET", "/:id/*path", func(c *gin.Context) { + id := c.Param("id") + path := c.Param("path") + + r, err := public.OpenResource(c.Request.Context(), id, requirePassword(c, id), path) + if err != nil { + panic(err) + } + serve.Serve(c.Writer, c.Request, r) + }) +} diff --git a/server/internal/api/serve/serve_resource.go b/server/internal/api/serve/serve_resource.go index ce35956a..7e38e1c0 100644 --- a/server/internal/api/serve/serve_resource.go +++ b/server/internal/api/serve/serve_resource.go @@ -32,15 +32,19 @@ type Resource interface { FSContentSize() int64 FSContentSHA256() string FSContentType() string - ReadDir(recursive bool) ([]Resource, error) OpenRead(start, length int64) (io.ReadCloser, error) } -type FileSystem interface { +func Serve(w http.ResponseWriter, r *http.Request, res Resource) { + if res.FSDir() { + serveCollection(w, r, res) + } else { + serveResource(w, r, res) + } } -func ServeCollection(w http.ResponseWriter, r *http.Request, file Resource) { +func serveCollection(w http.ResponseWriter, r *http.Request, file Resource) { if !strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, r.URL.String()+"/", http.StatusMovedPermanently) return @@ -84,7 +88,7 @@ func ServeCollection(w http.ResponseWriter, r *http.Request, file Resource) { fmt.Fprintf(w, "\n") } -func ServeResource(w http.ResponseWriter, r *http.Request, file Resource) { +func serveResource(w http.ResponseWriter, r *http.Request, file Resource) { w.Header().Set("Etag", file.FSContentSHA256()) w.Header().Set("Last-Modified", file.FSModified().Format(http.TimeFormat)) w.Header().Set("Content-Type", file.FSContentType()) diff --git a/server/internal/api/v1/fs/cat.go b/server/internal/api/v1/fs/cat.go index bbb0cb39..2cbb2508 100644 --- a/server/internal/api/v1/fs/cat.go +++ b/server/internal/api/v1/fs/cat.go @@ -5,6 +5,7 @@ import ( "github.com/google/uuid" "github.com/shroff/phylum/server/internal/api/auth" "github.com/shroff/phylum/server/internal/api/serve" + "github.com/shroff/phylum/server/internal/core/fs" ) func handleCatRequest(c *gin.Context) { @@ -13,10 +14,13 @@ func handleCatRequest(c *gin.Context) { panic(errResourceIDInvalid) } - fs := auth.GetFileSystem(c) - r, err := fs.ResourceByID(resourceID) + f := auth.GetFileSystem(c) + r, err := f.ResourceByID(resourceID) + if r.Dir { + err = fs.ErrResourceCollection + } if err != nil { panic(err) } - serve.ServeResource(c.Writer, c.Request, r) + serve.Serve(c.Writer, c.Request, r) } diff --git a/server/internal/api/webdav/impl/webdav.go b/server/internal/api/webdav/impl/webdav.go index 9b9f2175..68d437bf 100644 --- a/server/internal/api/webdav/impl/webdav.go +++ b/server/internal/api/webdav/impl/webdav.go @@ -212,15 +212,11 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta return status, err } // TODO: check locks for read-only access?? - file, err := h.FileSystem.ResourceByPath(reqPath) + res, err := h.FileSystem.ResourceByPath(reqPath) if err != nil { return http.StatusNotFound, err } - if file.Dir { - serve.ServeCollection(w, r, file) - } else { - serve.ServeResource(w, r, file) - } + serve.Serve(w, r, res) return 0, nil } diff --git a/server/internal/command/serve/cmd.go b/server/internal/command/serve/cmd.go index 76299982..94f191f3 100644 --- a/server/internal/command/serve/cmd.go +++ b/server/internal/command/serve/cmd.go @@ -2,6 +2,7 @@ package serve import ( "github.com/fvbock/endless" + "github.com/shroff/phylum/server/internal/api/public" apiv1 "github.com/shroff/phylum/server/internal/api/v1" "github.com/shroff/phylum/server/internal/api/webdav" "github.com/sirupsen/logrus" @@ -19,6 +20,7 @@ func SetupCommand() *cobra.Command { webdav.SetupHandler(engine.Group(config.GetString("webdav_prefix"))) apiv1.Setup(engine.Group("/api/v1")) + public.Setup(engine.Group("/public")) server := endless.NewServer(config.GetString("listen"), engine) server.BeforeBegin = func(addr string) { diff --git a/server/internal/core/fs/public/filesystem.go b/server/internal/core/fs/public/filesystem.go index 384dd202..2eff8273 100644 --- a/server/internal/core/fs/public/filesystem.go +++ b/server/internal/core/fs/public/filesystem.go @@ -2,7 +2,6 @@ package public import ( "context" - "io" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -11,12 +10,6 @@ import ( "github.com/shroff/phylum/server/internal/core/storage" ) -type FileSystem interface { - ResourceByPath(path string) (Resource, error) - OpenRead(r Resource, start, length int64) (io.ReadCloser, error) - ReadDir(r Resource, recursive bool) ([]Resource, error) -} - type filesystem struct { ctx context.Context db *db.DbHandler @@ -45,38 +38,3 @@ func (f filesystem) ResourceByPath(path string) (Resource, error) { contentSHA256: r.ContentSha256, }, nil } - -func (f filesystem) OpenRead(r Resource, start, length int64) (io.ReadCloser, error) { - return f.cs.OpenRead(r.id, start, length) -} - -func (f filesystem) ReadDir(r Resource, recursive bool) ([]Resource, error) { - if !r.dir { - return nil, fs.ErrResourceNotCollection - } - children, err := f.db.ReadDir(f.ctx, db.ReadDirParams{ - ID: r.id, - IncludeRoot: false, - Recursive: recursive, - }) - if err != nil { - return nil, err - } - - result := make([]Resource, len(children)) - - for i, c := range children { - result[i] = Resource{ - f: f, - id: c.ID, - name: c.Name, - dir: c.Dir, - created: c.Created.Time, - modified: c.Modified.Time, - contentSize: c.ContentSize, - contentType: c.ContentType, - contentSHA256: c.ContentSha256, - } - } - return result, nil -} diff --git a/server/internal/core/fs/public/public.go b/server/internal/core/fs/public/public.go index b67b3030..21f527db 100644 --- a/server/internal/core/fs/public/public.go +++ b/server/internal/core/fs/public/public.go @@ -14,45 +14,45 @@ import ( var ErrPublicShareNotFound = errors.NewError(http.StatusUnauthorized, "public_share_not_found", "public share not found") - -func OpenPublic(ctx context.Context, id string, password string) (FileSystem, error) { +func OpenResource(ctx context.Context, id string, password, path string) (Resource, error) { // Check exists s, err := db.Get().PublicShare(ctx, id) if errors.Is(err, pgx.ErrNoRows) { err = ErrPublicShareNotFound } if err != nil { - return nil, err + return Resource{}, err } // check password if s.PasswordHash.Valid { if ok, err := crypt.VerifyPassword(password, s.PasswordHash.String); err != nil { - return nil, err + return Resource{}, err } else if !ok { - return nil, ErrPublicShareNotFound + return Resource{}, ErrPublicShareNotFound } } // check expiration if s.Expires.Valid { if s.Expires.Time.Before(time.Now()) { - return nil, ErrPublicShareNotFound + return Resource{}, ErrPublicShareNotFound } } // check max accesses if s.AccessesLeft.Valid { if s.AccessesLeft.Int32 <= 0 { - return nil, ErrPublicShareNotFound + return Resource{}, ErrPublicShareNotFound } db.Get().AccessPublicShare(ctx, id) } - return filesystem{ + f := filesystem{ ctx: ctx, db: db.Get(), cs: storage.Get(), rootID: s.Root, - }, nil + } + return f.ResourceByPath(path) } diff --git a/server/internal/core/fs/public/resource.go b/server/internal/core/fs/public/resource.go index 7ead40a9..28ee230e 100644 --- a/server/internal/core/fs/public/resource.go +++ b/server/internal/core/fs/public/resource.go @@ -1,9 +1,13 @@ package public import ( + "io" "time" "github.com/google/uuid" + "github.com/shroff/phylum/server/internal/api/serve" + "github.com/shroff/phylum/server/internal/core/db" + "github.com/shroff/phylum/server/internal/core/fs" ) type Resource struct { @@ -11,6 +15,7 @@ type Resource struct { id uuid.UUID name string dir bool + path string created time.Time modified time.Time contentSize int64 @@ -18,11 +23,52 @@ type Resource struct { contentSHA256 string } -func (r Resource) MID() uuid.UUID { return r.id } -func (r Resource) MName() string { return r.name } -func (r Resource) MDir() bool { return r.dir } -func (r Resource) MCreated() time.Time { return r.created } -func (r Resource) MModified() time.Time { return r.modified } -func (r Resource) MContentSize() int64 { return r.contentSize } -func (r Resource) MContentSHA256() string { return r.contentSHA256 } -func (r Resource) MContentType() string { return r.contentType } +func (r Resource) FSID() uuid.UUID { return r.id } +func (r Resource) FSName() string { return r.name } +func (r Resource) FSPath() string { return r.name } +func (r Resource) FSDir() bool { return r.dir } +func (r Resource) FSCreated() time.Time { return r.created } +func (r Resource) FSModified() time.Time { return r.modified } +func (r Resource) FSContentSize() int64 { return r.contentSize } +func (r Resource) FSContentSHA256() string { return r.contentSHA256 } +func (r Resource) FSContentType() string { return r.contentType } + +func (r Resource) OpenRead(start, length int64) (io.ReadCloser, error) { + return r.f.cs.OpenRead(r.id, start, length) +} + +func (r Resource) ReadDir(recursive bool) ([]serve.Resource, error) { + if !r.dir { + return nil, fs.ErrResourceNotCollection + } + children, err := r.f.db.ReadDir(r.f.ctx, db.ReadDirParams{ + ID: r.id, + IncludeRoot: false, + Recursive: recursive, + }) + if err != nil { + return nil, err + } + + result := make([]serve.Resource, len(children)) + + for i, c := range children { + path := r.path + c.Path + if r.path == "/" { + path = c.Path + } + result[i] = Resource{ + f: r.f, + id: c.ID, + name: c.Name, + dir: c.Dir, + path: path, + created: c.Created.Time, + modified: c.Modified.Time, + contentSize: c.ContentSize, + contentType: c.ContentType, + contentSHA256: c.ContentSha256, + } + } + return result, nil +}