mirror of
https://git.sr.ht/~sircmpwn/tokidoki
synced 2025-12-12 06:07:22 +01:00
This commit introduces some helpers so that ETags can be calculated at the same time that files get read or written. Besides looking nicer, it should also help reduce lock contention around file access, as files do not need to be opened twice anymore.
151 lines
3.8 KiB
Go
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
|
|
}
|