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(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 } }