mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-05-04 19:30:24 -05:00
240 lines
7.3 KiB
Go
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
|
|
}
|