Files
phylum/server/internal/core/resource_locate.go
T

240 lines
7.3 KiB
Go

package core
import (
"errors"
"fmt"
"path"
"strings"
"codeberg.org/shroff/phylum/server/internal/db"
"github.com/doug-martin/goqu/v9"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
func (f *FileSystem) ResourceByID(id uuid.UUID) (Resource, error) {
if r, err := resourceByID(f.db, id, f.user.ID); err != nil {
return Resource{}, err
} else if err := r.checkPermission(f.user, PermissionRead); err != nil {
return Resource{}, err
} else {
return r, nil
}
}
func resourceByID(db db.Handler, id uuid.UUID, userID int32) (Resource, error) {
query := fullResourceQuery + "\nWHERE r.id = @id::UUID"
args := pgx.NamedArgs{
"user_id": userID,
"id": id,
}
if rows, err := db.Query(query, args); err != nil {
return Resource{}, err
} else {
return collectFullResource(rows)
}
}
// ResourceByPathWithRoot locates the resource at [path]. [path] may begin with
// a "<uuid>:" prefix, which will traverse the path relative to that root
// instead of this [FileSystem]'s default [PathRoot].
//
// An empty path or "/" will return the root resource.
//
// A permission check will be performed to ensure that the [FileSystem]'s [User]
// has read permissions on the resource. If not, then [ErrResourceNotFound] will
// be returned.
func (f *FileSystem) ResourceByPathWithRoot(path string) (Resource, error) {
parsedID, path, err := parseUUIDPrefix(path)
if err != nil {
return Resource{}, ErrResourceNotFound
}
id := f.pathRoot.Bytes
if parsedID.Valid {
id = parsedID.Bytes
} else if !f.pathRoot.Valid {
return Resource{}, ErrResourceNotFound
}
return resourceByPath(f.db, id, path, f.user)
}
// ResourceByPath locates the resource at [path].
//
// An empty path or "/" will return the root resource.
//
// A permission check will be performed to ensure that the [FileSystem]'s [User]
// has read permissions on the resource. If not, then [ErrResourceNotFound] will
// be returned.
func (f *FileSystem) ResourceByPath(path string) (Resource, error) {
if !f.pathRoot.Valid {
return Resource{}, ErrResourceNotFound
}
return resourceByPath(f.db, f.pathRoot.Bytes, path, f.user)
}
func resourceByPath(db db.Handler, root uuid.UUID, path string, user User) (Resource, error) {
nodes := goqu.T("nodes").As("n")
r := goqu.T("resources").As("r")
p := goqu.T("resources").As("p")
sub := pg.
Select(r.Col("id"), r.Col("parent"), nodes.Col("search"), goqu.L("n.depth + 1")).
From(r).
Join(nodes, goqu.On(r.Col("parent").Eq(nodes.Col("id")))).
Where(
r.Col("deleted").IsNull(),
r.Col("name").Eq(goqu.L("n.search[n.depth + 1]")),
)
rec := pg.
Select(r.Col("id"), r.Col("parent"), goqu.L("array_remove(string_to_array(?::TEXT, '/', NULL), '')", path), goqu.L("0")).
From(r).
Where(r.Col("id").Eq(goqu.V(root))).
UnionAll(sub)
l := goqu.T("publinks").As("l")
v := goqu.T("resource_versions").As("v")
q := pg.Select(r.All(),
pg.Select(goqu.L(versionsQuery)).From(v).Where(v.Col("resource_id").Eq(r.Col("id"))),
pg.Select(goqu.L(publinksQuery)).From(l).Where(l.Col("root").Eq(r.Col("id"))),
pg.Select(goqu.L("CASE WHEN COALESCE(p.permissions[?::INT]::INTEGER, 0) <> 0 THEN p.id ELSE NULL END AS visible_parent", user.ID)),
pg.Select(goqu.L("COALESCE(p.permissions, '{}'::JSONB)")),
).
From(r).
LeftJoin(goqu.T("resources").As("p"), goqu.On(p.Col("id").Eq(r.Col("parent")))).
WithRecursive("nodes(id, parent, search, depth)", rec).
Join(nodes, goqu.On(r.Col("id").Eq(nodes.Col("id")))).
Where(goqu.L("cardinality(n.search) = n.depth"))
if !user.hasPermission(PermissionFilesAll) {
q = q.Where(goqu.L("r.permissions[?::INT]::INTEGER <> 0", user.ID))
}
query, args, _ := q.ToSQL()
if rows, err := db.Query(query, args...); err != nil {
return Resource{}, err
} else {
return collectFullResource(rows)
}
// } else if r, err := collectFullResource(rows); err != nil {
// return Resource{}, err
// } else if err := r.checkPermission(f.user, PermissionRead); err != nil {
// return Resource{}, err
// } else {
// return r, nil
// }
}
// targetByPathWithRoot extracts the [name] and [parent] resource of from
// the given [path].
//
// Uses the last segment of [path] as the [name] and traverses the previous
// segments to locate [parent]. If [path] has no segments then [src].[name]
// is used as the [name] instead.
//
// If [path] begins with a "<uuid>:", then the path traversed is relative to
// that root.
//
// If [path] does not begin with a "<uuid>:" prefix, and is absolute (begins
// with '/') then the path traversed is relative to [f].[pathRoot]
//
// If [path] does not begin with a "<uuid>:" prefix, and is not absolute (does
// not begin with '/') then the path traversed is relative to [src]
func (f *FileSystem) targetByPathWithRoot(path string, src Resource) (name string, parent Resource, err error) {
id, path, err := parseUUIDPrefix(path)
if err != nil {
return "", Resource{}, err
}
if id.Valid {
f = f.withPathRoot(id)
} else if len(path) == 0 {
return "", Resource{}, ErrResourceNameInvalid
} else if path[0] != '/' && src.id != uuid.Nil {
f = f.withPathRoot(pgtype.UUID{Bytes: src.id, Valid: true})
}
path = strings.TrimRight(path, "/")
i := strings.LastIndex(path, "/")
parentPath := ""
if i > 0 {
parentPath = path[:i]
}
parent, err = f.ResourceByPath(parentPath)
if err != nil {
if errors.Is(err, ErrResourceNotFound) {
err = ErrParentNotFound
}
return "", Resource{}, err
}
name = path[i+1:]
if name == "" {
name = src.name
}
return name, parent, nil
}
// detectNameConflict checks if there already exists a [Resource] with parent
// [parentID] and name [name]. If there isn't then it simply returns [name]
//
// If there is a conflict and [autoRename] is `false`, then it returns
// [ErrResourceNameConflict], along with the name passed in.
//
// If there is a conflict and [autoRename] is `true` then it returns a name with
// the first available numbered suffix.
func detectNameConflict(db db.Handler, parentID uuid.UUID, name string, autoRename bool) (string, error) {
if _, _, err := childResourceIDByName(db, parentID, name); err != nil {
// No name conflict. Good to go!
if errors.Is(err, ErrResourceNotFound) {
return name, nil
}
return "", err
} else if !autoRename {
return name, ErrResourceNameConflict
}
ext := path.Ext(name)
basename := name[:len(name)-len(ext)]
counter := 1
for {
name = fmt.Sprintf("%s (%d)%s", basename, counter, ext)
if _, _, err := childResourceIDByName(db, parentID, name); err == nil {
counter++
} else if errors.Is(err, ErrResourceNotFound) {
return name, nil
} else {
return "", err
}
}
}
func childResourceIDByName(db db.Handler, parentID uuid.UUID, name string) (uuid.UUID, bool, error) {
const query = "SELECT id, dir FROM resources WHERE parent = @parent::UUID AND name = @name::TEXT AND deleted IS NULL"
args := pgx.NamedArgs{
"parent": parentID,
"name": name,
}
row := db.QueryRow(query, args)
var id uuid.UUID
var dir bool
err := row.Scan(&id, &dir)
if Is(err, pgx.ErrNoRows) {
err = ErrResourceNotFound
}
return id, dir, err
}
func parseUUIDPrefix(path string) (pgtype.UUID, string, error) {
i := strings.Index(path, ":")
si := strings.Index(path, "/")
// Ignore ':' after '/'
if i >= 0 && (si < 0 || si > i) {
id, err := uuid.Parse(path[:i])
return pgtype.UUID{Bytes: id, Valid: true}, path[i+1:], err
}
return pgtype.UUID{}, path, nil
}