diff --git a/server/internal/api/routes/resources.go b/server/internal/api/routes/resources.go index 9f707e32..447386d9 100644 --- a/server/internal/api/routes/resources.go +++ b/server/internal/api/routes/resources.go @@ -19,7 +19,7 @@ const errCodeResourceNotCollection = "resource_not_collection" type resourceResponse struct { ID uuid.UUID `json:"id"` - Parent *uuid.UUID `json:"parent"` + Parent uuid.UUID `json:"parent"` Name string `json:"name"` Dir bool `json:"dir"` Created time.Time `json:"created"` diff --git a/server/internal/app/app.go b/server/internal/app/app.go index 4bd064e6..ac4360bc 100644 --- a/server/internal/app/app.go +++ b/server/internal/app/app.go @@ -1,8 +1,12 @@ package app import ( + "context" + + "github.com/google/uuid" "github.com/shroff/phylum/server/internal/app/core" "github.com/shroff/phylum/server/internal/db" + "github.com/shroff/phylum/server/internal/sql" ) type App struct { @@ -19,6 +23,15 @@ func Initialize(db *db.DbHandler, debug bool) error { return err } + if _, err := db.Queries().CreateResource(context.Background(), sql.CreateResourceParams{ + ID: uuid.UUID{}, + Parent: uuid.UUID{}, + Name: "root", + Dir: true, + }); err != nil { + return err + } + Default = &App{ Debug: debug, Db: db, @@ -27,3 +40,7 @@ func Initialize(db *db.DbHandler, debug bool) error { return nil } + +func (a App) OpenFileSystem(ctx context.Context, user int32) (core.FileSystem, error) { + return core.OpenFileSystem(a.Db, ctx, nil, user) +} diff --git a/server/internal/app/core/filesystem.go b/server/internal/app/core/filesystem.go new file mode 100644 index 00000000..293919c5 --- /dev/null +++ b/server/internal/app/core/filesystem.go @@ -0,0 +1,252 @@ +package core + +import ( + "context" + "io" + "io/fs" + "strings" + "time" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" + "github.com/shroff/phylum/server/internal/db" + "github.com/shroff/phylum/server/internal/sql" + "github.com/sirupsen/logrus" +) + +type FileSystem interface { + OpenWithRoot(Resource) FileSystem + ResourceByPath(path string) (Resource, error) + ResourceByID(id uuid.UUID) (Resource, error) + OpenRead(r Resource, start, length int64) (io.ReadCloser, error) + OpenWrite(r Resource) (io.WriteCloser, error) + ReadDir(r Resource) ([]Resource, error) + CreateMemberResource(r Resource, id uuid.UUID, name string, dir bool) (Resource, error) + DeleteRecursive(r Resource, hardDelete bool) error + UpdateName(r Resource, name string) error + UpdateParent(r Resource, parent uuid.UUID) error + UpdatePermissions(r Resource, userID int32, permission int32) error +} + +type filesystem struct { + db *db.DbHandler + ctx context.Context + root Resource + user int32 +} + +func OpenFileSystem(db *db.DbHandler, ctx context.Context, root Resource, user int32) (FileSystem, error) { + if root == nil { + root = resource{ + id: uuid.UUID{}, + permission: 0, + parentID: uuid.UUID{}, + name: "root", + size: 0, + collection: true, + modTime: time.Time{}, + etag: "", + } + } + return filesystem{ + db: db, + ctx: ctx, + root: root, + user: user, + }, nil +} + +func (f filesystem) OpenWithRoot(root Resource) FileSystem { + return filesystem{ + db: f.db, + ctx: f.ctx, + root: root, + user: f.user, + } +} + +func (f filesystem) ResourceByPath(path string) (Resource, error) { + path = strings.Trim(path, "/") + segments := strings.Split(path, "/") + if path == "" { + // Calling strings.Split on an empty string returns a slice of length 1. That breaks the query + segments = []string{} + } + + res, err := f.db.Queries().ResourceByPath(f.ctx, sql.ResourceByPathParams{Root: f.root.ID(), Search: segments, UserID: f.user}) + if err != nil { + return nil, fs.ErrNotExist + } + + if !res.Permission.Valid || res.Permission.Int32 == 0 { + return nil, ErrInsufficientPermissions + } + + return resource{ + id: res.ID, + permission: res.Permission.Int32, + parentID: res.Parent, + name: res.Name, + size: res.Size.Int64, + collection: res.Dir, + modTime: res.Modified.Time, + etag: res.Etag.String, + }, nil +} + +func (f filesystem) ResourceByID(id uuid.UUID) (Resource, error) { + res, err := f.db.Queries().ResourceByIdWithPermissions(f.ctx, sql.ResourceByIdWithPermissionsParams{Root: f.root.ID(), ResourceID: id, UserID: f.user}) + if err == pgx.ErrNoRows || res.Permission.Int32 == 0 { + err = fs.ErrNotExist + } + if err != nil { + return nil, err + } + + return resource{ + id: res.ID, + permission: res.Permission.Int32, + parentID: res.Parent, + name: res.Name, + size: res.Size.Int64, + collection: res.Dir, + modTime: res.Modified.Time, + etag: res.Etag.String, + }, nil +} +func (f filesystem) OpenRead(r Resource, start, length int64) (io.ReadCloser, error) { + if r.Permission() < PermissionReadOnly { + return nil, ErrInsufficientPermissions + } + return r.storage.OpenRead(r.ID(), start, length) +} + +func (f filesystem) OpenWrite(r Resource) (io.WriteCloser, error) { + if r.Permission() < PermissionReadWrite { + return nil, ErrInsufficientPermissions + } + return r.storage.OpenWrite(r.id, func(len int, etag string) error { + return r.db.Queries().UpdateResourceContents(f.ctx, sql.UpdateResourceContentsParams{ + ID: r.id, + Size: pgtype.Int8{Int64: int64(len), Valid: true}, + Etag: pgtype.Text{String: etag, Valid: true}, + }) + }) +} + +func (f filesystem) ReadDir(r Resource) ([]Resource, error) { + if r.Permission() < PermissionReadOnly { + return nil, ErrInsufficientPermissions + } + children, err := f.db.Queries().ReadDir(f.ctx, sql.ReadDirParams{ID: r.ID(), UserID: f.user, IncludeRoot: false, Recursive: false}) + if err != nil { + return nil, err + } + + result := make([]Resource, len(children)) + for i, c := range children { + permission := r.Permission() + if c.Permission.Valid && c.Permission.Int32 > permission { + permission = c.Permission.Int32 + + } + result[i] = resource{ + id: c.ID, + permission: permission, + parentID: r.ID(), + name: c.Name, + size: c.Size.Int64, + modTime: c.Modified.Time, + collection: c.Dir, + etag: c.Etag.String, + } + } + return result, nil +} + +func (f filesystem) CreateMemberResource(r Resource, id uuid.UUID, name string, dir bool) (Resource, error) { + if r.Permission() < PermissionReadWrite { + return nil, ErrInsufficientPermissions + } + if !r.IsDir() { + return resource{}, ErrResourceNotCollection + } + var result sql.Resource + err := f.db.RunInTx(f.ctx, func(q *sql.Queries) error { + var err error + if result, err = q.CreateResource(f.ctx, sql.CreateResourceParams{ID: id, Parent: r.ID(), Name: name, Dir: dir}); err != nil { + return err + } + return q.UpdateResourceModified(f.ctx, r.ID()) + }) + if err != nil { + return nil, err + } + return resource{ + id: id, + parentID: r.ID(), + permission: r.Permission(), + name: result.Name, + size: 0, + modTime: result.Modified.Time, + collection: dir, + etag: "", + }, nil +} + +func (f filesystem) DeleteRecursive(r Resource, hardDelete bool) error { + if r.Permission() < PermissionReadWrite { + return ErrInsufficientPermissions + } + // TODO: versioning + return f.db.RunInTx(f.ctx, func(q *sql.Queries) error { + if hardDelete { + deleted, err := q.HardDeleteRecursive(f.ctx, r.ID()) + if err != nil { + return err + } + errors := r.storage.Delete(deleted) + for err := range errors { + logrus.Warn(err) + } + } else { + if err := q.DeleteRecursive(f.ctx, r.ID()); err != nil { + return err + } + } + + emptyUUID := uuid.UUID{} + if r.ParentID() != emptyUUID { + return q.UpdateResourceModified(f.ctx, r.ParentID()) + } + return nil + }) +} + +func (f filesystem) UpdateName(r Resource, name string) error { + return f.db.Queries().UpdateResourceName(f.ctx, sql.UpdateResourceNameParams{ID: r.ID(), Name: name}) +} + +func (f filesystem) UpdateParent(r Resource, parent uuid.UUID) error { + emptyUUID := uuid.UUID{} + if r.ParentID() == emptyUUID { + return ErrCannotReparentRootResource + } + return f.db.Queries().UpdateResourceParent(f.ctx, sql.UpdateResourceParentParams{ID: r.ID(), Parent: parent}) +} + +func (f filesystem) UpdatePermissions(r Resource, userID int32, permission int32) error { + if r.Permission() < PermissionAdmin { + return ErrInsufficientPermissions + } + if permission > PermissionAdmin { + return ErrCannotGrantOwnerPermission + } + + return f.db.Queries().UpdatePermissionsForResource(f.ctx, sql.UpdatePermissionsForResourceParams{ + ResourceID: r.ID(), + UserID: userID, + Permission: permission, + }) +} diff --git a/server/internal/app/core/resource.go b/server/internal/app/core/resource.go index 3d56b9af..b7d15860 100644 --- a/server/internal/app/core/resource.go +++ b/server/internal/app/core/resource.go @@ -1,19 +1,13 @@ package core import ( - "context" "errors" "fmt" - "io" "mime" "path/filepath" "time" "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgtype" - "github.com/shroff/phylum/server/internal/db" - "github.com/shroff/phylum/server/internal/sql" - "github.com/sirupsen/logrus" ) const ( @@ -31,9 +25,9 @@ var ( ErrCannotReparentToRoot = errors.New("cannot reparent resource to root") ) -type ResourceInfo interface { +type Resource interface { ID() uuid.UUID - ParentID() *uuid.UUID + ParentID() uuid.UUID Name() string Size() int64 Permission() int32 @@ -43,27 +37,12 @@ type ResourceInfo interface { ContentType() string } -type Resource interface { - ResourceInfo - OpenRead(start, length int64) (io.ReadCloser, error) - OpenWrite(ctx context.Context) (io.WriteCloser, error) - ReadDir(ctx context.Context) ([]Resource, error) - CreateMemberResource(ctx context.Context, id uuid.UUID, name string, dir bool) (Resource, error) - DeleteRecursive(ctx context.Context, hardDelete bool) error - UpdateName(ctx context.Context, name string) error - UpdateParent(ctx context.Context, parent uuid.UUID) error - UpdatePermissions(ctx context.Context, userID int32, permission int32) error -} - type resource struct { - db *db.DbHandler - storage Storage id uuid.UUID - parentID *uuid.UUID + permission int32 + parentID uuid.UUID name string size int64 - userID int32 - permission int32 collection bool modTime time.Time etag string @@ -73,7 +52,7 @@ func (r resource) ID() uuid.UUID { return r.id } func (r resource) Permission() int32 { return r.permission } -func (r resource) ParentID() *uuid.UUID { return r.parentID } +func (r resource) ParentID() uuid.UUID { return r.parentID } func (r resource) Name() string { return r.name } @@ -99,143 +78,3 @@ func (r resource) ContentType() string { } return "application/octet-stream" } - -func (r resource) OpenRead(start, length int64) (io.ReadCloser, error) { - if r.permission < PermissionReadOnly { - return nil, ErrInsufficientPermissions - } - return r.storage.OpenRead(r.id, start, length) -} - -func (r resource) OpenWrite(ctx context.Context) (io.WriteCloser, error) { - if r.permission < PermissionReadWrite { - return nil, ErrInsufficientPermissions - } - return r.storage.OpenWrite(r.id, func(len int, etag string) error { - return r.db.Queries().UpdateResourceContents(ctx, sql.UpdateResourceContentsParams{ - ID: r.id, - Size: pgtype.Int8{Int64: int64(len), Valid: true}, - Etag: pgtype.Text{String: etag, Valid: true}, - }) - }) -} - -func (r resource) ReadDir(ctx context.Context) ([]Resource, error) { - if r.permission < PermissionReadOnly { - return nil, ErrInsufficientPermissions - } - children, err := r.db.Queries().ReadDir(ctx, sql.ReadDirParams{ID: r.id, UserID: r.userID, IncludeRoot: false, Recursive: false}) - if err != nil { - return nil, err - } - - result := make([]Resource, len(children)) - for i, c := range children { - permission := r.Permission() - if c.Permission.Valid && c.Permission.Int32 > permission { - permission = c.Permission.Int32 - - } - result[i] = resource{ - db: r.db, - storage: r.storage, - id: c.ID, - userID: r.userID, - permission: permission, - parentID: &r.id, - name: c.Name, - size: c.Size.Int64, - modTime: c.Modified.Time, - collection: c.Dir, - etag: c.Etag.String, - } - } - return result, nil -} - -func (r resource) CreateMemberResource(ctx context.Context, id uuid.UUID, name string, dir bool) (Resource, error) { - if r.permission < PermissionReadWrite { - return nil, ErrInsufficientPermissions - } - if !r.collection { - return resource{}, ErrResourceNotCollection - } - var result sql.Resource - err := r.db.RunInTx(ctx, func(q *sql.Queries) error { - var err error - if result, err = q.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: &r.id, Name: name, Dir: dir}); err != nil { - return err - } - return q.UpdateResourceModified(ctx, r.id) - }) - if err != nil { - return nil, err - } - return resource{ - db: r.db, - storage: r.storage, - id: id, - parentID: &r.id, - userID: r.userID, - permission: r.permission, - name: result.Name, - size: 0, - modTime: result.Modified.Time, - collection: dir, - etag: "", - }, nil -} - -func (r resource) DeleteRecursive(ctx context.Context, hardDelete bool) error { - if r.permission < PermissionReadWrite { - return ErrInsufficientPermissions - } - // TODO: versioning - return r.db.RunInTx(ctx, func(q *sql.Queries) error { - if hardDelete { - deleted, err := q.HardDeleteRecursive(ctx, r.id) - if err != nil { - return err - } - errors := r.storage.Delete(deleted) - for err := range errors { - logrus.Warn(err) - } - } else { - if err := q.DeleteRecursive(ctx, r.id); err != nil { - return err - } - } - - if r.parentID != nil { - return q.UpdateResourceModified(ctx, *r.parentID) - } - return nil - }) -} - -func (r resource) UpdateName(ctx context.Context, name string) error { - return r.db.Queries().UpdateResourceName(ctx, sql.UpdateResourceNameParams{ID: r.id, Name: name}) -} - -func (r resource) UpdateParent(ctx context.Context, parent uuid.UUID) error { - if r.parentID == nil { - return ErrCannotReparentRootResource - } - return r.db.Queries().UpdateResourceParent(ctx, sql.UpdateResourceParentParams{ID: r.id, Parent: parent}) -} - -func (r resource) UpdatePermissions(ctx context.Context, userID int32, permission int32) error { - if r.permission < PermissionAdmin { - return ErrInsufficientPermissions - } - if permission > PermissionAdmin { - return ErrCannotGrantOwnerPermission - } - - return r.db.Queries().UpdatePermissionsForResource(ctx, sql.UpdatePermissionsForResourceParams{ - ResourceID: r.id, - UserID: userID, - Permission: permission, - }) -} diff --git a/server/internal/app/core/silo.go b/server/internal/app/core/silo.go index 6e073198..9c9c0e02 100644 --- a/server/internal/app/core/silo.go +++ b/server/internal/app/core/silo.go @@ -1,13 +1,8 @@ package core import ( - "context" - "io/fs" - "strings" - "github.com/google/uuid" "github.com/shroff/phylum/server/internal/db" - "github.com/shroff/phylum/server/internal/sql" ) type Silo interface { @@ -15,7 +10,6 @@ type Silo interface { Name() string Owner() int32 StorageName() string - ResourceByPath(ctx context.Context, path string, userID int32) (Resource, error) } type silo struct { @@ -51,35 +45,3 @@ func (s *silo) Owner() int32 { func (s *silo) StorageName() string { return s.storage.Name() } - -func (s *silo) ResourceByPath(ctx context.Context, path string, userID int32) (Resource, error) { - path = strings.Trim(path, "/") - segments := strings.Split(path, "/") - if path == "" { - // Calling strings.Split on an empty string returns a slice of length 1. That breaks the query - segments = []string{} - } - - res, err := s.db.Queries().ResourceByPath(ctx, sql.ResourceByPathParams{Root: s.root, Search: segments, UserID: userID}) - if err != nil { - return nil, fs.ErrNotExist - } - - if !res.Permission.Valid || res.Permission.Int32 == 0 { - return nil, ErrInsufficientPermissions - } - - return resource{ - db: s.db, - storage: s.storage, - id: res.ID, - userID: userID, - permission: res.Permission.Int32, - parentID: res.Parent, - name: res.Name, - size: res.Size.Int64, - collection: res.Dir, - modTime: res.Modified.Time, - etag: res.Etag.String, - }, nil -} diff --git a/server/internal/app/resources.go b/server/internal/app/resources.go index 9bf3e92d..86f067be 100644 --- a/server/internal/app/resources.go +++ b/server/internal/app/resources.go @@ -1,24 +1,7 @@ package app import ( - "context" "errors" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/shroff/phylum/server/internal/sql" ) var ErrResourceNotFound = errors.New("resource not found") - -func (a *App) LocateResource(id uuid.UUID, userID int32) (uuid.UUID, int, error) { - result, err := a.Db.Queries().PermissionsForResource(context.Background(), sql.PermissionsForResourceParams{ResourceID: id, UserID: userID}) - if err == pgx.ErrNoRows { - err = ErrResourceNotFound - } - if err != nil { - return uuid.UUID{}, 0, err - } - - return result.ID, int(result.Permission.Int32), nil -} diff --git a/server/internal/app/silos.go b/server/internal/app/silos.go index 79b66a86..655219b6 100644 --- a/server/internal/app/silos.go +++ b/server/internal/app/silos.go @@ -21,7 +21,7 @@ func (a App) CreateSilo(ctx context.Context, id uuid.UUID, owner int32, storage, } if _, err := q.CreateResource(ctx, sql.CreateResourceParams{ ID: id, - Parent: nil, + Parent: uuid.UUID{}, Name: id.String(), Dir: true, }); err != nil { @@ -86,25 +86,26 @@ func (a App) FindSilo(ctx context.Context, idOrName string) (core.Silo, error) { } func (a App) DeleteSilo(ctx context.Context, id uuid.UUID) error { - return a.Db.RunInTx(ctx, func(q *sql.Queries) error { - result, err := q.SiloById(ctx, id) - if err != nil { - return err - } - storage := a.FindStorageBackend(result.Storage) - if storage == nil { - return errors.New("storage backend not found for " + id.String()) - } + return nil + // return a.Db.RunInTx(ctx, func(q *sql.Queries) error { + // result, err := q.SiloById(ctx, id) + // if err != nil { + // return err + // } + // storage := a.FindStorageBackend(result.Storage) + // if storage == nil { + // return errors.New("storage backend not found for " + id.String()) + // } - silo := core.NewSilo(a.Db, result.Name, result.Owner, result.ID, storage) - resource, err := silo.ResourceByPath(ctx, "/", -1) - if err != nil { - return err - } - if err := resource.DeleteRecursive(ctx, true); err != nil { - return err - } + // silo := core.NewSilo(a.Db, result.Name, result.Owner, result.ID, storage) + // resource, err := silo.ResourceByPath(ctx, "/", -1) + // if err != nil { + // return err + // } + // if err := resource.DeleteRecursive(ctx, true); err != nil { + // return err + // } - return q.DeleteSilo(ctx, id) - }) + // return q.DeleteSilo(ctx, id) + // }) } diff --git a/server/internal/handler_webdav/handler.go b/server/internal/handler_webdav/handler.go index 5cf3ebeb..eba2ad82 100644 --- a/server/internal/handler_webdav/handler.go +++ b/server/internal/handler_webdav/handler.go @@ -42,46 +42,52 @@ func SetupHandler(r *gin.RouterGroup, app *app.App) { } func (h *handler) HandleRequest(c *gin.Context) { - path := c.Params.ByName("path") - identifier := strings.TrimLeft(path, "/") - if identifier == "" { - // No path specified - c.JSON(404, gin.H{ - "ERR_CODE": "err_silo_path_not_specified", - }) - return - } - index := strings.Index(identifier, "/") - if index != -1 { - identifier = identifier[0:index] - } - silo, err := h.app.FindSilo(context.Background(), identifier) - if err != nil { - c.Writer.WriteHeader(404) - c.JSON(404, gin.H{ - "ERR_CODE": "err_silo_not_found", - "ERR_DETAILS": err.Error(), - }) - return - } + // path := c.Params.ByName("path") + // identifier := strings.TrimLeft(path, "/") + // if identifier == "" { + // // No path specified + // c.JSON(404, gin.H{ + // "ERR_CODE": "err_silo_path_not_specified", + // }) + // return + // } + // index := strings.Index(identifier, "/") + // if index != -1 { + // identifier = identifier[0:index] + // } - userID := auth.GetUserID(c) + fs, err := h.app.OpenFileSystem(c.Request.Context(), auth.GetUserID(c)) + + // silo, err := h.app.FindSilo(context.Background(), identifier) + if err != nil { + c.AbortWithStatusJSON(404, gin.H{ + "ERR_CODE": "err_silo_not_found", + }) + return + } webdavHandler := webdav.Handler{ - Prefix: h.prefix + "/" + identifier, - FileSystem: adapter{silo: silo, userID: userID}, + Prefix: h.prefix, + FileSystem: adapter{fs: fs}, LockSystem: webdav.NewMemLS(), } webdavHandler.ServeHTTP(c.Writer, c.Request) } type adapter struct { - silo core.Silo - userID int32 + fs core.FileSystem } func (a adapter) Stat(ctx context.Context, name string) (core.Resource, error) { - return a.silo.ResourceByPath(ctx, name, a.userID) + return a.fs.ResourceByPath(name) +} + +func (a adapter) OpenRead(r core.Resource, start, len int64) (io.ReadCloser, error) { + return a.fs.OpenRead(r, start, len) +} + +func (a adapter) ReadDir(r core.Resource) ([]core.Resource, error) { + return a.fs.ReadDir(r) } func (a adapter) OpenWrite(ctx context.Context, name string) (io.WriteCloser, error) { @@ -99,18 +105,18 @@ func (a adapter) OpenWrite(ctx context.Context, name string) (io.WriteCloser, er } resourceName := name[index+1:] resourceId := uuid.New() - resource, err = parent.CreateMemberResource(ctx, resourceId, resourceName, false) + resource, err = a.fs.CreateMemberResource(parent, resourceId, resourceName, false) if err != nil { return nil, err } - return resource.OpenWrite(ctx) + return a.fs.OpenWrite(resource) } return nil, err } if resource.IsDir() { return nil, errors.New("cannot open collection for write") } - return resource.OpenWrite(ctx) + return a.fs.OpenWrite(resource) } func (a adapter) RemoveAll(ctx context.Context, name string) error { @@ -118,7 +124,7 @@ func (a adapter) RemoveAll(ctx context.Context, name string) error { if err != nil { return fs.ErrNotExist } - return resource.DeleteRecursive(ctx, false) + return a.fs.DeleteRecursive(resource, false) } func (a adapter) Mkdir(ctx context.Context, name string) error { @@ -133,7 +139,7 @@ func (a adapter) Mkdir(ctx context.Context, name string) error { return fs.ErrNotExist } dirName := name[index+1:] - _, err = parent.CreateMemberResource(ctx, uuid.New(), dirName, true) + _, err = a.fs.CreateMemberResource(parent, uuid.New(), dirName, true) return err } @@ -158,13 +164,13 @@ func (a adapter) Rename(ctx context.Context, oldName, newName string) error { return fs.ErrNotExist } - if src.ParentID() != nil && *src.ParentID() != parent.ID() { - if err = src.UpdateParent(ctx, parent.ID()); err != nil { + if src.ParentID() != parent.ID() { + if err = a.fs.UpdateParent(src, parent.ID()); err != nil { return err } } if src.Name() != newName && newName != "" && newName != "/" { - if err = src.UpdateName(ctx, newName); err != nil { + if err = a.fs.UpdateName(src, newName); err != nil { return err } } diff --git a/server/internal/migrations/data/001_resources.sql b/server/internal/migrations/data/001_resources.sql index fbc858a6..8ebe147a 100644 --- a/server/internal/migrations/data/001_resources.sql +++ b/server/internal/migrations/data/001_resources.sql @@ -1,6 +1,6 @@ CREATE TABLE resources ( id uuid PRIMARY KEY, - parent uuid REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE, + parent uuid NOT NULL REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE, name TEXT NOT NULL, dir BOOLEAN NOT NULL, created TIMESTAMP NOT NULL, diff --git a/server/internal/sql/models.go b/server/internal/sql/models.go index 0722807b..13433c01 100644 --- a/server/internal/sql/models.go +++ b/server/internal/sql/models.go @@ -24,7 +24,7 @@ type Permission struct { type Resource struct { ID uuid.UUID - Parent *uuid.UUID + Parent uuid.UUID Name string Dir bool Created pgtype.Timestamp diff --git a/server/internal/sql/permissions.sql.go b/server/internal/sql/permissions.sql.go index b4a39d40..fe0628b2 100644 --- a/server/internal/sql/permissions.sql.go +++ b/server/internal/sql/permissions.sql.go @@ -12,32 +12,68 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const permissionsForResource = `-- name: PermissionsForResource :one +const resourceByIdWithPermissions = `-- name: ResourceByIdWithPermissions :one WITH RECURSIVE nodes(id, parent, permission) AS ( SELECT r.id, r.parent, p.permission - FROM resources r LEFT JOIN permissions p on r.id = p.resource_id WHERE r.id = $1::uuid AND p.user_id = $2::int + FROM resources r + LEFT JOIN permissions p + on r.id = p.resource_id + AND p.user_id = $2::int + WHERE r.id = $3::uuid UNION ALL SELECT r.id, r.parent, CASE WHEN (n.permission IS NULL OR p.permission > n.permission) THEN p.permission ELSE n.permission END - FROM resources r JOIN nodes n ON r.id = n.parent LEFT JOIN permissions p ON r.id = p.resource_id AND p.user_id = $2::int + FROM resources r + JOIN nodes n + ON r.id = n.parent + LEFT JOIN permissions p + ON r.id = p.resource_id AND p.user_id = $2::int + WHERE r.parent = $1::uuid + OR r.parent = '00000000-0000-0000-0000-000000000000' ) -SELECT id, parent, permission FROM nodes WHERE parent IS NULL +SELECT n.id, n.parent, permission, r.id, r.parent, name, dir, created, modified, deleted, size, etag FROM nodes n +JOIN resources r +ON n.id = r.id +WHERE n.parent = $1::uuid ` -type PermissionsForResourceParams struct { - ResourceID uuid.UUID +type ResourceByIdWithPermissionsParams struct { + Root uuid.UUID UserID int32 + ResourceID uuid.UUID } -type PermissionsForResourceRow struct { +type ResourceByIdWithPermissionsRow struct { ID uuid.UUID - Parent *uuid.UUID + Parent uuid.UUID Permission pgtype.Int4 + ID_2 uuid.UUID + Parent_2 uuid.UUID + Name string + Dir bool + Created pgtype.Timestamp + Modified pgtype.Timestamp + Deleted pgtype.Timestamp + Size pgtype.Int8 + Etag pgtype.Text } -func (q *Queries) PermissionsForResource(ctx context.Context, arg PermissionsForResourceParams) (PermissionsForResourceRow, error) { - row := q.db.QueryRow(ctx, permissionsForResource, arg.ResourceID, arg.UserID) - var i PermissionsForResourceRow - err := row.Scan(&i.ID, &i.Parent, &i.Permission) +func (q *Queries) ResourceByIdWithPermissions(ctx context.Context, arg ResourceByIdWithPermissionsParams) (ResourceByIdWithPermissionsRow, error) { + row := q.db.QueryRow(ctx, resourceByIdWithPermissions, arg.Root, arg.UserID, arg.ResourceID) + var i ResourceByIdWithPermissionsRow + err := row.Scan( + &i.ID, + &i.Parent, + &i.Permission, + &i.ID_2, + &i.Parent_2, + &i.Name, + &i.Dir, + &i.Created, + &i.Modified, + &i.Deleted, + &i.Size, + &i.Etag, + ) return i, err } diff --git a/server/internal/sql/resources.sql.go b/server/internal/sql/resources.sql.go index 89b8fddc..d7ab461f 100644 --- a/server/internal/sql/resources.sql.go +++ b/server/internal/sql/resources.sql.go @@ -22,7 +22,7 @@ INSERT INTO resources( type CreateResourceParams struct { ID uuid.UUID - Parent *uuid.UUID + Parent uuid.UUID Name string Dir bool } @@ -137,7 +137,7 @@ type ReadDirParams struct { type ReadDirRow struct { ID uuid.UUID - Parent *uuid.UUID + Parent uuid.UUID Name string Dir bool Created pgtype.Timestamp @@ -236,7 +236,7 @@ type ResourceByPathParams struct { type ResourceByPathRow struct { ID uuid.UUID - Parent *uuid.UUID + Parent uuid.UUID Name string Dir bool Created pgtype.Timestamp diff --git a/server/internal/webdav/file.go b/server/internal/webdav/file.go index e08578a8..8cdb602e 100644 --- a/server/internal/webdav/file.go +++ b/server/internal/webdav/file.go @@ -23,6 +23,8 @@ type FileSystem interface { Mkdir(ctx context.Context, name string) error RemoveAll(ctx context.Context, name string) error Rename(ctx context.Context, oldName, newName string) error + ReadDir(r core.Resource) ([]core.Resource, error) + OpenRead(r core.Resource, start, length int64) (io.ReadCloser, error) OpenWrite(ctx context.Context, name string) (io.WriteCloser, error) } @@ -97,7 +99,7 @@ func copyFiles(ctx context.Context, fs FileSystem, src, dst string, overwrite bo return http.StatusForbidden, err } if depth == infiniteDepth { - children, err := srcStat.ReadDir(ctx) + children, err := fs.ReadDir(srcStat) if err != nil { return http.StatusForbidden, err } @@ -114,7 +116,7 @@ func copyFiles(ctx context.Context, fs FileSystem, src, dst string, overwrite bo } } else { - srcFile, err := srcStat.OpenRead(0, -1) + srcFile, err := fs.OpenRead(srcStat, 0, -1) if err != nil { if os.IsNotExist(err) { return http.StatusNotFound, err @@ -174,7 +176,7 @@ func walkFS(ctx context.Context, fs FileSystem, depth int, name string, info cor if err != nil { return walkFn(name, info, err) } - fileInfos, err := f.ReadDir(ctx) + fileInfos, err := fs.ReadDir(f) if err != nil { return walkFn(name, info, err) } diff --git a/server/internal/webdav/prop.go b/server/internal/webdav/prop.go index cde76e27..38f4a569 100644 --- a/server/internal/webdav/prop.go +++ b/server/internal/webdav/prop.go @@ -70,7 +70,7 @@ func makePropstats(x, y Propstat) []Propstat { var liveProps = map[xml.Name]struct { // findFn implements the propfind function of this property. If nil, // it indicates a hidden property. - findFn func(core.ResourceInfo) string + findFn func(core.Resource) string // dir is true if the property applies to directories. dir bool }{ @@ -265,34 +265,34 @@ func escapeXML(s string) string { return s } -func findResourceType(fi core.ResourceInfo) string { +func findResourceType(fi core.Resource) string { if fi.IsDir() { return `` } return "" } -func findDisplayName(fi core.ResourceInfo) string { +func findDisplayName(fi core.Resource) string { return escapeXML(fi.Name()) } -func findContentLength(fi core.ResourceInfo) string { +func findContentLength(fi core.Resource) string { return strconv.FormatInt(fi.Size(), 10) } -func findLastModified(fi core.ResourceInfo) string { +func findLastModified(fi core.Resource) string { return fi.ModTime().UTC().Format(http.TimeFormat) } -func findContentType(fi core.ResourceInfo) string { +func findContentType(fi core.Resource) string { return fi.ContentType() } -func findETag(fi core.ResourceInfo) string { +func findETag(fi core.Resource) string { return fi.ETag() } -func findSupportedLock(fi core.ResourceInfo) string { +func findSupportedLock(fi core.Resource) string { return `` + `` + `` + diff --git a/server/internal/webdav/serve_resource.go b/server/internal/webdav/serve_resource.go index 0d358655..a4f3d518 100644 --- a/server/internal/webdav/serve_resource.go +++ b/server/internal/webdav/serve_resource.go @@ -25,7 +25,7 @@ var htmlReplacer = strings.NewReplacer( "'", "'", ) -func serveCollection(w http.ResponseWriter, r *http.Request, file core.Resource) { +func serveCollection(w http.ResponseWriter, r *http.Request, fs FileSystem, file core.Resource) { if !strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, r.URL.String()+"/", http.StatusMovedPermanently) return @@ -36,7 +36,7 @@ func serveCollection(w http.ResponseWriter, r *http.Request, file core.Resource) } w.Header().Set("Last-Modified", file.ModTime().Format(http.TimeFormat)) - files, err := file.ReadDir(r.Context()) + files, err := fs.ReadDir(file) if err != nil { http.Error(w, "Error reading directory", http.StatusInternalServerError) return @@ -59,7 +59,7 @@ func serveCollection(w http.ResponseWriter, r *http.Request, file core.Resource) fmt.Fprintf(w, "\n") } -func serveResource(w http.ResponseWriter, r *http.Request, file core.Resource) { +func serveResource(w http.ResponseWriter, r *http.Request, fs FileSystem, file core.Resource) { w.Header().Set("Etag", file.ETag()) w.Header().Set("Last-Modified", file.ModTime().Format(http.TimeFormat)) w.Header().Set("Content-Type", file.ContentType()) @@ -95,9 +95,9 @@ func serveResource(w http.ResponseWriter, r *http.Request, file core.Resource) { sendSize = ra.length code = http.StatusPartialContent w.Header().Set("Content-Range", ra.contentRange(file.Size())) - reader, err = file.OpenRead(ra.start, ra.length) + reader, err = fs.OpenRead(file, ra.start, ra.length) } else { - reader, err = file.OpenRead(0, -1) + reader, err = fs.OpenRead(file, 0, -1) } if err != nil { @@ -206,7 +206,7 @@ func parseRange(s string, size int64) ([]httpRange, error) { // checkPreconditions evaluates request preconditions and reports whether a precondition // resulted in sending StatusNotModified or StatusPreconditionFailed. -func checkPreconditions(w http.ResponseWriter, r *http.Request, ri core.ResourceInfo) (done bool, rangeHeader string) { +func checkPreconditions(w http.ResponseWriter, r *http.Request, ri core.Resource) (done bool, rangeHeader string) { // This function carefully follows RFC 7232 section 6. ch := checkIfMatch(r, ri) if ch == condNone { @@ -289,7 +289,7 @@ const ( condFalse ) -func checkIfMatch(r *http.Request, ri core.ResourceInfo) condResult { +func checkIfMatch(r *http.Request, ri core.Resource) condResult { im := r.Header.Get("If-Match") if im == "" { return condNone @@ -319,7 +319,7 @@ func checkIfMatch(r *http.Request, ri core.ResourceInfo) condResult { return condFalse } -func checkIfUnmodifiedSince(r *http.Request, ri core.ResourceInfo) condResult { +func checkIfUnmodifiedSince(r *http.Request, ri core.Resource) condResult { ius := r.Header.Get("If-Unmodified-Since") if ius == "" || isZeroTime(ri.ModTime()) { return condNone @@ -338,7 +338,7 @@ func checkIfUnmodifiedSince(r *http.Request, ri core.ResourceInfo) condResult { return condFalse } -func checkIfNoneMatch(r *http.Request, ri core.ResourceInfo) condResult { +func checkIfNoneMatch(r *http.Request, ri core.Resource) condResult { inm := r.Header.Get("If-None-Match") if inm == "" { return condNone @@ -368,7 +368,7 @@ func checkIfNoneMatch(r *http.Request, ri core.ResourceInfo) condResult { return condTrue } -func checkIfModifiedSince(r *http.Request, ri core.ResourceInfo) condResult { +func checkIfModifiedSince(r *http.Request, ri core.Resource) condResult { if r.Method != "GET" && r.Method != "HEAD" { return condNone } @@ -389,7 +389,7 @@ func checkIfModifiedSince(r *http.Request, ri core.ResourceInfo) condResult { return condTrue } -func checkIfRange(r *http.Request, ri core.ResourceInfo) condResult { +func checkIfRange(r *http.Request, ri core.Resource) condResult { if r.Method != "GET" && r.Method != "HEAD" { return condNone } diff --git a/server/internal/webdav/webdav.go b/server/internal/webdav/webdav.go index a863eee6..3c0bd292 100644 --- a/server/internal/webdav/webdav.go +++ b/server/internal/webdav/webdav.go @@ -206,9 +206,9 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta return http.StatusNotFound, err } if file.IsDir() { - serveCollection(w, r, file) + serveCollection(w, r, h.FileSystem, file) } else { - serveResource(w, r, file) + serveResource(w, r, h.FileSystem, file) } return 0, nil @@ -625,7 +625,7 @@ func makePropstatResponse(href string, pstats []Propstat) *response { return &resp } -func handlePropfindError(err error, info core.ResourceInfo) error { +func handlePropfindError(err error, info core.Resource) error { var skipResp error = nil if info.IsDir() { skipResp = filepath.SkipDir diff --git a/server/sql/queries/permissions.sql b/server/sql/queries/permissions.sql index 0a3300e4..50e84c7b 100644 --- a/server/sql/queries/permissions.sql +++ b/server/sql/queries/permissions.sql @@ -1,13 +1,26 @@ --- name: PermissionsForResource :one +-- name: ResourceByIdWithPermissions :one WITH RECURSIVE nodes(id, parent, permission) AS ( SELECT r.id, r.parent, p.permission - FROM resources r LEFT JOIN permissions p on r.id = p.resource_id WHERE r.id = @resource_id::uuid AND p.user_id = @user_id::int + FROM resources r + LEFT JOIN permissions p + on r.id = p.resource_id + AND p.user_id = @user_id::int + WHERE r.id = @resource_id::uuid UNION ALL SELECT r.id, r.parent, CASE WHEN (n.permission IS NULL OR p.permission > n.permission) THEN p.permission ELSE n.permission END - FROM resources r JOIN nodes n ON r.id = n.parent LEFT JOIN permissions p ON r.id = p.resource_id AND p.user_id = @user_id::int + FROM resources r + JOIN nodes n + ON r.id = n.parent + LEFT JOIN permissions p + ON r.id = p.resource_id AND p.user_id = @user_id::int + WHERE r.parent = @root::uuid + OR r.parent = '00000000-0000-0000-0000-000000000000' ) -SELECT * FROM nodes WHERE parent IS NULL; +SELECT * FROM nodes n +JOIN resources r +ON n.id = r.id +WHERE n.parent = @root::uuid; -- name: UpdatePermissionsForResource :exec INSERT INTO permissions(resource_id, user_id, permission)