Tighten integration between to internal webdav library

This commit is contained in:
Abhishek Shroff
2024-04-18 22:24:07 +05:30
parent 0a90c7b82d
commit bcea7b7441
19 changed files with 435 additions and 711 deletions

View File

@@ -5,7 +5,7 @@ import (
"os"
"path"
"github.com/shroff/phylum/server/internal/library"
"github.com/shroff/phylum/server/internal/core"
"github.com/shroff/phylum/server/internal/sql"
"github.com/shroff/phylum/server/internal/storage"
"github.com/shroff/phylum/server/internal/user"
@@ -15,7 +15,7 @@ import (
)
var debug bool = false
var libraryManager *library.Manager
var fs *core.FileSystem
var userManager *user.Manager
var storageManager *storage.Manager
@@ -68,7 +68,7 @@ func SetupCommand() {
if err != nil {
logrus.Fatal(err)
}
libraryManager, err = library.NewManager(db, storageManager)
fs, err = core.OpenFileSystem(db, storageManager)
if err != nil {
logrus.Fatal(err)
}

View File

@@ -1,10 +1,6 @@
package command
import (
"context"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@@ -26,24 +22,24 @@ func setupLibraryCreateCommand() *cobra.Command {
Short: "Create Library",
Args: cobra.ExactArgs(3),
Run: func(cmd *cobra.Command, args []string) {
id := uuid.New()
// id := uuid.New()
storageName := args[0]
storage := storageManager.Find(storageName)
if storage == nil {
logrus.Fatal("Storage not found: " + storageName)
}
// storageName := args[0]
// storage := storageManager.Find(storageName)
// if storage == nil {
// logrus.Fatal("Storage not found: " + storageName)
// }
username := args[1]
user, err := userManager.FindUser(context.Background(), username)
if err != nil {
logrus.Fatal("User not found: " + username)
}
name := args[2]
if err := libraryManager.Create(context.Background(), id, storageName, user.ID, name); err != nil {
logrus.Fatal(err)
}
logrus.Info("Created " + id.String())
// username := args[1]
// user, err := userManager.FindUser(context.Background(), username)
// if err != nil {
// logrus.Fatal("User not found: " + username)
// }
// name := args[2]
// if err := libraryManager.Create(context.Background(), id, storageName, user.ID, name); err != nil {
// logrus.Fatal(err)
// }
// logrus.Info("Created " + id.String())
},
}
}
@@ -54,13 +50,13 @@ func setupLibraryDeleteCommand() *cobra.Command {
Short: "Delete Library",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
id, err := uuid.Parse(args[0])
if err != nil {
logrus.Fatal("Not an ID: " + args[0])
}
if err := libraryManager.Delete(context.Background(), id); err != nil {
logrus.Fatal(err)
}
// id, err := uuid.Parse(args[0])
// if err != nil {
// logrus.Fatal("Not an ID: " + args[0])
// }
// if err := libraryManager.Delete(context.Background(), id); err != nil {
// logrus.Fatal(err)
// }
},
}
}

View File

