mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-01-07 04:00:17 -06:00
Tighten integration between to internal webdav library
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
77
internal/core/file.go
Normal 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
139
internal/core/filesystem.go
Normal 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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>` +
|
||||
|
||||
@@ -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(
|
||||
"'", "'",
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user