mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-02-19 03:58:46 -06:00
619 lines
16 KiB
Go
619 lines
16 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"errors"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/shroff/phylum/server/internal/db"
|
|
"github.com/shroff/phylum/server/internal/storage"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type DiskUsageInfo struct {
|
|
TotalSize int64
|
|
Entities int64
|
|
Files int64
|
|
Dirs int64
|
|
}
|
|
|
|
type FileSystem interface {
|
|
WithDb(db *db.DbHandler) FileSystem
|
|
RunInTx(fn func(FileSystem) error) error
|
|
RootID() uuid.UUID
|
|
ResourceByID(id uuid.UUID) (Resource, error)
|
|
ResourceByPath(path string) (Resource, error)
|
|
CreateResourceByPath(path string, dir bool) (Resource, error)
|
|
ResourceByPathOrUuid(pathOrUuid string) (Resource, error)
|
|
OpenRead(r Resource, start, length int64) (io.ReadCloser, error)
|
|
OpenWrite(r Resource) (io.WriteCloser, error)
|
|
ReadDir(r Resource) ([]Resource, error)
|
|
CreateMemberResource(r Resource, id uuid.UUID, name string, dir bool) (Resource, error)
|
|
DeleteChildRecursive(r Resource, name string, hardDelete bool) error
|
|
DeleteRecursive(r Resource, hardDelete bool) (uuid.UUIDs, error)
|
|
Move(r Resource, target string, overwrite bool) (Resource, error)
|
|
Copy(r Resource, target string, id uuid.UUID, recursive, overwrite bool) (Resource, error)
|
|
UpdatePermissions(r Resource, username string, permission Permission) error
|
|
DiskUsage(r Resource) (DiskUsageInfo, error)
|
|
}
|
|
|
|
type filesystem struct {
|
|
db *db.DbHandler
|
|
ctx context.Context
|
|
cs storage.Storage
|
|
rootID uuid.UUID
|
|
username string
|
|
}
|
|
|
|
func OpenFileSystem(dbh *db.DbHandler, ctx context.Context, cs storage.Storage, username string, rootID uuid.UUID) FileSystem {
|
|
return filesystem{
|
|
db: dbh,
|
|
ctx: ctx,
|
|
cs: cs,
|
|
rootID: rootID,
|
|
username: username,
|
|
}
|
|
}
|
|
|
|
func (f filesystem) RootID() uuid.UUID {
|
|
return f.rootID
|
|
}
|
|
|
|
func (f filesystem) WithDb(db *db.DbHandler) FileSystem {
|
|
return f.withDb(db)
|
|
}
|
|
func (f filesystem) withDb(db *db.DbHandler) filesystem {
|
|
return filesystem{
|
|
db: db,
|
|
ctx: f.ctx,
|
|
cs: f.cs,
|
|
rootID: f.rootID,
|
|
username: f.username,
|
|
}
|
|
}
|
|
|
|
func (f filesystem) RunInTx(fn func(FileSystem) error) error {
|
|
return f.db.WithTx(f.ctx, func(db *db.DbHandler) error {
|
|
return fn(f.WithDb(db))
|
|
})
|
|
}
|
|
|
|
func (f filesystem) ResourceByPath(path string) (Resource, error) {
|
|
path = strings.Trim(path, "/")
|
|
segments := strings.Split(path, "/")
|
|
if path == "" {
|
|
// Calling strings.Split on an empty string returns a slice of length 1. That breaks the query
|
|
segments = []string{}
|
|
}
|
|
|
|
res, err := f.db.ResourceByPath(f.ctx, db.ResourceByPathParams{Root: f.rootID, Search: segments})
|
|
if err == pgx.ErrNoRows {
|
|
err = ErrResourceNotFound
|
|
}
|
|
if err != nil {
|
|
return Resource{}, err
|
|
}
|
|
return f.ResourceByID(res.ID)
|
|
}
|
|
|
|
func (f filesystem) ResourceByID(id uuid.UUID) (Resource, error) {
|
|
r, err := f.db.ResourceByID(f.ctx, db.ResourceByIDParams{Root: f.rootID, ResourceID: id, Username: f.username})
|
|
// TODO: verify found
|
|
if err == pgx.ErrNoRows || !r.Found || r.UserPermission == 0 {
|
|
err = ErrResourceNotFound
|
|
}
|
|
if err != nil {
|
|
return Resource{}, err
|
|
}
|
|
|
|
var delTime *time.Time
|
|
if r.Deleted.Valid {
|
|
delTime = &r.Deleted.Time
|
|
}
|
|
return Resource{
|
|
ID: r.ID,
|
|
ParentID: r.Parent,
|
|
Name: r.Name,
|
|
Dir: r.Dir,
|
|
Created: r.Created.Time,
|
|
Modified: r.Modified.Time,
|
|
Deleted: delTime,
|
|
ContentSize: r.ContentSize,
|
|
ContentType: r.ContentType,
|
|
ContentSHA256: r.ContentSha256,
|
|
Permissions: string(r.Permissions),
|
|
// Definitely Needed
|
|
UserPermissions: r.UserPermission,
|
|
InheritedPermissions: string(r.InheritedPermissions),
|
|
}, nil
|
|
}
|
|
|
|
func (f filesystem) ResourceByPathOrUuid(pathOrUuid string) (Resource, error) {
|
|
if pathOrUuid[0] == '/' {
|
|
return f.ResourceByPath(pathOrUuid)
|
|
}
|
|
if id, err := uuid.Parse(pathOrUuid); err != nil {
|
|
return Resource{}, err
|
|
} else {
|
|
return f.ResourceByID(id)
|
|
}
|
|
}
|
|
|
|
func (f filesystem) targetByPathOrUuid(src Resource, pathOrUuid string) (Resource, string, error) {
|
|
if pathOrUuid[0] == '/' {
|
|
path := strings.TrimRight(pathOrUuid, "/")
|
|
if dest, err := f.ResourceByPath(path); err == nil {
|
|
return dest, src.Name, nil
|
|
} else {
|
|
index := strings.LastIndex(path, "/")
|
|
name := path[index+1:]
|
|
path = path[0:index]
|
|
if parent, err := f.ResourceByPath(path); err != nil {
|
|
if errors.Is(err, ErrResourceNotFound) {
|
|
err = ErrParentNotFound
|
|
}
|
|
return Resource{}, "", err
|
|
} else if checkNameInvalid(name) {
|
|
return Resource{}, "", ErrResourceNameInvalid
|
|
} else {
|
|
return parent, name, nil
|
|
}
|
|
}
|
|
}
|
|
index := strings.Index(pathOrUuid, "/")
|
|
if index == -1 {
|
|
return src, pathOrUuid, nil
|
|
}
|
|
if id, err := uuid.Parse(pathOrUuid[0:index]); err != nil {
|
|
return Resource{}, "", err
|
|
} else if parent, err := f.ResourceByID(id); err != nil {
|
|
if errors.Is(err, ErrResourceNotFound) {
|
|
err = ErrParentNotFound
|
|
}
|
|
return Resource{}, "", err
|
|
} else {
|
|
name := pathOrUuid[index+1:]
|
|
if name == "" {
|
|
name = src.Name
|
|
}
|
|
if checkNameInvalid(name) {
|
|
return Resource{}, "", ErrResourceNameInvalid
|
|
}
|
|
return parent, name, nil
|
|
}
|
|
}
|
|
|
|
func (f filesystem) OpenRead(r Resource, start, length int64) (io.ReadCloser, error) {
|
|
if r.UserPermissions < PermissionReadOnly {
|
|
return nil, ErrInsufficientPermissions
|
|
}
|
|
return f.cs.OpenRead(r.ID, start, length)
|
|
}
|
|
|
|
func (f filesystem) OpenWrite(r Resource) (io.WriteCloser, error) {
|
|
if r.UserPermissions < PermissionReadWrite {
|
|
return nil, ErrInsufficientPermissions
|
|
}
|
|
return f.cs.OpenWrite(r.ID, sha256.New, func(len int, sum, mime string) error {
|
|
return f.db.UpdateResourceContents(f.ctx, db.UpdateResourceContentsParams{
|
|
ID: r.ID,
|
|
ContentType: mime,
|
|
ContentSize: int64(len),
|
|
ContentSha256: sum,
|
|
})
|
|
})
|
|
}
|
|
|
|
func (f filesystem) ReadDir(r Resource) ([]Resource, error) {
|
|
if r.UserPermissions < PermissionReadOnly {
|
|
return nil, ErrInsufficientPermissions
|
|
}
|
|
if !r.Dir {
|
|
return nil, ErrResourceNotCollection
|
|
}
|
|
children, err := f.db.ReadDir(f.ctx, db.ReadDirParams{
|
|
ID: r.ID,
|
|
IncludeRoot: false,
|
|
Recursive: false,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]Resource, len(children))
|
|
for i, c := range children {
|
|
result[i] = Resource{
|
|
ID: c.ID,
|
|
ParentID: c.Parent,
|
|
Name: c.Name,
|
|
Created: c.Created.Time,
|
|
Modified: c.Modified.Time,
|
|
Deleted: nil, // Query will not return deleted results
|
|
Dir: c.Dir,
|
|
ContentSize: c.ContentSize,
|
|
ContentType: c.ContentType,
|
|
ContentSHA256: c.ContentSha256,
|
|
Permissions: string(c.Permissions),
|
|
// Not Needed
|
|
// UserPermissions: 0,
|
|
// InheritedPermissions: "",
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
func (f filesystem) CreateResourceByPath(path string, dir bool) (Resource, error) {
|
|
path = strings.TrimRight(path, "/")
|
|
index := strings.LastIndex(path, "/")
|
|
name := path[index+1:]
|
|
parentPath := path[0:index]
|
|
parent, err := f.ResourceByPath(parentPath)
|
|
if err != nil {
|
|
if errors.Is(err, ErrResourceNotFound) {
|
|
err = ErrParentNotFound
|
|
}
|
|
return Resource{}, err
|
|
}
|
|
return f.CreateMemberResource(parent, uuid.New(), name, true)
|
|
}
|
|
|
|
func (f filesystem) CreateMemberResource(r Resource, id uuid.UUID, name string, dir bool) (Resource, error) {
|
|
if !r.Dir {
|
|
return Resource{}, ErrResourceNotCollection
|
|
}
|
|
if r.UserPermissions < PermissionReadWrite {
|
|
return Resource{}, ErrInsufficientPermissions
|
|
}
|
|
if name == "" || checkNameInvalid(name) {
|
|
return Resource{}, ErrResourceNameInvalid
|
|
}
|
|
var result db.Resource
|
|
err := f.db.WithTx(f.ctx, func(d *db.DbHandler) error {
|
|
var err error
|
|
parent := r.ID
|
|
if result, err = d.CreateResource(f.ctx, db.CreateResourceParams{ID: id, Parent: &parent, Name: name, Dir: dir}); err != nil {
|
|
if strings.Contains(err.Error(), "unique_member_resource_name") {
|
|
return ErrResourceNameConflict
|
|
}
|
|
if strings.Contains(err.Error(), "resources_pkey") {
|
|
return ErrResourceIDConflict
|
|
}
|
|
return err
|
|
}
|
|
return d.UpdateResourceModified(f.ctx, r.ID)
|
|
})
|
|
if err != nil {
|
|
return Resource{}, err
|
|
}
|
|
return Resource{
|
|
ID: result.ID,
|
|
ParentID: result.Parent,
|
|
Name: result.Name,
|
|
Dir: result.Dir,
|
|
Created: result.Created.Time,
|
|
Modified: result.Modified.Time,
|
|
Deleted: nil, // Cannot be deleted when created
|
|
ContentType: result.ContentType,
|
|
ContentSize: result.ContentSize,
|
|
ContentSHA256: result.ContentSha256,
|
|
Permissions: string(result.Permissions),
|
|
UserPermissions: r.UserPermissions,
|
|
// Not Needed
|
|
// InheritedPermissions: "",
|
|
}, nil
|
|
}
|
|
|
|
func (f filesystem) DeleteChildRecursive(r Resource, name string, hardDelete bool) error {
|
|
if result, err := f.db.ChildResourceByName(f.ctx, db.ChildResourceByNameParams{Parent: r.ID, Name: name}); err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ErrResourceNotFound
|
|
}
|
|
return err
|
|
} else {
|
|
child := Resource{
|
|
ID: result.ID,
|
|
ParentID: result.Parent,
|
|
Name: result.Name,
|
|
Dir: result.Dir,
|
|
Created: result.Created.Time,
|
|
Modified: result.Modified.Time,
|
|
Deleted: nil,
|
|
ContentType: result.ContentType,
|
|
ContentSize: result.ContentSize,
|
|
ContentSHA256: result.ContentSha256,
|
|
Permissions: string(result.Permissions),
|
|
UserPermissions: r.UserPermissions,
|
|
// Not Needed
|
|
// InheritedPermissions: "",
|
|
}
|
|
_, err := f.DeleteRecursive(child, hardDelete)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (f filesystem) DeleteRecursive(r Resource, hardDelete bool) (uuid.UUIDs, error) {
|
|
if r.UserPermissions < PermissionReadWrite {
|
|
return nil, ErrInsufficientPermissions
|
|
}
|
|
// TODO: versioning
|
|
var ids uuid.UUIDs
|
|
err := f.db.WithTx(f.ctx, func(d *db.DbHandler) error {
|
|
var err error
|
|
if hardDelete {
|
|
ids, err = d.HardDeleteRecursive(f.ctx, r.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
errors := f.cs.DeleteAll(ids)
|
|
for err := range errors {
|
|
logrus.Warn(err)
|
|
}
|
|
} else {
|
|
if ids, err = d.DeleteRecursive(f.ctx, r.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
parent := r.ParentID
|
|
if parent != nil {
|
|
return d.UpdateResourceModified(f.ctx, *parent)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ids, nil
|
|
}
|
|
|
|
func (f filesystem) UpdateName(r Resource, name string) (Resource, error) {
|
|
if r, err := f.db.UpdateResourceName(f.ctx, db.UpdateResourceNameParams{ID: r.ID, Name: name}); err != nil {
|
|
return Resource{}, err
|
|
} else {
|
|
var deleted *time.Time
|
|
if r.Deleted.Valid {
|
|
deleted = &r.Deleted.Time
|
|
}
|
|
return Resource{
|
|
ID: r.ID,
|
|
ParentID: r.Parent,
|
|
Name: r.Name,
|
|
Created: r.Created.Time,
|
|
Modified: r.Modified.Time,
|
|
Deleted: deleted,
|
|
Dir: r.Dir,
|
|
ContentSize: r.ContentSize,
|
|
ContentType: r.ContentType,
|
|
ContentSHA256: r.ContentSha256,
|
|
Permissions: string(r.Permissions),
|
|
// Not Needed
|
|
// UserPermissions: 0,
|
|
// InheritedPermissions: "",
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (f filesystem) Move(r Resource, target string, overwrite bool) (Resource, error) {
|
|
if r.ParentID == nil {
|
|
return Resource{}, ErrInsufficientPermissions
|
|
}
|
|
|
|
// Check source directory permissions
|
|
parent, err := f.ResourceByID(*r.ParentID)
|
|
if err != nil {
|
|
return Resource{}, err
|
|
}
|
|
if parent.UserPermissions < PermissionReadWrite {
|
|
return Resource{}, ErrInsufficientPermissions
|
|
}
|
|
|
|
// Check destParent directory permissions (if applicable)
|
|
destParent, destName, err := f.targetByPathOrUuid(r, target)
|
|
if err != nil {
|
|
if errors.Is(err, ErrResourceNotFound) {
|
|
err = ErrParentNotFound
|
|
}
|
|
return Resource{}, err
|
|
}
|
|
if destParent.UserPermissions < PermissionReadWrite {
|
|
return Resource{}, ErrInsufficientPermissions
|
|
}
|
|
if res, err := f.db.ResourceByID(f.ctx, db.ResourceByIDParams{Root: r.ID, ResourceID: destParent.ID, Username: f.username}); err != nil || res.Found {
|
|
return Resource{}, ErrResourceMoveTargetSubdirectory
|
|
}
|
|
result := Resource{}
|
|
|
|
return result, f.db.WithTx(f.ctx, func(dbh *db.DbHandler) error {
|
|
f = f.withDb(dbh)
|
|
if overwrite {
|
|
f.DeleteChildRecursive(destParent, destName, false)
|
|
}
|
|
if r, err := f.db.UpdateResourceNameParent(f.ctx, db.UpdateResourceNameParentParams{ID: r.ID, Name: destName, Parent: destParent.ID}); err != nil {
|
|
if strings.Contains(err.Error(), "unique_member_resource_name") {
|
|
return ErrResourceNameConflict
|
|
}
|
|
return err
|
|
} else {
|
|
var deleted *time.Time
|
|
if r.Deleted.Valid {
|
|
deleted = &r.Deleted.Time
|
|
}
|
|
result = Resource{
|
|
ID: r.ID,
|
|
ParentID: r.Parent,
|
|
Name: r.Name,
|
|
Created: r.Created.Time,
|
|
Modified: r.Modified.Time,
|
|
Deleted: deleted,
|
|
Dir: r.Dir,
|
|
ContentSize: r.ContentSize,
|
|
ContentType: r.ContentType,
|
|
ContentSHA256: r.ContentSha256,
|
|
Permissions: string(r.Permissions),
|
|
// Not Needed
|
|
// UserPermissions: 0,
|
|
// InheritedPermissions: "",
|
|
}
|
|
return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
func (f filesystem) Copy(src Resource, target string, id uuid.UUID, recursive, overwrite bool) (dest Resource, e error) {
|
|
// Check source directory permissions
|
|
if src.UserPermissions < PermissionReadOnly {
|
|
e = ErrInsufficientPermissions
|
|
return
|
|
}
|
|
|
|
// Check dest directory permissions
|
|
destParent, destName, err := f.targetByPathOrUuid(src, target)
|
|
if err != nil {
|
|
if errors.Is(err, ErrResourceNotFound) {
|
|
err = ErrParentNotFound
|
|
}
|
|
return Resource{}, err
|
|
}
|
|
if destParent.UserPermissions < PermissionReadWrite {
|
|
e = ErrInsufficientPermissions
|
|
return
|
|
}
|
|
|
|
var tree []db.ReadDirRow
|
|
if recursive && src.Dir {
|
|
var err error
|
|
tree, err = f.db.ReadDir(f.ctx, db.ReadDirParams{ID: src.ID, IncludeRoot: false, Recursive: true})
|
|
if err != nil {
|
|
return Resource{}, err
|
|
}
|
|
}
|
|
create := make([]db.CreateResourcesParams, 0, len(tree))
|
|
copy := make(map[uuid.UUID]uuid.UUID)
|
|
ids := make(map[string]uuid.UUID)
|
|
if src.Dir {
|
|
ids[""] = id
|
|
} else {
|
|
copy[src.ID] = id
|
|
}
|
|
|
|
for _, src := range tree {
|
|
id := uuid.New()
|
|
parent := ids[src.Path[0:strings.LastIndex(src.Path, "/")]]
|
|
|
|
create = append(create, db.CreateResourcesParams{
|
|
ID: id,
|
|
Parent: parent,
|
|
Name: src.Name,
|
|
Dir: src.Dir,
|
|
ContentType: src.ContentType,
|
|
ContentSha256: src.ContentSha256,
|
|
})
|
|
|
|
if src.Dir {
|
|
ids[src.Path] = id
|
|
} else {
|
|
copy[src.ID] = id
|
|
}
|
|
}
|
|
|
|
e = f.db.WithTx(f.ctx, func(dbh *db.DbHandler) error {
|
|
f := f.withDb(dbh)
|
|
if overwrite {
|
|
if err := f.DeleteChildRecursive(destParent, destName, false); err != nil && !errors.Is(err, ErrResourceNotFound) {
|
|
return err
|
|
}
|
|
}
|
|
dest, err = f.CreateMemberResource(destParent, id, destName, src.Dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err := dbh.CreateResources(f.ctx, create)
|
|
return err
|
|
})
|
|
|
|
if e == nil {
|
|
func() {
|
|
for k, v := range copy {
|
|
if _, err := f.copyContents(k, v); err != nil {
|
|
logrus.Warn("unable to copy " + k.String() + " to " + v.String() + ": " + err.Error())
|
|
}
|
|
}
|
|
|
|
}()
|
|
} else {
|
|
logrus.Warn(e)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (f filesystem) copyContents(k, v uuid.UUID) (int64, error) {
|
|
src, err := f.ResourceByID(k)
|
|
if err != nil {
|
|
return 0, errors.New("unable to get " + k.String() + ": " + err.Error())
|
|
}
|
|
dest, err := f.ResourceByID(v)
|
|
if err != nil {
|
|
return 0, errors.New("unable to get " + v.String() + ": " + err.Error())
|
|
}
|
|
in, err := f.OpenRead(src, 0, -1)
|
|
if err != nil {
|
|
return 0, errors.New("Unable to open " + k.String() + ": " + err.Error())
|
|
}
|
|
defer in.Close()
|
|
out, err := f.OpenWrite(dest)
|
|
if err != nil {
|
|
return 0, errors.New("Unable to open " + v.String() + ": " + err.Error())
|
|
}
|
|
defer out.Close()
|
|
|
|
return io.Copy(out, in)
|
|
}
|
|
|
|
func (f filesystem) DiskUsage(r Resource) (DiskUsageInfo, error) {
|
|
if info, err := f.db.DiskUsage(f.ctx, r.ID); err != nil {
|
|
return DiskUsageInfo{}, err
|
|
} else {
|
|
return DiskUsageInfo{
|
|
TotalSize: info.Size,
|
|
Entities: info.Entities,
|
|
Files: info.Files,
|
|
Dirs: info.Dirs,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (f filesystem) UpdatePermissions(r Resource, username string, permission Permission) error {
|
|
if r.UserPermissions < PermissionReadWriteShare {
|
|
return ErrInsufficientPermissions
|
|
}
|
|
if permission > PermissionReadWriteShare {
|
|
permission = PermissionReadWriteShare
|
|
}
|
|
|
|
if permission == 0 {
|
|
return f.db.RemoveUserPermissionForResource(f.ctx, db.RemoveUserPermissionForResourceParams{
|
|
ResourceID: r.ID,
|
|
Username: username,
|
|
})
|
|
}
|
|
return f.db.UpdateUserPermissionsForResource(f.ctx, db.UpdateUserPermissionsForResourceParams{
|
|
ResourceID: r.ID,
|
|
Username: username,
|
|
Permission: permission,
|
|
})
|
|
}
|
|
|
|
func checkNameInvalid(s string) bool {
|
|
return strings.ContainsFunc(s, func(r rune) bool {
|
|
return r == 0 || r == '/'
|
|
})
|
|
}
|