WIP: AFilesystem

This commit is contained in:
Abhishek Shroff
2024-08-04 23:49:29 +05:30
parent 8ad0732a0f
commit ecec4a95cf
17 changed files with 435 additions and 324 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ const errCodeResourceNotCollection = "resource_not_collection"
type resourceResponse struct {
ID uuid.UUID `json:"id"`
Parent *uuid.UUID `json:"parent"`
Parent uuid.UUID `json:"parent"`
Name string `json:"name"`
Dir bool `json:"dir"`
Created time.Time `json:"created"`
+17
View File
@@ -1,8 +1,12 @@
package app
import (
"context"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/app/core"
"github.com/shroff/phylum/server/internal/db"
"github.com/shroff/phylum/server/internal/sql"
)
type App struct {
@@ -19,6 +23,15 @@ func Initialize(db *db.DbHandler, debug bool) error {
return err
}
if _, err := db.Queries().CreateResource(context.Background(), sql.CreateResourceParams{
ID: uuid.UUID{},
Parent: uuid.UUID{},
Name: "root",
Dir: true,
}); err != nil {
return err
}
Default = &App{
Debug: debug,
Db: db,
@@ -27,3 +40,7 @@ func Initialize(db *db.DbHandler, debug bool) error {
return nil
}
func (a App) OpenFileSystem(ctx context.Context, user int32) (core.FileSystem, error) {
return core.OpenFileSystem(a.Db, ctx, nil, user)
}
+252
View File
@@ -0,0 +1,252 @@
package core
import (
"context"
"io"
"io/fs"
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/shroff/phylum/server/internal/db"
"github.com/shroff/phylum/server/internal/sql"
"github.com/sirupsen/logrus"
)
type FileSystem interface {
OpenWithRoot(Resource) FileSystem
ResourceByPath(path string) (Resource, error)
ResourceByID(id uuid.UUID) (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)
DeleteRecursive(r Resource, hardDelete bool) error
UpdateName(r Resource, name string) error
UpdateParent(r Resource, parent uuid.UUID) error
UpdatePermissions(r Resource, userID int32, permission int32) error
}
type filesystem struct {
db *db.DbHandler
ctx context.Context
root Resource
user int32
}
func OpenFileSystem(db *db.DbHandler, ctx context.Context, root Resource, user int32) (FileSystem, error) {
if root == nil {
root = resource{
id: uuid.UUID{},
permission: 0,
parentID: uuid.UUID{},
name: "root",
size: 0,
collection: true,
modTime: time.Time{},
etag: "",
}
}
return filesystem{
db: db,
ctx: ctx,
root: root,
user: user,
}, nil
}
func (f filesystem) OpenWithRoot(root Resource) FileSystem {
return filesystem{
db: f.db,
ctx: f.ctx,
root: root,
user: f.user,
}
}
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.Queries().ResourceByPath(f.ctx, sql.ResourceByPathParams{Root: f.root.ID(), Search: segments, UserID: f.user})
if err != nil {
return nil, fs.ErrNotExist
}
if !res.Permission.Valid || res.Permission.Int32 == 0 {
return nil, ErrInsufficientPermissions
}
return resource{
id: res.ID,
permission: res.Permission.Int32,
parentID: res.Parent,
name: res.Name,
size: res.Size.Int64,
collection: res.Dir,
modTime: res.Modified.Time,
etag: res.Etag.String,
}, nil
}
func (f filesystem) ResourceByID(id uuid.UUID) (Resource, error) {
res, err := f.db.Queries().ResourceByIdWithPermissions(f.ctx, sql.ResourceByIdWithPermissionsParams{Root: f.root.ID(), ResourceID: id, UserID: f.user})
if err == pgx.ErrNoRows || res.Permission.Int32 == 0 {
err = fs.ErrNotExist
}
if err != nil {
return nil, err
}
return resource{
id: res.ID,
permission: res.Permission.Int32,
parentID: res.Parent,
name: res.Name,
size: res.Size.Int64,
collection: res.Dir,
modTime: res.Modified.Time,
etag: res.Etag.String,
}, nil
}
func (f filesystem) OpenRead(r Resource, start, length int64) (io.ReadCloser, error) {
if r.Permission() < PermissionReadOnly {
return nil, ErrInsufficientPermissions
}
return r.storage.OpenRead(r.ID(), start, length)
}
func (f filesystem) OpenWrite(r Resource) (io.WriteCloser, error) {
if r.Permission() < PermissionReadWrite {
return nil, ErrInsufficientPermissions
}
return r.storage.OpenWrite(r.id, func(len int, etag string) error {
return r.db.Queries().UpdateResourceContents(f.ctx, sql.UpdateResourceContentsParams{
ID: r.id,
Size: pgtype.Int8{Int64: int64(len), Valid: true},
Etag: pgtype.Text{String: etag, Valid: true},
})
})
}
func (f filesystem) ReadDir(r Resource) ([]Resource, error) {
if r.Permission() < PermissionReadOnly {
return nil, ErrInsufficientPermissions
}
children, err := f.db.Queries().ReadDir(f.ctx, sql.ReadDirParams{ID: r.ID(), UserID: f.user, IncludeRoot: false, Recursive: false})
if err != nil {
return nil, err
}
result := make([]Resource, len(children))
for i, c := range children {
permission := r.Permission()
if c.Permission.Valid && c.Permission.Int32 > permission {
permission = c.Permission.Int32
}
result[i] = resource{
id: c.ID,
permission: permission,
parentID: r.ID(),
name: c.Name,
size: c.Size.Int64,
modTime: c.Modified.Time,
collection: c.Dir,
etag: c.Etag.String,
}
}
return result, nil
}
func (f filesystem) CreateMemberResource(r Resource, id uuid.UUID, name string, dir bool) (Resource, error) {
if r.Permission() < PermissionReadWrite {
return nil, ErrInsufficientPermissions
}
if !r.IsDir() {
return resource{}, ErrResourceNotCollection
}
var result sql.Resource
err := f.db.RunInTx(f.ctx, func(q *sql.Queries) error {
var err error
if result, err = q.CreateResource(f.ctx, sql.CreateResourceParams{ID: id, Parent: r.ID(), Name: name, Dir: dir}); err != nil {
return err
}
return q.UpdateResourceModified(f.ctx, r.ID())
})
if err != nil {
return nil, err
}
return resource{
id: id,
parentID: r.ID(),
permission: r.Permission(),
name: result.Name,
size: 0,
modTime: result.Modified.Time,
collection: dir,
etag: "",
}, nil
}
func (f filesystem) DeleteRecursive(r Resource, hardDelete bool) error {
if r.Permission() < PermissionReadWrite {
return ErrInsufficientPermissions
}
// TODO: versioning
return f.db.RunInTx(f.ctx, func(q *sql.Queries) error {
if hardDelete {
deleted, err := q.HardDeleteRecursive(f.ctx, r.ID())
if err != nil {
return err
}
errors := r.storage.Delete(deleted)
for err := range errors {
logrus.Warn(err)
}
} else {
if err := q.DeleteRecursive(f.ctx, r.ID()); err != nil {
return err
}
}
emptyUUID := uuid.UUID{}
if r.ParentID() != emptyUUID {
return q.UpdateResourceModified(f.ctx, r.ParentID())
}
return nil
})
}
func (f filesystem) UpdateName(r Resource, name string) error {
return f.db.Queries().UpdateResourceName(f.ctx, sql.UpdateResourceNameParams{ID: r.ID(), Name: name})
}
func (f filesystem) UpdateParent(r Resource, parent uuid.UUID) error {
emptyUUID := uuid.UUID{}
if r.ParentID() == emptyUUID {
return ErrCannotReparentRootResource
}
return f.db.Queries().UpdateResourceParent(f.ctx, sql.UpdateResourceParentParams{ID: r.ID(), Parent: parent})
}
func (f filesystem) UpdatePermissions(r Resource, userID int32, permission int32) error {
if r.Permission() < PermissionAdmin {
return ErrInsufficientPermissions
}
if permission > PermissionAdmin {
return ErrCannotGrantOwnerPermission
}
return f.db.Queries().UpdatePermissionsForResource(f.ctx, sql.UpdatePermissionsForResourceParams{
ResourceID: r.ID(),
UserID: userID,
Permission: permission,
})
}
+5 -166
View File
@@ -1,19 +1,13 @@
package core
import (
"context"
"errors"
"fmt"
"io"
"mime"
"path/filepath"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/shroff/phylum/server/internal/db"
"github.com/shroff/phylum/server/internal/sql"
"github.com/sirupsen/logrus"
)
const (
@@ -31,9 +25,9 @@ var (
ErrCannotReparentToRoot = errors.New("cannot reparent resource to root")
)
type ResourceInfo interface {
type Resource interface {
ID() uuid.UUID
ParentID() *uuid.UUID
ParentID() uuid.UUID
Name() string
Size() int64
Permission() int32
@@ -43,27 +37,12 @@ type ResourceInfo interface {
ContentType() string
}
type Resource interface {
ResourceInfo
OpenRead(start, length int64) (io.ReadCloser, error)
OpenWrite(ctx context.Context) (io.WriteCloser, error)
ReadDir(ctx context.Context) ([]Resource, error)
CreateMemberResource(ctx context.Context, id uuid.UUID, name string, dir bool) (Resource, error)
DeleteRecursive(ctx context.Context, hardDelete bool) error
UpdateName(ctx context.Context, name string) error
UpdateParent(ctx context.Context, parent uuid.UUID) error
UpdatePermissions(ctx context.Context, userID int32, permission int32) error
}
type resource struct {
db *db.DbHandler
storage Storage
id uuid.UUID
parentID *uuid.UUID
permission int32
parentID uuid.UUID
name string
size int64
userID int32
permission int32
collection bool
modTime time.Time
etag string
@@ -73,7 +52,7 @@ func (r resource) ID() uuid.UUID { return r.id }
func (r resource) Permission() int32 { return r.permission }
func (r resource) ParentID() *uuid.UUID { return r.parentID }
func (r resource) ParentID() uuid.UUID { return r.parentID }
func (r resource) Name() string { return r.name }
@@ -99,143 +78,3 @@ func (r resource) ContentType() string {
}
return "application/octet-stream"
}
func (r resource) OpenRead(start, length int64) (io.ReadCloser, error) {
if r.permission < PermissionReadOnly {
return nil, ErrInsufficientPermissions
}
return r.storage.OpenRead(r.id, start, length)
}
func (r resource) OpenWrite(ctx context.Context) (io.WriteCloser, error) {
if r.permission < PermissionReadWrite {
return nil, ErrInsufficientPermissions
}
return r.storage.OpenWrite(r.id, func(len int, etag string) error {
return r.db.Queries().UpdateResourceContents(ctx, sql.UpdateResourceContentsParams{
ID: r.id,
Size: pgtype.Int8{Int64: int64(len), Valid: true},
Etag: pgtype.Text{String: etag, Valid: true},
})
})
}
func (r resource) ReadDir(ctx context.Context) ([]Resource, error) {
if r.permission < PermissionReadOnly {
return nil, ErrInsufficientPermissions
}
children, err := r.db.Queries().ReadDir(ctx, sql.ReadDirParams{ID: r.id, UserID: r.userID, IncludeRoot: false, Recursive: false})
if err != nil {
return nil, err
}
result := make([]Resource, len(children))
for i, c := range children {
permission := r.Permission()
if c.Permission.Valid && c.Permission.Int32 > permission {
permission = c.Permission.Int32
}
result[i] = resource{
db: r.db,
storage: r.storage,
id: c.ID,
userID: r.userID,
permission: permission,
parentID: &r.id,
name: c.Name,
size: c.Size.Int64,
modTime: c.Modified.Time,
collection: c.Dir,
etag: c.Etag.String,
}
}
return result, nil
}
func (r resource) CreateMemberResource(ctx context.Context, id uuid.UUID, name string, dir bool) (Resource, error) {
if r.permission < PermissionReadWrite {
return nil, ErrInsufficientPermissions
}
if !r.collection {
return resource{}, ErrResourceNotCollection
}
var result sql.Resource
err := r.db.RunInTx(ctx, func(q *sql.Queries) error {
var err error
if result, err = q.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: &r.id, Name: name, Dir: dir}); err != nil {
return err
}
return q.UpdateResourceModified(ctx, r.id)
})
if err != nil {
return nil, err
}
return resource{
db: r.db,
storage: r.storage,
id: id,
parentID: &r.id,
userID: r.userID,
permission: r.permission,
name: result.Name,
size: 0,
modTime: result.Modified.Time,
collection: dir,
etag: "",
}, nil
}
func (r resource) DeleteRecursive(ctx context.Context, hardDelete bool) error {
if r.permission < PermissionReadWrite {
return ErrInsufficientPermissions
}
// TODO: versioning
return r.db.RunInTx(ctx, func(q *sql.Queries) error {
if hardDelete {
deleted, err := q.HardDeleteRecursive(ctx, r.id)
if err != nil {
return err
}
errors := r.storage.Delete(deleted)
for err := range errors {
logrus.Warn(err)
}
} else {
if err := q.DeleteRecursive(ctx, r.id); err != nil {
return err
}
}
if r.parentID != nil {
return q.UpdateResourceModified(ctx, *r.parentID)
}
return nil
})
}
func (r resource) UpdateName(ctx context.Context, name string) error {
return r.db.Queries().UpdateResourceName(ctx, sql.UpdateResourceNameParams{ID: r.id, Name: name})
}
func (r resource) UpdateParent(ctx context.Context, parent uuid.UUID) error {
if r.parentID == nil {
return ErrCannotReparentRootResource
}
return r.db.Queries().UpdateResourceParent(ctx, sql.UpdateResourceParentParams{ID: r.id, Parent: parent})
}
func (r resource) UpdatePermissions(ctx context.Context, userID int32, permission int32) error {
if r.permission < PermissionAdmin {
return ErrInsufficientPermissions
}
if permission > PermissionAdmin {
return ErrCannotGrantOwnerPermission
}
return r.db.Queries().UpdatePermissionsForResource(ctx, sql.UpdatePermissionsForResourceParams{
ResourceID: r.id,
UserID: userID,
Permission: permission,
})
}
-38
View File
@@ -1,13 +1,8 @@
package core
import (
"context"
"io/fs"
"strings"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/db"
"github.com/shroff/phylum/server/internal/sql"
)
type Silo interface {
@@ -15,7 +10,6 @@ type Silo interface {
Name() string
Owner() int32
StorageName() string
ResourceByPath(ctx context.Context, path string, userID int32) (Resource, error)
}
type silo struct {
@@ -51,35 +45,3 @@ func (s *silo) Owner() int32 {
func (s *silo) StorageName() string {
return s.storage.Name()
}
func (s *silo) ResourceByPath(ctx context.Context, path string, userID int32) (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 := s.db.Queries().ResourceByPath(ctx, sql.ResourceByPathParams{Root: s.root, Search: segments, UserID: userID})
if err != nil {
return nil, fs.ErrNotExist
}
if !res.Permission.Valid || res.Permission.Int32 == 0 {
return nil, ErrInsufficientPermissions
}
return resource{
db: s.db,
storage: s.storage,
id: res.ID,
userID: userID,
permission: res.Permission.Int32,
parentID: res.Parent,
name: res.Name,
size: res.Size.Int64,
collection: res.Dir,
modTime: res.Modified.Time,
etag: res.Etag.String,
}, nil
}
-17
View File
@@ -1,24 +1,7 @@
package app
import (
"context"
"errors"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/shroff/phylum/server/internal/sql"
)
var ErrResourceNotFound = errors.New("resource not found")
func (a *App) LocateResource(id uuid.UUID, userID int32) (uuid.UUID, int, error) {
result, err := a.Db.Queries().PermissionsForResource(context.Background(), sql.PermissionsForResourceParams{ResourceID: id, UserID: userID})
if err == pgx.ErrNoRows {
err = ErrResourceNotFound
}
if err != nil {
return uuid.UUID{}, 0, err
}
return result.ID, int(result.Permission.Int32), nil
}
+21 -20
View File
@@ -21,7 +21,7 @@ func (a App) CreateSilo(ctx context.Context, id uuid.UUID, owner int32, storage,
}
if _, err := q.CreateResource(ctx, sql.CreateResourceParams{
ID: id,
Parent: nil,
Parent: uuid.UUID{},
Name: id.String(),
Dir: true,
}); err != nil {
@@ -86,25 +86,26 @@ func (a App) FindSilo(ctx context.Context, idOrName string) (core.Silo, error) {
}
func (a App) DeleteSilo(ctx context.Context, id uuid.UUID) error {
return a.Db.RunInTx(ctx, func(q *sql.Queries) error {
result, err := q.SiloById(ctx, id)
if err != nil {
return err
}
storage := a.FindStorageBackend(result.Storage)
if storage == nil {
return errors.New("storage backend not found for " + id.String())
}
return nil
// return a.Db.RunInTx(ctx, func(q *sql.Queries) error {
// result, err := q.SiloById(ctx, id)
// if err != nil {
// return err
// }
// storage := a.FindStorageBackend(result.Storage)
// if storage == nil {
// return errors.New("storage backend not found for " + id.String())
// }
silo := core.NewSilo(a.Db, result.Name, result.Owner, result.ID, storage)
resource, err := silo.ResourceByPath(ctx, "/", -1)
if err != nil {
return err
}
if err := resource.DeleteRecursive(ctx, true); err != nil {
return err
}
// silo := core.NewSilo(a.Db, result.Name, result.Owner, result.ID, storage)
// resource, err := silo.ResourceByPath(ctx, "/", -1)
// if err != nil {
// return err
// }
// if err := resource.DeleteRecursive(ctx, true); err != nil {
// return err
// }
return q.DeleteSilo(ctx, id)
})
// return q.DeleteSilo(ctx, id)
// })
}
+42 -36
View File
@@ -42,46 +42,52 @@ func SetupHandler(r *gin.RouterGroup, app *app.App) {
}
func (h *handler) HandleRequest(c *gin.Context) {
path := c.Params.ByName("path")
identifier := strings.TrimLeft(path, "/")
if identifier == "" {
// No path specified
c.JSON(404, gin.H{
"ERR_CODE": "err_silo_path_not_specified",
})
return
}
index := strings.Index(identifier, "/")
if index != -1 {
identifier = identifier[0:index]
}
silo, err := h.app.FindSilo(context.Background(), identifier)
if err != nil {
c.Writer.WriteHeader(404)
c.JSON(404, gin.H{
"ERR_CODE": "err_silo_not_found",
"ERR_DETAILS": err.Error(),
})
return
}
// path := c.Params.ByName("path")
// identifier := strings.TrimLeft(path, "/")
// if identifier == "" {
// // No path specified
// c.JSON(404, gin.H{
// "ERR_CODE": "err_silo_path_not_specified",
// })
// return
// }
// index := strings.Index(identifier, "/")
// if index != -1 {
// identifier = identifier[0:index]
// }
userID := auth.GetUserID(c)
fs, err := h.app.OpenFileSystem(c.Request.Context(), auth.GetUserID(c))
// silo, err := h.app.FindSilo(context.Background(), identifier)
if err != nil {
c.AbortWithStatusJSON(404, gin.H{
"ERR_CODE": "err_silo_not_found",
})
return
}
webdavHandler := webdav.Handler{
Prefix: h.prefix + "/" + identifier,
FileSystem: adapter{silo: silo, userID: userID},
Prefix: h.prefix,
FileSystem: adapter{fs: fs},
LockSystem: webdav.NewMemLS(),
}
webdavHandler.ServeHTTP(c.Writer, c.Request)
}
type adapter struct {
silo core.Silo
userID int32
fs core.FileSystem
}
func (a adapter) Stat(ctx context.Context, name string) (core.Resource, error) {
return a.silo.ResourceByPath(ctx, name, a.userID)
return a.fs.ResourceByPath(name)
}
func (a adapter) OpenRead(r core.Resource, start, len int64) (io.ReadCloser, error) {
return a.fs.OpenRead(r, start, len)
}
func (a adapter) ReadDir(r core.Resource) ([]core.Resource, error) {
return a.fs.ReadDir(r)
}
func (a adapter) OpenWrite(ctx context.Context, name string) (io.WriteCloser, error) {
@@ -99,18 +105,18 @@ func (a adapter) OpenWrite(ctx context.Context, name string) (io.WriteCloser, er
}
resourceName := name[index+1:]
resourceId := uuid.New()
resource, err = parent.CreateMemberResource(ctx, resourceId, resourceName, false)
resource, err = a.fs.CreateMemberResource(parent, resourceId, resourceName, false)
if err != nil {
return nil, err
}
return resource.OpenWrite(ctx)
return a.fs.OpenWrite(resource)
}
return nil, err
}
if resource.IsDir() {
return nil, errors.New("cannot open collection for write")
}
return resource.OpenWrite(ctx)
return a.fs.OpenWrite(resource)
}
func (a adapter) RemoveAll(ctx context.Context, name string) error {
@@ -118,7 +124,7 @@ func (a adapter) RemoveAll(ctx context.Context, name string) error {
if err != nil {
return fs.ErrNotExist
}
return resource.DeleteRecursive(ctx, false)
return a.fs.DeleteRecursive(resource, false)
}
func (a adapter) Mkdir(ctx context.Context, name string) error {
@@ -133,7 +139,7 @@ func (a adapter) Mkdir(ctx context.Context, name string) error {
return fs.ErrNotExist
}
dirName := name[index+1:]
_, err = parent.CreateMemberResource(ctx, uuid.New(), dirName, true)
_, err = a.fs.CreateMemberResource(parent, uuid.New(), dirName, true)
return err
}
@@ -158,13 +164,13 @@ func (a adapter) Rename(ctx context.Context, oldName, newName string) error {
return fs.ErrNotExist
}
if src.ParentID() != nil && *src.ParentID() != parent.ID() {
if err = src.UpdateParent(ctx, parent.ID()); err != nil {
if src.ParentID() != parent.ID() {
if err = a.fs.UpdateParent(src, parent.ID()); err != nil {
return err
}
}
if src.Name() != newName && newName != "" && newName != "/" {
if err = src.UpdateName(ctx, newName); err != nil {
if err = a.fs.UpdateName(src, newName); err != nil {
return err
}
}
@@ -1,6 +1,6 @@
CREATE TABLE resources (
id uuid PRIMARY KEY,
parent uuid REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE,
parent uuid NOT NULL REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE,
name TEXT NOT NULL,
dir BOOLEAN NOT NULL,
created TIMESTAMP NOT NULL,
+1 -1
View File
@@ -24,7 +24,7 @@ type Permission struct {
type Resource struct {
ID uuid.UUID
Parent *uuid.UUID
Parent uuid.UUID
Name string
Dir bool
Created pgtype.Timestamp
+48 -12
View File
@@ -12,32 +12,68 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const permissionsForResource = `-- name: PermissionsForResource :one
const resourceByIdWithPermissions = `-- name: ResourceByIdWithPermissions :one
WITH RECURSIVE nodes(id, parent, permission) AS (
SELECT r.id, r.parent, p.permission
FROM resources r LEFT JOIN permissions p on r.id = p.resource_id WHERE r.id = $1::uuid AND p.user_id = $2::int
FROM resources r
LEFT JOIN permissions p
on r.id = p.resource_id
AND p.user_id = $2::int
WHERE r.id = $3::uuid
UNION ALL
SELECT r.id, r.parent, CASE WHEN (n.permission IS NULL OR p.permission > n.permission) THEN p.permission ELSE n.permission END
FROM resources r JOIN nodes n ON r.id = n.parent LEFT JOIN permissions p ON r.id = p.resource_id AND p.user_id = $2::int
FROM resources r
JOIN nodes n
ON r.id = n.parent
LEFT JOIN permissions p
ON r.id = p.resource_id AND p.user_id = $2::int
WHERE r.parent = $1::uuid
OR r.parent = '00000000-0000-0000-0000-000000000000'
)
SELECT id, parent, permission FROM nodes WHERE parent IS NULL
SELECT n.id, n.parent, permission, r.id, r.parent, name, dir, created, modified, deleted, size, etag FROM nodes n
JOIN resources r
ON n.id = r.id
WHERE n.parent = $1::uuid
`
type PermissionsForResourceParams struct {
ResourceID uuid.UUID
type ResourceByIdWithPermissionsParams struct {
Root uuid.UUID
UserID int32
ResourceID uuid.UUID
}
type PermissionsForResourceRow struct {
type ResourceByIdWithPermissionsRow struct {
ID uuid.UUID
Parent *uuid.UUID
Parent uuid.UUID
Permission pgtype.Int4
ID_2 uuid.UUID
Parent_2 uuid.UUID
Name string
Dir bool
Created pgtype.Timestamp
Modified pgtype.Timestamp
Deleted pgtype.Timestamp
Size pgtype.Int8
Etag pgtype.Text
}
func (q *Queries) PermissionsForResource(ctx context.Context, arg PermissionsForResourceParams) (PermissionsForResourceRow, error) {
row := q.db.QueryRow(ctx, permissionsForResource, arg.ResourceID, arg.UserID)
var i PermissionsForResourceRow
err := row.Scan(&i.ID, &i.Parent, &i.Permission)
func (q *Queries) ResourceByIdWithPermissions(ctx context.Context, arg ResourceByIdWithPermissionsParams) (ResourceByIdWithPermissionsRow, error) {
row := q.db.QueryRow(ctx, resourceByIdWithPermissions, arg.Root, arg.UserID, arg.ResourceID)
var i ResourceByIdWithPermissionsRow
err := row.Scan(
&i.ID,
&i.Parent,
&i.Permission,
&i.ID_2,
&i.Parent_2,
&i.Name,
&i.Dir,
&i.Created,
&i.Modified,
&i.Deleted,
&i.Size,
&i.Etag,
)
return i, err
}
+3 -3
View File
@@ -22,7 +22,7 @@ INSERT INTO resources(
type CreateResourceParams struct {
ID uuid.UUID
Parent *uuid.UUID
Parent uuid.UUID
Name string
Dir bool
}
@@ -137,7 +137,7 @@ type ReadDirParams struct {
type ReadDirRow struct {
ID uuid.UUID
Parent *uuid.UUID
Parent uuid.UUID
Name string
Dir bool
Created pgtype.Timestamp
@@ -236,7 +236,7 @@ type ResourceByPathParams struct {
type ResourceByPathRow struct {
ID uuid.UUID
Parent *uuid.UUID
Parent uuid.UUID
Name string
Dir bool
Created pgtype.Timestamp
+5 -3
View File
@@ -23,6 +23,8 @@ type FileSystem interface {
Mkdir(ctx context.Context, name string) error
RemoveAll(ctx context.Context, name string) error
Rename(ctx context.Context, oldName, newName string) error
ReadDir(r core.Resource) ([]core.Resource, error)
OpenRead(r core.Resource, start, length int64) (io.ReadCloser, error)
OpenWrite(ctx context.Context, name string) (io.WriteCloser, error)
}
@@ -97,7 +99,7 @@ func copyFiles(ctx context.Context, fs FileSystem, src, dst string, overwrite bo
return http.StatusForbidden, err
}
if depth == infiniteDepth {
children, err := srcStat.ReadDir(ctx)
children, err := fs.ReadDir(srcStat)
if err != nil {
return http.StatusForbidden, err
}
@@ -114,7 +116,7 @@ func copyFiles(ctx context.Context, fs FileSystem, src, dst string, overwrite bo
}
} else {
srcFile, err := srcStat.OpenRead(0, -1)
srcFile, err := fs.OpenRead(srcStat, 0, -1)
if err != nil {
if os.IsNotExist(err) {
return http.StatusNotFound, err
@@ -174,7 +176,7 @@ func walkFS(ctx context.Context, fs FileSystem, depth int, name string, info cor
if err != nil {
return walkFn(name, info, err)
}
fileInfos, err := f.ReadDir(ctx)
fileInfos, err := fs.ReadDir(f)
if err != nil {
return walkFn(name, info, err)
}
+8 -8
View File
@@ -70,7 +70,7 @@ func makePropstats(x, y Propstat) []Propstat {
var liveProps = map[xml.Name]struct {
// findFn implements the propfind function of this property. If nil,
// it indicates a hidden property.
findFn func(core.ResourceInfo) string
findFn func(core.Resource) string
// dir is true if the property applies to directories.
dir bool
}{
@@ -265,34 +265,34 @@ func escapeXML(s string) string {
return s
}
func findResourceType(fi core.ResourceInfo) string {
func findResourceType(fi core.Resource) string {
if fi.IsDir() {
return `<D:collection xmlns:D="DAV:"/>`
}
return ""
}
func findDisplayName(fi core.ResourceInfo) string {
func findDisplayName(fi core.Resource) string {
return escapeXML(fi.Name())
}
func findContentLength(fi core.ResourceInfo) string {
func findContentLength(fi core.Resource) string {
return strconv.FormatInt(fi.Size(), 10)
}
func findLastModified(fi core.ResourceInfo) string {
func findLastModified(fi core.Resource) string {
return fi.ModTime().UTC().Format(http.TimeFormat)
}
func findContentType(fi core.ResourceInfo) string {
func findContentType(fi core.Resource) string {
return fi.ContentType()
}
func findETag(fi core.ResourceInfo) string {
func findETag(fi core.Resource) string {
return fi.ETag()
}
func findSupportedLock(fi core.ResourceInfo) string {
func findSupportedLock(fi core.Resource) string {
return `` +
`<D:lockentry xmlns:D="DAV:">` +
`<D:lockscope><D:exclusive/></D:lockscope>` +
+11 -11
View File
@@ -25,7 +25,7 @@ var htmlReplacer = strings.NewReplacer(
"'", "&#39;",
)
func serveCollection(w http.ResponseWriter, r *http.Request, file core.Resource) {
func serveCollection(w http.ResponseWriter, r *http.Request, fs FileSystem, file core.Resource) {
if !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.String()+"/", http.StatusMovedPermanently)
return
@@ -36,7 +36,7 @@ func serveCollection(w http.ResponseWriter, r *http.Request, file core.Resource)
}
w.Header().Set("Last-Modified", file.ModTime().Format(http.TimeFormat))
files, err := file.ReadDir(r.Context())
files, err := fs.ReadDir(file)
if err != nil {
http.Error(w, "Error reading directory", http.StatusInternalServerError)
return
@@ -59,7 +59,7 @@ func serveCollection(w http.ResponseWriter, r *http.Request, file core.Resource)
fmt.Fprintf(w, "</pre>\n")
}
func serveResource(w http.ResponseWriter, r *http.Request, file core.Resource) {
func serveResource(w http.ResponseWriter, r *http.Request, fs FileSystem, file core.Resource) {
w.Header().Set("Etag", file.ETag())
w.Header().Set("Last-Modified", file.ModTime().Format(http.TimeFormat))
w.Header().Set("Content-Type", file.ContentType())
@@ -95,9 +95,9 @@ func serveResource(w http.ResponseWriter, r *http.Request, file core.Resource) {
sendSize = ra.length
code = http.StatusPartialContent
w.Header().Set("Content-Range", ra.contentRange(file.Size()))
reader, err = file.OpenRead(ra.start, ra.length)
reader, err = fs.OpenRead(file, ra.start, ra.length)
} else {
reader, err = file.OpenRead(0, -1)
reader, err = fs.OpenRead(file, 0, -1)
}
if err != nil {
@@ -206,7 +206,7 @@ func parseRange(s string, size int64) ([]httpRange, error) {
// checkPreconditions evaluates request preconditions and reports whether a precondition
// resulted in sending StatusNotModified or StatusPreconditionFailed.
func checkPreconditions(w http.ResponseWriter, r *http.Request, ri core.ResourceInfo) (done bool, rangeHeader string) {
func checkPreconditions(w http.ResponseWriter, r *http.Request, ri core.Resource) (done bool, rangeHeader string) {
// This function carefully follows RFC 7232 section 6.
ch := checkIfMatch(r, ri)
if ch == condNone {
@@ -289,7 +289,7 @@ const (
condFalse
)
func checkIfMatch(r *http.Request, ri core.ResourceInfo) condResult {
func checkIfMatch(r *http.Request, ri core.Resource) condResult {
im := r.Header.Get("If-Match")
if im == "" {
return condNone
@@ -319,7 +319,7 @@ func checkIfMatch(r *http.Request, ri core.ResourceInfo) condResult {
return condFalse
}
func checkIfUnmodifiedSince(r *http.Request, ri core.ResourceInfo) condResult {
func checkIfUnmodifiedSince(r *http.Request, ri core.Resource) condResult {
ius := r.Header.Get("If-Unmodified-Since")
if ius == "" || isZeroTime(ri.ModTime()) {
return condNone
@@ -338,7 +338,7 @@ func checkIfUnmodifiedSince(r *http.Request, ri core.ResourceInfo) condResult {
return condFalse
}
func checkIfNoneMatch(r *http.Request, ri core.ResourceInfo) condResult {
func checkIfNoneMatch(r *http.Request, ri core.Resource) condResult {
inm := r.Header.Get("If-None-Match")
if inm == "" {
return condNone
@@ -368,7 +368,7 @@ func checkIfNoneMatch(r *http.Request, ri core.ResourceInfo) condResult {
return condTrue
}
func checkIfModifiedSince(r *http.Request, ri core.ResourceInfo) condResult {
func checkIfModifiedSince(r *http.Request, ri core.Resource) condResult {
if r.Method != "GET" && r.Method != "HEAD" {
return condNone
}
@@ -389,7 +389,7 @@ func checkIfModifiedSince(r *http.Request, ri core.ResourceInfo) condResult {
return condTrue
}
func checkIfRange(r *http.Request, ri core.ResourceInfo) condResult {
func checkIfRange(r *http.Request, ri core.Resource) condResult {
if r.Method != "GET" && r.Method != "HEAD" {
return condNone
}
+3 -3
View File
@@ -206,9 +206,9 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
return http.StatusNotFound, err
}
if file.IsDir() {
serveCollection(w, r, file)
serveCollection(w, r, h.FileSystem, file)
} else {
serveResource(w, r, file)
serveResource(w, r, h.FileSystem, file)
}
return 0, nil
@@ -625,7 +625,7 @@ func makePropstatResponse(href string, pstats []Propstat) *response {
return &resp
}
func handlePropfindError(err error, info core.ResourceInfo) error {
func handlePropfindError(err error, info core.Resource) error {
var skipResp error = nil
if info.IsDir() {
skipResp = filepath.SkipDir
+17 -4
View File
@@ -1,13 +1,26 @@
-- name: PermissionsForResource :one
-- name: ResourceByIdWithPermissions :one
WITH RECURSIVE nodes(id, parent, permission) AS (
SELECT r.id, r.parent, p.permission
FROM resources r LEFT JOIN permissions p on r.id = p.resource_id WHERE r.id = @resource_id::uuid AND p.user_id = @user_id::int
FROM resources r
LEFT JOIN permissions p
on r.id = p.resource_id
AND p.user_id = @user_id::int
WHERE r.id = @resource_id::uuid
UNION ALL
SELECT r.id, r.parent, CASE WHEN (n.permission IS NULL OR p.permission > n.permission) THEN p.permission ELSE n.permission END
FROM resources r JOIN nodes n ON r.id = n.parent LEFT JOIN permissions p ON r.id = p.resource_id AND p.user_id = @user_id::int
FROM resources r
JOIN nodes n
ON r.id = n.parent
LEFT JOIN permissions p
ON r.id = p.resource_id AND p.user_id = @user_id::int
WHERE r.parent = @root::uuid
OR r.parent = '00000000-0000-0000-0000-000000000000'
)
SELECT * FROM nodes WHERE parent IS NULL;
SELECT * FROM nodes n
JOIN resources r
ON n.id = r.id
WHERE n.parent = @root::uuid;
-- name: UpdatePermissionsForResource :exec
INSERT INTO permissions(resource_id, user_id, permission)