mirror of
https://git.sr.ht/~sircmpwn/tokidoki
synced 2025-12-12 06:07:22 +01:00
It's apparently common in files from Google calendar. The whole regex probably needs to be reconsidered, but for now just make this work and keep it simple.
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
|
|
}
|