@@ -21,7 +21,7 @@ func setupServeCommand() *cobra.Command {
config := viper.GetViper()
engine := createEngine(config.GetBool("log_body"), config.GetBool("cors_enabled"), config.GetStringSlice("cors_origins"))
webdav.SetupHandler(engine.Group(config.GetString("webdav_prefix")), libraryManager, userManager)
webdav.SetupHandler(engine.Group(config.GetString("webdav_prefix")), fs, userManager)
server := endless.NewServer(config.GetString("listen"), engine)
server.BeforeBegin = func(addr string) {

77
internal/core/file.go Normal file
View File

@@ -0,0 +1,77 @@
package core
import (
"context"
"fmt"
"io"
"mime"
"path/filepath"
"time"
"github.com/google/uuid"
)
type FileInfo interface {
ID() uuid.UUID
Name() string
Size() int64
ModTime() time.Time
IsDir() bool
ETag() string
ContentType() string
}
type File interface {
FileInfo
OpenRead(ctx context.Context, start, length int64) (io.ReadCloser, error)
ReadDir(ctx context.Context) ([]FileInfo, error)
}
type fileInfo struct {
id uuid.UUID
name string
size int64
collection bool
modTime time.Time
etag string
}
type file struct {
fileInfo
fs *FileSystem
}
func (f fileInfo) ID() uuid.UUID { return f.id }
func (f fileInfo) Name() string { return f.name }
func (f fileInfo) Size() int64 { return f.size }
func (f fileInfo) ModTime() time.Time { return f.modTime }
func (f fileInfo) IsDir() bool { return f.collection }
func (f fileInfo) ETag() string {
if f.etag != "" {
return f.etag
}
// The Apache http 2.4 web server by default concatenates the
// modification time and size of a file.
return fmt.Sprintf(`"%x%x"`, f.modTime.UnixMilli(), f.size)
}
func (f fileInfo) ContentType() string {
mimeType := mime.TypeByExtension(filepath.Ext(f.name))
if mimeType != "" {
return mimeType
}
return "application/octet-stream"
}
func (f file) OpenRead(ctx context.Context, start, length int64) (io.ReadCloser, error) {
return f.fs.OpenRead(ctx, f.id, start, length)
}
func (f file) ReadDir(ctx context.Context) ([]FileInfo, error) {
return f.fs.ReadDir(ctx, f.id, false, false)
}

139
internal/core/filesystem.go Normal file
View File

@@ -0,0 +1,139 @@
package core
import (
"context"
"io"
"io/fs"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/shroff/phylum/server/internal/sql"
"github.com/shroff/phylum/server/internal/storage"
"github.com/sirupsen/logrus"
)
type FileSystem struct {
db *sql.DbHandler
storageManager *storage.Manager
}
func OpenFileSystem(db *sql.DbHandler, storageManager *storage.Manager) (*FileSystem, error) {
fs := &FileSystem{db: db, storageManager: storageManager}
if root, err := db.Queries().ResourceById(context.Background(), uuid.UUID{}); err != nil {
logrus.Info("Root directory not found. Creating")
if err := db.Queries().CreateResource(context.Background(), sql.CreateResourceParams{ID: uuid.UUID{}, Name: "root", Dir: true}); err != nil {
logrus.Fatal("Unable to create root directory: " + err.Error())
}
} else if !root.Dir {
logrus.Fatal("Root is not a directory?!")
}
return fs, nil
}
func (f *FileSystem) OpenRead(ctx context.Context, id uuid.UUID, start, length int64) (io.ReadCloser, error) {
return f.storageManager.OpenRead(ctx, id, start, length)
}
func (f *FileSystem) OpenWrite(ctx context.Context, id uuid.UUID) (io.WriteCloser, error) {
return f.storageManager.OpenWrite(id, func(len int, etag string) error {
return f.db.Queries().UpdateResourceContents(ctx, sql.UpdateResourceContentsParams{
ID: id,
Size: pgtype.Int4{Int32: int32(len), Valid: true},
Etag: pgtype.Text{String: etag, Valid: true},
})
})
}
func (f *FileSystem) ReadDir(ctx context.Context, id uuid.UUID, includeRoot bool, recursive bool) ([]FileInfo, error) {
children, err := f.db.Queries().ReadDir(ctx, sql.ReadDirParams{ID: id, IncludeRoot: includeRoot, Recursive: recursive})
if err != nil {
return nil, err
}
result := make([]FileInfo, len(children))
for i, c := range children {
result[i] = fileInfo{
name: c.Name,
size: int64(c.Size.Int32),
modTime: c.Modified.Time,
collection: c.Dir,
id: c.ID,
etag: c.Etag.String,
}
}
return result, nil
}
func (f *FileSystem) DeleteRecursive(ctx context.Context, id uuid.UUID) error {
// TODO: versioning
hardDelete := false
return f.db.RunInTx(ctx, func(q *sql.Queries) error {
p, err := q.ResourceById(ctx, id)
if err != nil {
return err
}
if hardDelete {
deleted, err := q.HardDeleteRecursive(ctx, id)
if err != nil {
return err
}
errors := f.storageManager.Delete(deleted)
for err := range errors {
logrus.Warn(err)
}
} else {
if err = q.DeleteRecursive(ctx, id); err != nil {
return err
}
}
if p.Parent != nil {
return q.UpdateResourceModified(ctx, *p.Parent)
}
return nil
})
}
func (f *FileSystem) CreateResource(ctx context.Context, id uuid.UUID, parent uuid.UUID, name string, dir bool) error {
return f.db.RunInTx(ctx, func(q *sql.Queries) error {
if err := q.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: &parent, Name: name, Dir: dir}); err != nil {
return err
}
return q.UpdateResourceModified(ctx, parent)
})
}
func (f *FileSystem) Move(ctx context.Context, id uuid.UUID, parent uuid.UUID, name string) error {
return f.db.Queries().Rename(ctx, sql.RenameParams{ID: id, Parent: parent, Name: name})
}
func (f *FileSystem) ResourceByPath(ctx context.Context, path string) (File, 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(ctx, segments)
if err != nil {
return nil, fs.ErrNotExist
}
//TODO: Permissions checks
return file{
fileInfo: fileInfo{
id: res.ID,
name: res.Name,
size: int64(res.Size.Int32),
collection: res.Dir,
modTime: res.Modified.Time,
etag: res.Etag.String,
},
fs: f,
}, nil
}

View File

