Files
phylum/server/internal/core/fs/create.go
2025-05-16 10:37:07 +05:30

271 lines
6.7 KiB
Go

package fs
import (
"errors"
"fmt"
"path"
"strings"
"github.com/doug-martin/goqu/v9"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const emptyContentType = "text/plain"
const emptyContentSHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
func (f filesystem) CreateResourceByPath(path string, id uuid.UUID, dir, createParents bool, conflictResolution ResourceBindConflictResolution) (Resource, error) {
if !createParents {
name, parent, err := f.targetNameParentByPathWithRoot(path, Resource{})
if err != nil {
if errors.Is(err, ErrResourceNotFound) {
err = ErrParentNotFound
}
return Resource{}, err
}
return parent.createMemberResource(name, id, dir, conflictResolution)
}
root, path, err := parseUUIDPrefix(path)
if err != nil {
return Resource{}, ErrResourcePathInvalid
}
if root.Valid {
f = f.withPathRoot(root)
}
segments := strings.Split(strings.TrimRight(strings.TrimLeft(path, "/"), "/"), "/")
r, err := f.ResourceByID(f.pathRoot.Bytes)
for i, s := range segments {
if err != nil {
return Resource{}, err
}
d := true
resourceID, _ := uuid.NewV7()
conflict := ResourceBindConflictResolutionEnsure
if i == len(segments)-1 {
d = dir
if id != uuid.Nil {
resourceID = id
}
conflict = conflictResolution
}
r, err = r.createMemberResource(s, resourceID, d, conflict)
}
return r, err
}
func (r Resource) createMemberResource(name string, id uuid.UUID, dir bool, conflictResolution ResourceBindConflictResolution) (Resource, error) {
if r.deleted.Valid {
return Resource{}, ErrResourceDeleted
}
if !r.Dir() {
return Resource{}, ErrResourceNotCollection
}
if !r.hasPermission(PermissionWrite) {
return Resource{}, ErrInsufficientPermissions
}
if CheckNameInvalid(name) {
return Resource{}, ErrResourceNameInvalid
}
if id == uuid.Nil {
id, _ = uuid.NewV7()
}
var res Resource
var created bool
err := r.f.runInTx(func(f filesystem) error {
var err error
contentType := emptyContentType
contentSHA256 := emptyContentSHA256
if r.dir {
contentType = ""
contentSHA256 = ""
}
if res, created, _, err = f.createResource(id, r.id, name, dir, 0, contentType, contentSHA256, r.permissions, conflictResolution); err != nil {
if strings.Contains(err.Error(), "unique_member_resource_name") {
return ErrResourceNameConflict
}
return err
} else if created {
if err := f.recomputePermissions(r.id); err != nil {
return err
}
return f.updateResourceModified(r.id)
}
return nil
})
if err == ErrResourceIDConflict {
return r.f.ResourceByID(id)
}
if err != nil {
return Resource{}, err
}
if !r.Dir() {
out, err := r.f.cs.OpenWrite(id, nil, nil)
if err == nil {
err = out.Close()
}
if err != nil {
return Resource{}, err
}
}
res.f = r.f
res.userPermission = r.userPermission
return res, nil
}
func (f filesystem) createResource(
id uuid.UUID,
parent uuid.UUID,
name string,
dir bool,
contentLength int,
contentType string,
contentSHA256 string,
permissions []byte,
conflictResolution ResourceBindConflictResolution,
) (res Resource, created, deleted bool, err error) {
err = f.runInTx(func(f filesystem) error {
res, err = f.insertResource(
id,
parent,
name,
dir,
contentLength,
contentType,
contentSHA256,
permissions,
)
return err
})
if err == nil {
created = true
return
}
if strings.Contains(err.Error(), "unique_member_resource_name") {
switch conflictResolution {
case ResourceBindConflictResolutionError:
err = ErrResourceNameConflict
case ResourceBindConflictResolutionEnsure:
res, err = f.childResourceByName(parent, name)
if err == nil && res.dir != dir {
err = ErrResourceNameConflict
}
case ResourceBindConflictResolutionRename:
ext := path.Ext(name)
basename := name[:len(name)-len(ext)]
counter := 1
for {
name := fmt.Sprintf("%s (%d)%s", basename, counter, ext)
err = f.runInTx(func(f filesystem) error {
res, err = f.insertResource(
id,
parent,
name,
dir,
contentLength,
contentType,
contentSHA256,
permissions,
)
return err
})
if err != nil {
if !strings.Contains(err.Error(), "unique_member_resource_name") {
return
}
counter++
} else {
created = true
return
}
}
case ResourceBindConflictResolutionOverwrite:
res, err = f.childResourceByName(parent, name)
if err == nil {
deleted = true
if res.dir == dir {
if dir {
err = f.deleteRecursive(res.id, parent, true, true)
} else {
err = f.updateResourceContents(
res.id,
contentLength,
contentType,
contentSHA256,
)
res.contentLength = contentLength
res.contentType = contentType
res.contentSHA256 = contentSHA256
}
} else {
err = f.deleteRecursive(res.id, parent, true, false)
if err == nil {
res, created, _, err = f.createResource(
id,
parent,
name,
dir,
contentLength,
contentType,
contentSHA256,
permissions,
ResourceBindConflictResolutionError,
)
}
}
}
case ResourceBindConflictResolutionDelete:
res, err = f.childResourceByName(parent, name)
if err == nil {
deleted = true
err = f.deleteRecursive(res.id, parent, true, false)
if err == nil {
res, created, _, err = f.createResource(
id,
parent,
name,
dir,
contentLength,
contentType,
contentSHA256,
permissions,
ResourceBindConflictResolutionError,
)
}
}
}
} else if strings.Contains(err.Error(), "resources_pkey") {
// TODO: maybe the request already succeeded in the previous attempt but the client didn't receive the response?
err = ErrResourceIDConflict
}
return
}
func (f filesystem) insertResource(id, parent uuid.UUID, name string, dir bool, contentLength int, contentType, contentSha256 string, permissions []byte) (Resource, error) {
query, args, _ := pg.From("resources").
Insert().
Rows(goqu.Record{
"id": goqu.V(id),
"parent": goqu.V(parent),
"name": goqu.V(name),
"dir": goqu.V(dir),
"content_length": goqu.V(contentLength),
"content_type": goqu.V(contentType),
"content_sha256": goqu.V(contentSha256),
"permissions": goqu.V(permissions),
}).
Returning("*", goqu.L("'[]'::JSONB"), goqu.L("NULL"), goqu.L("'{}'::JSONB")).
ToSQL()
if rows, err := f.db.Query(query, args...); err != nil {
return Resource{}, err
} else {
r, err := f.collectFullResource(rows)
r.parentID = pgtype.UUID{Bytes: parent, Valid: true}
r.visibleParent = r.parentID
r.inheritedPermissions = permissions
return r, err
}
}