diff --git a/server/internal/api/v1/responses/helpers.go b/server/internal/api/v1/responses/helpers.go index db538b9d..60b77cc2 100644 --- a/server/internal/api/v1/responses/helpers.go +++ b/server/internal/api/v1/responses/helpers.go @@ -46,5 +46,6 @@ func ResourceFromFS(r fs.Resource) Resource { ContentSHA256: r.ContentSHA256(), Permissions: string(r.Permissions()), Grants: string(r.Grants()), + Links: string(r.Links()), } } diff --git a/server/internal/api/v1/responses/responses.go b/server/internal/api/v1/responses/responses.go index a7eaca25..10304829 100644 --- a/server/internal/api/v1/responses/responses.go +++ b/server/internal/api/v1/responses/responses.go @@ -25,6 +25,7 @@ type Resource struct { ContentSHA256 string `json:"c_sha256"` Permissions string `json:"permissions"` Grants string `json:"grants"` + Links string `json:"links"` } type ResourceFull struct { diff --git a/server/internal/command/common/format.go b/server/internal/command/common/format.go index 3b9130bc..36bb94af 100644 --- a/server/internal/command/common/format.go +++ b/server/internal/command/common/format.go @@ -9,23 +9,6 @@ import ( "github.com/shroff/phylum/server/internal/core/fs" ) -func formatRow(id string, dir bool, size int, sha256, name, permissions string, deleted bool) string { - sizeStr := " -" - if dir { - sha256 = "- " - } else { - sizeStr = FormatSize(size) - } - if sha256 == "" { - sha256 = "xxxxxxxx" - } - delStr := " " - if deleted { - delStr = "*" - } - return fmt.Sprintf("%s %s %s %s%-24s %s", id, sizeStr, sha256[0:8], delStr, name, permissions) -} - func FormatResourceSummary(r fs.Resource, name string, deleted pgtype.Timestamp) string { if name == "" { name = r.Name() @@ -33,7 +16,21 @@ func FormatResourceSummary(r fs.Resource, name string, deleted pgtype.Timestamp) if r.Dir() { name += "/" } - return formatRow(r.ID().String(), r.Dir(), int(r.ContentLength()), r.ContentSHA256(), name, formatGrantsJson(r.Grants()), deleted != r.Deleted()) + sha256 := r.ContentSHA256() + sizeStr := " -" + if r.Dir() { + sha256 = "- " + } else { + sizeStr = FormatSize(r.ContentLength()) + } + if sha256 == "" { + sha256 = "xxxxxxxx" + } + delStr := " " + if deleted != r.Deleted() { + delStr = "*" + } + return fmt.Sprintf("%s %s %s %s%-24s %s %s", r.ID().String(), sizeStr, sha256[0:8], delStr, name, formatGrantsJson(r.Grants()), r.Links()) } func FormatSize(size int) string { diff --git a/server/internal/core/db/migrations/data/008_linked_resources.sql b/server/internal/core/db/migrations/data/008_linked_resources.sql new file mode 100644 index 00000000..ae23809b --- /dev/null +++ b/server/internal/core/db/migrations/data/008_linked_resources.sql @@ -0,0 +1,7 @@ +CREATE VIEW linked_resources AS + SELECT r.*, (SELECT jsonb_agg(l.name) FROM publinks l WHERE l.root = r.id AND l.deleted IS NULL) AS links + FROM resources r; + +---- create above / drop below ---- + +DROP VIEW linked_resources; \ No newline at end of file diff --git a/server/internal/core/fs/create.go b/server/internal/core/fs/create.go index d2b12b36..99fd8d14 100644 --- a/server/internal/core/fs/create.go +++ b/server/internal/core/fs/create.go @@ -116,7 +116,7 @@ func (f filesystem) createResource( case ResourceBindConflictResolutionError: err = ErrResourceNameConflict case ResourceBindConflictResolutionEnsure: - res, err = f.ChildResourceByName(parent, name) + res, err = f.childResourceByName(parent, name) if err == nil && res.dir != dir { err = ErrResourceNameConflict } @@ -150,7 +150,7 @@ func (f filesystem) createResource( } } case ResourceBindConflictResolutionOverwrite: - res, err = f.ChildResourceByName(parent, name) + res, err = f.childResourceByName(parent, name) if err == nil { deleted = true if res.dir == dir { @@ -185,7 +185,7 @@ func (f filesystem) createResource( } } case ResourceBindConflictResolutionDelete: - res, err = f.ChildResourceByName(parent, name) + res, err = f.childResourceByName(parent, name) if err == nil { deleted = true err = f.deleteRecursive(res.id, parent, true, false) @@ -229,6 +229,6 @@ func (f filesystem) insertResource(id, parent uuid.UUID, name string, dir bool, if rows, err := f.db.Query(query, args...); err != nil { return Resource{}, err } else { - return f.collectResource(rows) + return f.collectResource(rows, false) } } diff --git a/server/internal/core/fs/find.go b/server/internal/core/fs/find.go index 9ad2ba80..474cd3b1 100644 --- a/server/internal/core/fs/find.go +++ b/server/internal/core/fs/find.go @@ -23,7 +23,8 @@ func (f filesystem) ResourceByPath(path string) (Resource, error) { Where(resources.Col("id").Eq(goqu.V(f.rootID))). UnionAll(sub) - q := pg.Select(resources.All()). + l := goqu.T("publinks").As("l") + q := pg.Select(resources.All(), pg.Select(goqu.L("jsonb_agg(?)", l.Col("name"))).From(l).Where(l.Col("root").Eq(resources.Col("id")), l.Col("deleted").IsNull())). From(resources). WithRecursive("nodes(id, parent, search, depth)", rec). Join(nodes, goqu.On(resources.Col("id").Eq(nodes.Col("id")))). @@ -34,25 +35,25 @@ func (f filesystem) ResourceByPath(path string) (Resource, error) { if rows, err := f.db.Query(query, args...); err != nil { return Resource{}, err } else { - return f.collectResource(rows) + return f.collectResource(rows, true) } } func (f filesystem) ResourceByID(id uuid.UUID) (Resource, error) { - const query = "SELECT * FROM resources WHERE id = $1::UUID" + const query = "SELECT r.*, (SELECT jsonb_agg(l.name) FROM publinks l WHERE l.root = r.id AND l.deleted IS NULL) AS links FROM resources r WHERE id = $1::UUID" if rows, err := f.db.Query(query, id); err != nil { return Resource{}, err } else { - return f.collectResource(rows) + return f.collectResource(rows, true) } } -func (f filesystem) ChildResourceByName(parentID uuid.UUID, name string) (Resource, error) { +func (f filesystem) childResourceByName(parentID uuid.UUID, name string) (Resource, error) { const query = "SELECT * FROM resources WHERE parent = $1::UUID AND name = $2::TEXT AND deleted IS NULL" if rows, err := f.db.Query(query, parentID, name); err != nil { return Resource{}, err } else { - return f.collectResource(rows) + return f.collectResource(rows, false) } } diff --git a/server/internal/core/fs/resource.go b/server/internal/core/fs/resource.go index fe1d5ab1..b56c89cb 100644 --- a/server/internal/core/fs/resource.go +++ b/server/internal/core/fs/resource.go @@ -1,6 +1,7 @@ package fs import ( + "encoding/json" "time" "github.com/google/uuid" @@ -22,6 +23,7 @@ type Resource struct { contentSHA256 string permissions []byte grants []byte + links []byte userPermission Permission } @@ -37,13 +39,18 @@ func (r Resource) ContentSHA256() string { return r.contentSHA256 } func (r Resource) ContentType() string { return r.contentType } func (r Resource) Permissions() []byte { return r.permissions } func (r Resource) Grants() []byte { return r.grants } +func (r Resource) Links() []byte { return r.links } func (r Resource) hasPermission(p Permission) bool { return r.userPermission&p != 0 } -func (f filesystem) collectResource(rows pgx.Rows) (Resource, error) { - if r, err := pgx.CollectExactlyOneRow(rows, f.scanResource); err == nil { +func (f filesystem) collectResource(rows pgx.Rows, includeLinks bool) (Resource, error) { + scanner := f.scanLinkedResource + if !includeLinks { + scanner = f.scanResource + } + if r, err := pgx.CollectExactlyOneRow(rows, scanner); err == nil { return r, nil } else { if err == pgx.ErrNoRows { @@ -54,6 +61,41 @@ func (f filesystem) collectResource(rows pgx.Rows) (Resource, error) { } +func (f filesystem) scanLinkedResource(row pgx.CollectableRow) (Resource, error) { + r := Resource{f: f} + err := row.Scan( + &r.id, + &r.name, + &r.parentID, + &r.dir, + &r.created, + &r.modified, + &r.deleted, + &r.contentLength, + &r.contentType, + &r.contentSHA256, + &r.permissions, + &r.grants, + &r.links, + ) + if err != nil { + return r, err + } + permission := Permission(0) + if f.fullAccess { + permission = -1 + } else if p, err := readPermissionFromJson(r.permissions, f.username); err != nil { + return Resource{}, err + } else { + permission = p + } + if permission&PermissionRead == 0 { + return Resource{}, ErrResourceNotFound + } + r.userPermission = permission + return r, err +} + func (f filesystem) scanResource(row pgx.CollectableRow) (Resource, error) { r := Resource{f: f} err := row.Scan( @@ -87,3 +129,18 @@ func (f filesystem) scanResource(row pgx.CollectableRow) (Resource, error) { r.userPermission = permission return r, err } + +func readPermissionFromJson(j []byte, username string) (Permission, error) { + if j == nil { + return PermissionNone, nil + } + p := make(map[string]Permission) + err := json.Unmarshal(j, &p) + if err != nil { + return PermissionNone, err + } + if p, ok := p[username]; ok { + return p, nil + } + return PermissionNone, nil +} diff --git a/server/internal/core/fs/resource_delete.go b/server/internal/core/fs/resource_delete.go index fbf8fd4a..a4a672aa 100644 --- a/server/internal/core/fs/resource_delete.go +++ b/server/internal/core/fs/resource_delete.go @@ -18,7 +18,7 @@ func (r Resource) DeleteChildRecursive(name string) (Resource, error) { return Resource{}, ErrInsufficientPermissions } - c, err := r.f.ChildResourceByName(r.ID(), name) + c, err := r.f.childResourceByName(r.ID(), name) if err != nil { if errors.Is(err, pgx.ErrNoRows) { err = ErrResourceNotFound @@ -90,14 +90,14 @@ func (r Resource) RestoreDeleted(parentPathOrUUID string, name string, autoRenam if name == "" { name = r.name } - _, err = r.f.ChildResourceByName(p.id, name) + _, err = r.f.childResourceByName(p.id, name) if autoRename && err == nil { ext := path.Ext(name) basename := name[:len(name)-len(ext)] counter := 1 for { name = fmt.Sprintf("%s (%d)%s", basename, counter, ext) - if _, err = r.f.ChildResourceByName(p.id, name); err == nil { + if _, err = r.f.childResourceByName(p.id, name); err == nil { counter++ } else { break diff --git a/server/internal/core/fs/sql_common.go b/server/internal/core/fs/sql_common.go index ce0d96a0..490da877 100644 --- a/server/internal/core/fs/sql_common.go +++ b/server/internal/core/fs/sql_common.go @@ -1,7 +1,6 @@ package fs import ( - "encoding/json" "slices" "strings" @@ -70,21 +69,6 @@ func selectDirectChildren(id uuid.UUID, deleted pgtype.Timestamp, includeDeleted return r, q } -func readPermissionFromJson(j []byte, username string) (Permission, error) { - if j == nil { - return PermissionNone, nil - } - p := make(map[string]Permission) - err := json.Unmarshal(j, &p) - if err != nil { - return PermissionNone, err - } - if p, ok := p[username]; ok { - return p, nil - } - return PermissionNone, nil -} - func collectNonDirResourceIDs(rows pgx.Rows, e error) (total int, ids []uuid.UUID, err error) { if e != nil { return 0, nil, e