@@ -1,154 +0,0 @@
package webdav
import (
"context"
"fmt"
"io"
"io/fs"
"net/http"
"strings"
webdav "github.com/emersion/go-webdav"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/library"
"github.com/shroff/phylum/server/internal/sql"
)
type adapter struct {
lib *library.Library
prefix string
}
func (a adapter) Open(ctx context.Context, name string) (io.ReadCloser, error) {
resource, err := a.resourceByPath(ctx, name)
if err != nil {
return nil, err
}
return a.lib.OpenRead(ctx, resource.ID, 0, -1)
}
func (a adapter) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) {
resource, err := a.resourceByPath(ctx, name)
if err != nil {
return nil, err
}
val := &webdav.FileInfo{
Path: string(a.prefix + resource.Path),
Size: int64(resource.Size.Int32),
ModTime: resource.Modified.Time,
IsDir: resource.Dir,
MIMEType: resource.Name,
ETag: resource.Etag.String,
}
return val, nil
}
func (a adapter) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) {
dir, err := a.resourceByPath(ctx, name)
if err != nil {
return nil, err
}
if !dir.Dir {
return nil, fs.ErrInvalid
}
children, err := a.lib.ReadDir(ctx, dir.ID, false, recursive)
if err != nil {
return nil, err
}
result := make([]webdav.FileInfo, len(children))
prefix := a.prefix + dir.Path
for i, c := range children {
result[i] = webdav.FileInfo{
Path: string(prefix + c.Path),
Size: int64(c.Size.Int32),
ModTime: c.Modified.Time,
IsDir: c.Dir,
MIMEType: c.Name,
ETag: c.Etag.String,
}
}
return result, nil
}
func (a adapter) Create(ctx context.Context, name string) (io.WriteCloser, error) {
var id uuid.UUID
if resource, err := a.resourceByPath(ctx, name); err == nil {
id = resource.ID
} else {
name = strings.TrimRight(name, "/")
index := strings.LastIndex(name, "/")
parentPath := name[0:index]
parent, err := a.resourceByPath(ctx, parentPath)
if err != nil {
return nil, fs.ErrNotExist
}
fileName := name[index+1:]
id = uuid.New()
if err = a.lib.CreateResource(ctx, id, parent.ID, fileName, false); err != nil {
return nil, err
}
}
return a.lib.OpenWrite(ctx, id)
}
func (a adapter) RemoveAll(ctx context.Context, name string) error {
resource, err := a.resourceByPath(ctx, name)
if err != nil {
return fs.ErrNotExist
}
return a.lib.DeleteRecursive(ctx, resource.ID, false)
}
func (a adapter) Mkdir(ctx context.Context, name string) error {
if _, err := a.resourceByPath(ctx, name); err == nil {
return fs.ErrExist
}
name = strings.TrimRight(name, "/")
index := strings.LastIndex(name, "/")
parentPath := name[0:index]
parent, err := a.resourceByPath(ctx, parentPath)
if err != nil {
return fs.ErrNotExist
}
dirName := name[index+1:]
err = a.lib.CreateResource(ctx, uuid.New(), parent.ID, dirName, true)
return err
}
func (a adapter) Copy(ctx context.Context, name, dest string, options *webdav.CopyOptions) (created bool, err error) {
// TODO: Implement
return false, webdav.NewHTTPError(http.StatusMethodNotAllowed, fmt.Errorf("not implemented"))
}
func (a adapter) Move(ctx context.Context, name, destName string, options *webdav.MoveOptions) (created bool, err error) {
src, err := a.resourceByPath(ctx, name)
if err != nil {
return false, fs.ErrNotExist
}
existing, err := a.resourceByPath(ctx, destName)
if err == nil {
if options.NoOverwrite {
return false, fs.ErrExist
} else {
a.lib.DeleteRecursive(ctx, existing.ID, false)
}
}
destName = strings.TrimRight(destName, "/")
index := strings.LastIndex(destName, "/")
parentPath := destName[0:index]
parent, err := a.resourceByPath(ctx, parentPath)
destName = destName[index+1:]
if err != nil {
return false, webdav.NewHTTPError(http.StatusConflict, nil)
}
if err = a.lib.Move(ctx, src.ID, parent.ID, destName); err != nil {
return false, err
}
return true, nil
}
func (a adapter) resourceByPath(ctx context.Context, name string) (res sql.ResourceByPathRow, err error) {
return a.lib.ResourceByPath(ctx, strings.TrimPrefix(name, a.prefix))
}

View File

@@ -1,59 +0,0 @@
package webdav
import (
"context"
"strings"
webdav "github.com/emersion/go-webdav"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/library"
"github.com/sirupsen/logrus"
)
type handler struct {
libraryManager library.Manager
prefix string
}
func NewHandler(libraryManager library.Manager, prefix string) *handler {
logrus.Info("Setting up WebDAV access at " + prefix)
return &handler{
libraryManager: libraryManager,
prefix: prefix,
}
}
func (h *handler) HandleRequest(c *gin.Context) {
path := c.Params.ByName("path")
idStr := strings.TrimLeft(path, "/")
if idStr == "" {
// No path specified
c.Writer.WriteHeader(404)
return
}
index := strings.Index(idStr, "/")
if index != -1 {
idStr = idStr[0:index]
}
id, err := uuid.Parse(idStr)
if err != nil {
c.Writer.WriteHeader(404)
return
}
library, err := h.libraryManager.Get(context.Background(), id)
if err != nil {
c.Writer.WriteHeader(404)
return
}
webdavHandler := webdav.Handler{
FileSystem: adapter{
lib: library,
prefix: h.prefix + "/" + idStr,
},
}
webdavHandler.ServeHTTP(c.Writer, c.Request)
}

View File

@@ -1,114 +0,0 @@
package webdav
import (
"context"
"errors"
"io"
"io/fs"
"strings"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/library"
"github.com/shroff/phylum/server/internal/webdav"
)
type adapter struct {
lib *library.Library
}
func (a adapter) Stat(ctx context.Context, name string) (webdav.ResourceInfo, error) {
resource, err := a.lib.ResourceByPath(ctx, name)
if err != nil {
return nil, err
}
val := resourceInfo{
lib: a.lib,
name: resource.Name,
size: int64(resource.Size.Int32),
modTime: resource.Modified.Time,
collection: resource.Dir,
resourceID: resource.ID,
etag: resource.Etag.String,
}
return val, nil
}
func (a adapter) OpenWrite(ctx context.Context, name string) (io.WriteCloser, error) {
resource, err := a.lib.ResourceByPath(ctx, name)
resourceId := resource.ID
if err != nil {
if err == fs.ErrNotExist {
// Try to create the resource if the parent collection exists
name = strings.TrimRight(name, "/")
index := strings.LastIndex(name, "/")
parentPath := name[0:index]
parent, err := a.lib.ResourceByPath(ctx, parentPath)
if err != nil {
return nil, fs.ErrNotExist
}
resourceName := name[index+1:]
resourceId = uuid.New()
err = a.lib.CreateResource(ctx, resourceId, parent.ID, resourceName, false)
if err != nil {
return nil, err
}
} else {
return nil, err
}
} else if resource.Dir {
return nil, errors.New("cannot open collection for write")
}
return a.lib.OpenWrite(ctx, resourceId)
}
func (a adapter) RemoveAll(ctx context.Context, name string) error {
resource, err := a.lib.ResourceByPath(ctx, name)
if err != nil {
return fs.ErrNotExist
}
return a.lib.DeleteRecursive(ctx, resource.ID, false)
}
func (a adapter) Mkdir(ctx context.Context, name string) error {
if _, err := a.lib.ResourceByPath(ctx, name); err == nil {
return fs.ErrExist
}
name = strings.TrimRight(name, "/")
index := strings.LastIndex(name, "/")
parentPath := name[0:index]
parent, err := a.lib.ResourceByPath(ctx, parentPath)
if err != nil {
return fs.ErrNotExist
}
dirName := name[index+1:]
err = a.lib.CreateResource(ctx, uuid.New(), parent.ID, dirName, true)
return err
}
func (a adapter) Rename(ctx context.Context, oldName, newName string) error {
src, err := a.lib.ResourceByPath(ctx, oldName)
if err != nil {
return fs.ErrNotExist
}
_, err = a.lib.ResourceByPath(ctx, newName)
if err == nil {
return fs.ErrExist
}
newName = strings.TrimRight(newName, "/")
index := strings.LastIndex(newName, "/")
parentPath := newName[0:index]
parent, err := a.lib.ResourceByPath(ctx, parentPath)
newName = newName[index+1:]
if err != nil {
return fs.ErrNotExist
}
if err = a.lib.Move(ctx, src.ID, parent.ID, newName); err != nil {
return err
}
return nil
}

