tokidoki/storage/filesystem.go
2024-11-11 10:42:27 +01:00

151 lines
3.8 KiB
Go

package storage
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/rs/zerolog/log"
"github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/caldav"
"github.com/emersion/go-webdav/carddav"
)
type filesystemBackend struct {
webdav.UserPrincipalBackend
path string
caldavPrefix string
carddavPrefix string
// maps file path to *sync.RWMutex
locks sync.Map
}
var (
validFilenameRegex = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9@\._-]+(.[a-zA-Z]+)?$`)
defaultResourceName = "default"
)
func NewFilesystem(fsPath, caldavPrefix, carddavPrefix string, userPrincipalBackend webdav.UserPrincipalBackend) (caldav.Backend, carddav.Backend, error) {
info, err := os.Stat(fsPath)
if err != nil {
return nil, nil, fmt.Errorf("failed to create filesystem backend: %s", err.Error())
}
if !info.IsDir() {
return nil, nil, fmt.Errorf("base path for filesystem backend must be a directory")
}
backend := &filesystemBackend{
UserPrincipalBackend: userPrincipalBackend,
path: fsPath,
caldavPrefix: caldavPrefix,
carddavPrefix: carddavPrefix,
}
return backend, backend, nil
}
func (b *filesystemBackend) RLock(filename string) {
lock_, _ := b.locks.LoadOrStore(filename, &sync.RWMutex{})
lock := lock_.(*sync.RWMutex)
lock.RLock()
}
func (b *filesystemBackend) RUnlock(filename string) {
lock_, ok := b.locks.Load(filename)
if !ok {
panic("attempt to unlock non-existing lock")
}
lock := lock_.(*sync.RWMutex)
lock.RUnlock()
}
func (b *filesystemBackend) Lock(filename string) {
lock_, _ := b.locks.LoadOrStore(filename, &sync.RWMutex{})
lock := lock_.(*sync.RWMutex)
lock.Lock()
}
func (b *filesystemBackend) Unlock(filename string) {
lock_, ok := b.locks.Load(filename)
if !ok {
panic("attempt to unlock non-existing lock")
}
lock := lock_.(*sync.RWMutex)
lock.Unlock()
}
func ensureLocalDir(path string) error {
if _, err := os.Stat(path); os.IsNotExist(err) {
err = os.MkdirAll(path, 0755)
if err != nil {
return fmt.Errorf("error creating '%s': %s", path, err.Error())
}
}
return nil
}
func (b *filesystemBackend) localDir(homeSetPath string, components ...string) (string, error) {
c := append([]string{b.path}, homeSetPath)
c = append(c, components...)
localPath := filepath.Join(c...)
if err := ensureLocalDir(localPath); err != nil {
return "", err
}
return localPath, nil
}
// don't use this directly, use localCalDAVPath or localCardDAVPath instead.
// note that homesetpath is expected to end in /
func (b *filesystemBackend) safeLocalPath(homeSetPath string, urlPath string) (string, error) {
localPath := filepath.Join(b.path, homeSetPath)
if err := ensureLocalDir(localPath); err != nil {
return "", err
}
if urlPath == "" {
return localPath, nil
}
// We are mapping to local filesystem path, so be conservative about what to accept
if strings.HasSuffix(urlPath, "/") {
urlPath = path.Clean(urlPath) + "/"
} else {
urlPath = path.Clean(urlPath)
}
if !strings.HasPrefix(urlPath, homeSetPath) {
err := fmt.Errorf("access to resource outside of home set: %s", urlPath)
return "", webdav.NewHTTPError(403, err)
}
urlPath = strings.TrimPrefix(urlPath, homeSetPath)
// only accept simple file names for now
dir, file := path.Split(urlPath)
if file != "" && !validFilenameRegex.MatchString(file) {
log.Debug().Str("file", file).Msg("file name does not match regex")
err := fmt.Errorf("invalid file name: %s", file)
return "", webdav.NewHTTPError(400, err)
}
return filepath.Join(localPath, dir, file), nil
}
func etagForFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
src := NewETagReader(f)
if _, err := io.ReadAll(src); err != nil {
return "", err
}
return src.ETag(), nil
}