View File

@@ -2,28 +2,31 @@ package webdav
import (
"context"
"errors"
"io"
"io/fs"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/core"
"github.com/shroff/phylum/server/internal/cryptutil"
"github.com/shroff/phylum/server/internal/library"
"github.com/shroff/phylum/server/internal/user"
"github.com/shroff/phylum/server/internal/webdav"
"github.com/sirupsen/logrus"
)
type handler struct {
libraryManager *library.Manager
prefix string
fs *core.FileSystem
prefix string
}
func SetupHandler(r *gin.RouterGroup, libraryManager *library.Manager, userManager *user.Manager) {
func SetupHandler(r *gin.RouterGroup, fs *core.FileSystem, userManager *user.Manager) {
logrus.Info("Setting up WebDAV access at " + r.BasePath())
handler := &handler{
libraryManager: libraryManager,
prefix: r.BasePath(),
fs: fs,
prefix: r.BasePath(),
}
r.Use(func(c *gin.Context) {
username, pass, ok := c.Request.BasicAuth()
@@ -66,33 +69,117 @@ func SetupHandler(r *gin.RouterGroup, libraryManager *library.Manager, userManag
}
func (h *handler) HandleRequest(c *gin.Context) {
path := c.Params.ByName("path")
idStr := strings.TrimLeft(path, "/")
if idStr == "" {
// No path specified
c.Writer.WriteHeader(404)
return
}
index := strings.Index(idStr, "/")
if index != -1 {
idStr = idStr[0:index]
}
id, err := uuid.Parse(idStr)
if err != nil {
c.Writer.WriteHeader(404)
return
}
// path := c.Params.ByName("path")
// idStr := strings.TrimLeft(path, "/")
// if idStr == "" {
// // No path specified
// c.Writer.WriteHeader(404)
// return
// }
// index := strings.Index(idStr, "/")
// if index != -1 {
// idStr = idStr[0:index]
// }
// id, err := uuid.Parse(idStr)
// if err != nil {
// c.Writer.WriteHeader(404)
// return
// }
library, err := h.libraryManager.Get(context.Background(), id)
if err != nil {
c.Writer.WriteHeader(404)
return
}
// library, err := h.libraryManager.Get(context.Background(), id)
// if err != nil {
// c.Writer.WriteHeader(404)
// return
// }
webdavHandler := webdav.Handler{
Prefix: h.prefix + "/" + idStr,
FileSystem: adapter{lib: library},
Prefix: h.prefix,
FileSystem: h,
LockSystem: webdav.NewMemLS(),
}
webdavHandler.ServeHTTP(c.Writer, c.Request)
}
func (a handler) Stat(ctx context.Context, name string) (core.File, error) {
return a.fs.ResourceByPath(ctx, name)
}
func (a handler) OpenWrite(ctx context.Context, name string) (io.WriteCloser, error) {
resource, err := a.fs.ResourceByPath(ctx, name)
resourceId := resource.ID()
if err != nil {
if err == fs.ErrNotExist {
// Try to create the resource if the parent collection exists
name = strings.TrimRight(name, "/")
index := strings.LastIndex(name, "/")
parentPath := name[0:index]
parent, err := a.fs.ResourceByPath(ctx, parentPath)
if err != nil {
return nil, fs.ErrNotExist
}
resourceName := name[index+1:]
resourceId = uuid.New()
err = a.fs.CreateResource(ctx, resourceId, parent.ID(), resourceName, false)
if err != nil {
return nil, err
}
} else {
return nil, err
}
} else if resource.IsDir() {
return nil, errors.New("cannot open collection for write")
}
return a.fs.OpenWrite(ctx, resourceId)
}
func (a handler) RemoveAll(ctx context.Context, name string) error {
resource, err := a.fs.ResourceByPath(ctx, name)
if err != nil {
return fs.ErrNotExist
}
return a.fs.DeleteRecursive(ctx, resource.ID())
}
func (a handler) Mkdir(ctx context.Context, name string) error {
if _, err := a.fs.ResourceByPath(ctx, name); err == nil {
return fs.ErrExist
}
name = strings.TrimRight(name, "/")
index := strings.LastIndex(name, "/")
parentPath := name[0:index]
parent, err := a.fs.ResourceByPath(ctx, parentPath)
if err != nil {
return fs.ErrNotExist
}
dirName := name[index+1:]
err = a.fs.CreateResource(ctx, uuid.New(), parent.ID(), dirName, true)
return err
}
func (a handler) Rename(ctx context.Context, oldName, newName string) error {
src, err := a.fs.ResourceByPath(ctx, oldName)
if err != nil {
return fs.ErrNotExist
}
_, err = a.fs.ResourceByPath(ctx, newName)
if err == nil {
return fs.ErrExist
}
newName = strings.TrimRight(newName, "/")
index := strings.LastIndex(newName, "/")
parentPath := newName[0:index]
parent, err := a.fs.ResourceByPath(ctx, parentPath)
newName = newName[index+1:]
if err != nil {
return fs.ErrNotExist
}
if err = a.fs.Move(ctx, src.ID(), parent.ID(), newName); err != nil {
return err
}
return nil
}

View File

@@ -1,73 +0,0 @@
package webdav
import (
"context"
"errors"
"fmt"
"io"
"mime"
"path/filepath"
"time"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/library"
"github.com/shroff/phylum/server/internal/webdav"
)
type resourceInfo struct {
lib *library.Library
name string
size int64
collection bool
modTime time.Time
resourceID uuid.UUID
etag string
}
func (ri resourceInfo) Name() string { return ri.name }
func (ri resourceInfo) Size() int64 { return ri.size }
func (ri resourceInfo) ModTime() time.Time { return ri.modTime }
func (ri resourceInfo) IsDir() bool { return ri.collection }
func (ri resourceInfo) ETag() string {
if ri.etag != "" {
return ri.etag
}
// The Apache http 2.4 web server by default concatenates the
// modification time and size of a file.
return fmt.Sprintf(`"%x%x"`, ri.modTime.UnixMilli(), ri.size)
}
func (ri resourceInfo) ContentType() string {
mimeType := mime.TypeByExtension(filepath.Ext(ri.name))
if mimeType != "" {
return mimeType
}
return "application/octet-stream"
}
func (ri resourceInfo) OpenRead(ctx context.Context, start, length int64) (io.ReadCloser, error) {
return ri.lib.OpenRead(ctx, ri.resourceID, start, length)
}
func (ri resourceInfo) Readdir(ctx context.Context) ([]webdav.ResourceInfo, error) {
if !ri.collection {
return nil, errors.New("readdir not supported for non-collection resources")
}
children, err := ri.lib.ReadDir(context.Background(), ri.resourceID, false, false)
if err != nil {
return nil, err
}
result := make([]webdav.ResourceInfo, len(children))
for i, c := range children {
result[i] = resourceInfo{
lib: ri.lib,
name: c.Name,
size: int64(c.Size.Int32),
modTime: c.Modified.Time,
collection: c.Dir,
resourceID: c.ID,
etag: c.Etag.String,
}
}
return result, nil
}

View File

@@ -1,105 +0,0 @@
package library
import (
"context"
"io"
"io/fs"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/shroff/phylum/server/internal/sql"
"github.com/shroff/phylum/server/internal/storage"
"github.com/sirupsen/logrus"
)
type Library struct {
db *sql.DbHandler
root uuid.UUID
cs storage.Storage
}
func (l Library) OpenRead(ctx context.Context, id uuid.UUID, start, length int64) (io.ReadCloser, error) {
return l.cs.OpenRead(id, start, length)
}
func (l Library) OpenWrite(ctx context.Context, id uuid.UUID) (io.WriteCloser, error) {
return l.cs.OpenWrite(id, func(len int, etag string) error {
return l.db.Queries().UpdateResourceContents(ctx, sql.UpdateResourceContentsParams{
ID: id,
Size: pgtype.Int4{Int32: int32(len), Valid: true},
Etag: pgtype.Text{String: etag, Valid: true},
})
})
}
func (l Library) ReadDir(ctx context.Context, id uuid.UUID, includeRoot bool, recursive bool) ([]sql.ReadDirRow, error) {
return l.db.Queries().ReadDir(ctx, sql.ReadDirParams{ID: id, IncludeRoot: includeRoot, Recursive: recursive})
}
func (l Library) DeleteRecursive(ctx context.Context, id uuid.UUID, hardDelete bool) error {
return l.db.RunInTx(ctx, func(q *sql.Queries) error {
p, err := q.ResourceById(ctx, id)
if err != nil {
return err
}
if hardDelete {
} else {
_, err = q.DeleteRecursive(ctx, id)
if err != nil {
return err
}
}
if hardDelete {
deleted, err := q.HardDeleteRecursive(ctx, id)
if err != nil {
return err
}
if hardDelete {
errors := l.cs.Delete(deleted)
for err := range errors {
logrus.Warn(err)
}
}
}
if p.Parent != nil {
return q.UpdateResourceModified(ctx, *p.Parent)
}
return nil
})
}
func (l Library) CreateResource(ctx context.Context, id uuid.UUID, parent uuid.UUID, name string, dir bool) error {
return l.db.RunInTx(ctx, func(q *sql.Queries) error {
if err := q.CreateResource(ctx, sql.CreateResourceParams{ID: id, Parent: &parent, Name: name, Dir: dir}); err != nil {
return err
}
return q.UpdateResourceModified(ctx, parent)
})
}
func (l Library) Move(ctx context.Context, id uuid.UUID, parent uuid.UUID, name string) error {
return l.db.Queries().Rename(ctx, sql.RenameParams{ID: id, Parent: parent, Name: name})
}
func (l Library) ResourceByPath(ctx context.Context, path string) (sql.ResourceByPathRow, 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 := l.db.Queries().ResourceByPath(ctx, sql.ResourceByPathParams{Search: segments, Root: l.root})
if err != nil {
return sql.ResourceByPathRow{}, fs.ErrNotExist
}
//TODO: Permissions checks
return res, nil
}

View File

@@ -1,59 +0,0 @@
package library
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/sql"
"github.com/shroff/phylum/server/internal/storage"
)
type Manager struct {
db *sql.DbHandler
storageManager *storage.Manager
}
func NewManager(db *sql.DbHandler, storageManager *storage.Manager) (*Manager, error) {
return &Manager{db: db, storageManager: storageManager}, nil
}
func (b Manager) Get(ctx context.Context, id uuid.UUID) (*Library, error) {
// TODO: Permissions checks
lib, err := b.db.Queries().LibraryById(ctx, id)
if err != nil {
return nil, err
}
cs := b.storageManager.Find(lib.StorageBackend)
if cs == nil {
return nil, errors.New("storage backend not found: " + lib.StorageBackend)
}
return &Library{db: b.db, root: lib.ID, cs: cs}, nil
}
func (b Manager) Create(ctx context.Context, id uuid.UUID, storageBackend string, owner int32, displayName string) error {
return b.db.RunInTx(ctx, func(q *sql.Queries) error {
if err := q.CreateLibrary(ctx, sql.CreateLibraryParams{
ID: id,
StorageBackend: storageBackend,
Owner: owner,
DisplayName: displayName,
}); err != nil {
return err
}
if err := q.CreateResource(ctx, sql.CreateResourceParams{ID: id, Dir: true}); err != nil {
return err
}
return nil
})
}
func (b Manager) Delete(ctx context.Context, id uuid.UUID) error {
if lib, err := b.Get(ctx, id); err == nil {
return lib.DeleteRecursive(ctx, lib.root, true)
} else {
return fmt.Errorf("library does not exist: %s", id)
}
}

View File

@@ -37,7 +37,7 @@ func (q *Queries) CreateResource(ctx context.Context, arg CreateResourceParams)
return err
}
const deleteRecursive = `-- name: DeleteRecursive :many
const deleteRecursive = `-- name: DeleteRecursive :exec
WITH RECURSIVE nodes(id, parent) AS (
SELECT r.id, r.parent
FROM resources r WHERE r.id = $1::uuid
@@ -49,32 +49,11 @@ WITH RECURSIVE nodes(id, parent) AS (
UPDATE resources
SET modified = NOW(), deleted = NOW()
WHERE id in (SELECT id FROM nodes)
RETURNING id, dir
`
type DeleteRecursiveRow struct {
ID uuid.UUID
Dir bool
}
func (q *Queries) DeleteRecursive(ctx context.Context, id uuid.UUID) ([]DeleteRecursiveRow, error) {
rows, err := q.db.Query(ctx, deleteRecursive, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []DeleteRecursiveRow
for rows.Next() {
var i DeleteRecursiveRow
if err := rows.Scan(&i.ID, &i.Dir); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
func (q *Queries) DeleteRecursive(ctx context.Context, id uuid.UUID) error {
_, err := q.db.Exec(ctx, deleteRecursive, id)
return err
}
const hardDeleteRecursive = `-- name: HardDeleteRecursive :many
@@ -224,7 +203,7 @@ func (q *Queries) ResourceById(ctx context.Context, id uuid.UUID) (Resource, err
const resourceByPath = `-- name: ResourceByPath :one
WITH RECURSIVE nodes(id, parent, name, dir, created, modified, size, etag, depth, path, search) AS (
SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, 0, ''::text, $1::text[]
FROM resources r WHERE r.id = $2::uuid
FROM resources r WHERE r.id = '00000000-0000-0000-0000-000000000000'::uuid
UNION ALL
SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, n.depth + 1, concat(n.path, '/', r.name), n.search
FROM resources r JOIN nodes n on r.parent = n.id
@@ -234,11 +213,6 @@ WITH RECURSIVE nodes(id, parent, name, dir, created, modified, size, etag, depth
SELECT id, parent, name, dir, created, modified, size, etag, depth, path, search FROM nodes WHERE cardinality(search) = depth
`
type ResourceByPathParams struct {
Search []string
Root uuid.UUID
}
type ResourceByPathRow struct {
ID uuid.UUID
Parent *uuid.UUID
@@ -253,8 +227,8 @@ type ResourceByPathRow struct {
Search []string
}
func (q *Queries) ResourceByPath(ctx context.Context, arg ResourceByPathParams) (ResourceByPathRow, error) {
row := q.db.QueryRow(ctx, resourceByPath, arg.Search, arg.Root)
func (q *Queries) ResourceByPath(ctx context.Context, search []string) (ResourceByPathRow, error) {
row := q.db.QueryRow(ctx, resourceByPath, search)
var i ResourceByPathRow
err := row.Scan(
&i.ID,

View File

@@ -3,7 +3,9 @@ package storage
import (
"context"
"errors"
"io"
"github.com/google/uuid"
"github.com/shroff/phylum/server/internal/sql"
)
@@ -48,6 +50,23 @@ func (m Manager) Create(name string, driver string, params map[string]string) er
return nil
}
func (m Manager) OpenRead(ctx context.Context, id uuid.UUID, start, length int64) (io.ReadCloser, error) {
return m.findStorage(id).OpenRead(id, start, length)
}
func (m Manager) OpenWrite(id uuid.UUID, callback func(int, string) error) (io.WriteCloser, error) {
return m.findStorage(id).OpenWrite(id, callback)
}
func (m Manager) Delete(rows []sql.HardDeleteRecursiveRow) []error {
// TODO: Not working
return nil
}
func (m Manager) findStorage(id uuid.UUID) Storage {
return nil
}
func (m Manager) Find(name string) Storage {
return m.backends[name]
}

View File

@@ -11,31 +11,21 @@ import (
"os"
"path"
"path/filepath"
"time"
"github.com/shroff/phylum/server/internal/core"
)
// A FileSystem implements access to a collection of named files. The elements
// in a file path are separated by slash ('/', U+002F) characters, regardless
// of host operating system convention.
type FileSystem interface {
Stat(ctx context.Context, name string) (core.File, error)
Mkdir(ctx context.Context, name string) error
RemoveAll(ctx context.Context, name string) error
Rename(ctx context.Context, oldName, newName string) error
Stat(ctx context.Context, name string) (ResourceInfo, error)
OpenWrite(ctx context.Context, name string) (io.WriteCloser, error)
}
type ResourceInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
ETag() string // entity tag for efficient caching
ContentType() string // content type
OpenRead(ctx context.Context, start, length int64) (io.ReadCloser, error)
Readdir(ctx context.Context) ([]ResourceInfo, error)
}
// moveFiles moves files and/or directories from src to dst.
//
// See section 9.9.4 for when various HTTP status codes apply.
@@ -107,7 +97,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 := srcStat.ReadDir(ctx)
if err != nil {
return http.StatusForbidden, err
}
@@ -156,14 +146,14 @@ func copyFiles(ctx context.Context, fs FileSystem, src, dst string, overwrite bo
return http.StatusNoContent, nil
}
type WalkFunc func(path string, info ResourceInfo, err error) error
type WalkFunc func(path string, info core.FileInfo, err error) error
// walkFS traverses filesystem fs starting at name up to depth levels.
//
// Allowed values for depth are 0, 1 or infiniteDepth. For each visited node,
// walkFS calls walkFn. If a visited file system node is a directory and
// walkFn returns filepath.SkipDir, walkFS will skip traversal of this node.
func walkFS(ctx context.Context, fs FileSystem, depth int, name string, info ResourceInfo, walkFn WalkFunc) error {
func walkFS(ctx context.Context, fs FileSystem, depth int, name string, info core.FileInfo, walkFn WalkFunc) error {
// This implementation is based on Walk's code in the standard path/filepath package.
err := walkFn(name, info, nil)
if err != nil {
@@ -184,7 +174,7 @@ func walkFS(ctx context.Context, fs FileSystem, depth int, name string, info Res
if err != nil {
return walkFn(name, info, err)
}
fileInfos, err := f.Readdir(ctx)
fileInfos, err := f.ReadDir(ctx)
if err != nil {
return walkFn(name, info, err)
}

View File

@@ -10,6 +10,8 @@ import (
"encoding/xml"
"net/http"
"strconv"
"github.com/shroff/phylum/server/internal/core"
)
// Proppatch describes a property update instruction as defined in RFC 4918.
@@ -68,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(ResourceInfo) string
findFn func(core.FileInfo) string
// dir is true if the property applies to directories.
dir bool
}{
@@ -263,34 +265,34 @@ func escapeXML(s string) string {
return s
}
func findResourceType(fi ResourceInfo) string {
func findResourceType(fi core.FileInfo) string {
if fi.IsDir() {
return `<D:collection xmlns:D="DAV:"/>`
}
return ""
}
func findDisplayName(fi ResourceInfo) string {
func findDisplayName(fi core.FileInfo) string {
return escapeXML(fi.Name())
}
func findContentLength(fi ResourceInfo) string {
func findContentLength(fi core.FileInfo) string {
return strconv.FormatInt(fi.Size(), 10)
}
func findLastModified(fi ResourceInfo) string {
func findLastModified(fi core.FileInfo) string {
return fi.ModTime().UTC().Format(http.TimeFormat)
}
func findContentType(fi ResourceInfo) string {
func findContentType(fi core.FileInfo) string {
return fi.ContentType()
}
func findETag(fi ResourceInfo) string {
func findETag(fi core.FileInfo) string {
return fi.ETag()
}
func findSupportedLock(fi ResourceInfo) string {
func findSupportedLock(fi core.FileInfo) string {
return `` +
`<D:lockentry xmlns:D="DAV:">` +
`<D:lockscope><D:exclusive/></D:lockscope>` +

View File

@@ -11,6 +11,8 @@ import (
"strconv"
"strings"
"time"
"github.com/shroff/phylum/server/internal/core"
)
var htmlReplacer = strings.NewReplacer(
@@ -23,18 +25,18 @@ var htmlReplacer = strings.NewReplacer(
"'", "&#39;",
)
func serveCollection(w http.ResponseWriter, r *http.Request, ri ResourceInfo) {
func serveCollection(w http.ResponseWriter, r *http.Request, file core.File) {
if !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.String()+"/", http.StatusMovedPermanently)
return
}
if checkIfModifiedSince(r, ri) == condFalse {
if checkIfModifiedSince(r, file) == condFalse {
writeNotModified(w)
return
}
w.Header().Set("Last-Modified", ri.ModTime().Format(http.TimeFormat))
w.Header().Set("Last-Modified", file.ModTime().Format(http.TimeFormat))
files, err := ri.Readdir(r.Context())
files, err := file.ReadDir(r.Context())
if err != nil {
http.Error(w, "Error reading directory", http.StatusInternalServerError)
return
@@ -57,21 +59,21 @@ func serveCollection(w http.ResponseWriter, r *http.Request, ri ResourceInfo) {
fmt.Fprintf(w, "</pre>\n")
}
func serveResource(w http.ResponseWriter, r *http.Request, ri ResourceInfo) {
w.Header().Set("Etag", ri.ETag())
w.Header().Set("Last-Modified", ri.ModTime().Format(http.TimeFormat))
w.Header().Set("Content-Type", ri.ContentType())
func serveResource(w http.ResponseWriter, r *http.Request, file core.File) {
w.Header().Set("Etag", file.ETag())
w.Header().Set("Last-Modified", file.ModTime().Format(http.TimeFormat))
w.Header().Set("Content-Type", file.ContentType())
done, rangeReq := checkPreconditions(w, r, ri)
done, rangeReq := checkPreconditions(w, r, file)
if done {
return
}
code := http.StatusOK
sendSize := ri.Size()
ranges, err := parseRange(rangeReq, ri.Size())
sendSize := file.Size()
ranges, err := parseRange(rangeReq, file.Size())
if err != nil {
w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", ri.Size()))
w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", file.Size()))
http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
return
}
@@ -92,10 +94,10 @@ func serveResource(w http.ResponseWriter, r *http.Request, ri ResourceInfo) {
ra := ranges[0]
sendSize = ra.length
code = http.StatusPartialContent
w.Header().Set("Content-Range", ra.contentRange(ri.Size()))
reader, err = ri.OpenRead(r.Context(), ra.start, ra.length)
w.Header().Set("Content-Range", ra.contentRange(file.Size()))
reader, err = file.OpenRead(r.Context(), ra.start, ra.length)
} else {
reader, err = ri.OpenRead(r.Context(), 0, -1)
reader, err = file.OpenRead(r.Context(), 0, -1)
}
if err != nil {
@@ -204,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 ResourceInfo) (done bool, rangeHeader string) {
func checkPreconditions(w http.ResponseWriter, r *http.Request, ri core.FileInfo) (done bool, rangeHeader string) {
// This function carefully follows RFC 7232 section 6.
ch := checkIfMatch(r, ri)
if ch == condNone {
@@ -287,7 +289,7 @@ const (
condFalse
)
func checkIfMatch(r *http.Request, ri ResourceInfo) condResult {
func checkIfMatch(r *http.Request, ri core.FileInfo) condResult {
im := r.Header.Get("If-Match")
if im == "" {
return condNone
@@ -317,7 +319,7 @@ func checkIfMatch(r *http.Request, ri ResourceInfo) condResult {
return condFalse
}
func checkIfUnmodifiedSince(r *http.Request, ri ResourceInfo) condResult {
func checkIfUnmodifiedSince(r *http.Request, ri core.FileInfo) condResult {
ius := r.Header.Get("If-Unmodified-Since")
if ius == "" || isZeroTime(ri.ModTime()) {
return condNone
@@ -336,7 +338,7 @@ func checkIfUnmodifiedSince(r *http.Request, ri ResourceInfo) condResult {
return condFalse
}
func checkIfNoneMatch(r *http.Request, ri ResourceInfo) condResult {
func checkIfNoneMatch(r *http.Request, ri core.FileInfo) condResult {
inm := r.Header.Get("If-None-Match")
if inm == "" {
return condNone
@@ -366,7 +368,7 @@ func checkIfNoneMatch(r *http.Request, ri ResourceInfo) condResult {
return condTrue
}
func checkIfModifiedSince(r *http.Request, ri ResourceInfo) condResult {
func checkIfModifiedSince(r *http.Request, ri core.FileInfo) condResult {
if r.Method != "GET" && r.Method != "HEAD" {
return condNone
}
@@ -387,7 +389,7 @@ func checkIfModifiedSince(r *http.Request, ri ResourceInfo) condResult {
return condTrue
}
func checkIfRange(r *http.Request, ri ResourceInfo) condResult {
func checkIfRange(r *http.Request, ri core.FileInfo) condResult {
if r.Method != "GET" && r.Method != "HEAD" {
return condNone
}

View File

@@ -16,6 +16,8 @@ import (
"path/filepath"
"strings"
"time"
"github.com/shroff/phylum/server/internal/core"
)
type Handler struct {
@@ -199,14 +201,14 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
}
// TODO: check locks for read-only access??
ctx := r.Context()
fi, err := h.FileSystem.Stat(ctx, reqPath)
file, err := h.FileSystem.Stat(ctx, reqPath)
if err != nil {
return http.StatusNotFound, err
}
if fi.IsDir() {
serveCollection(w, r, fi)
if file.IsDir() {
serveCollection(w, r, file)
} else {
serveResource(w, r, fi)
serveResource(w, r, file)
}
return 0, nil
@@ -522,7 +524,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
mw := multistatusWriter{w: w}
walkFn := func(reqPath string, info ResourceInfo, err error) error {
walkFn := func(reqPath string, info core.FileInfo, err error) error {
if err != nil {
return handlePropfindError(err, info)
}
@@ -623,9 +625,9 @@ func makePropstatResponse(href string, pstats []Propstat) *response {
return &resp
}
func handlePropfindError(err error, info ResourceInfo) error {
func handlePropfindError(err error, info core.FileInfo) error {
var skipResp error = nil
if info != nil && info.IsDir() {
if info.IsDir() {
skipResp = filepath.SkipDir
}

View File

@@ -47,7 +47,7 @@ WHERE CASE WHEN @include_root::boolean THEN true ELSE depth > 0 END;
-- name: ResourceByPath :one
WITH RECURSIVE nodes(id, parent, name, dir, created, modified, size, etag, depth, path, search) AS (
SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, 0, ''::text, @search::text[]
FROM resources r WHERE r.id = @root::uuid
FROM resources r WHERE r.id = '00000000-0000-0000-0000-000000000000'::uuid
UNION ALL
SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, n.depth + 1, concat(n.path, '/', r.name), n.search
FROM resources r JOIN nodes n on r.parent = n.id