Compare commits
1
Commits
001cc68172
...
be47d6844c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be47d6844c |
+11
-2
@@ -82,7 +82,7 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, cal := range cals {
|
for _, cal := range cals {
|
||||||
if err := st.EnsureCollection(user.Username, "cal-"+cal.Name); err != nil {
|
if err := st.EnsureCollection(user.Username, "col/calendars/"+cal.Name); err != nil {
|
||||||
logger.Warn("creating calendar collection", "user", user.Username, "cal", cal.Name, "error", err)
|
logger.Warn("creating calendar collection", "user", user.Username, "cal", cal.Name, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,12 +92,21 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, book := range books {
|
for _, book := range books {
|
||||||
if err := st.EnsureCollection(user.Username, "card-"+book); err != nil {
|
if err := st.EnsureCollection(user.Username, "col/addressbooks/"+book); err != nil {
|
||||||
logger.Warn("creating address book collection", "user", user.Username, "book", book, "error", err)
|
logger.Warn("creating address book collection", "user", user.Username, "book", book, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run migration from legacy flat layout (cal-*/card-* dirs and data/files/)
|
||||||
|
// into new structured col/ subdirectory layout (<user>/col/<type>/<name>).
|
||||||
|
logger.Info("starting migration from old layout to new col/ subdirectory structure")
|
||||||
|
if err := store.MigrateDataDir(cfg.Storage.DataDir); err != nil {
|
||||||
|
logger.Error("running storage migration", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
logger.Info("migration complete")
|
||||||
|
|
||||||
// ---- Middleware ----
|
// ---- Middleware ----
|
||||||
authMw := auth.NewMiddleware(cfg, dbase, logger)
|
authMw := auth.NewMiddleware(cfg, dbase, logger)
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ func (b *Backend) ListCalendars(ctx context.Context) ([]caldav.Calendar, error)
|
|||||||
|
|
||||||
cals := []caldav.Calendar{b.birthdaysCalendarMeta(p.Username)}
|
cals := []caldav.Calendar{b.birthdaysCalendarMeta(p.Username)}
|
||||||
for _, cal := range names {
|
for _, cal := range names {
|
||||||
if err := b.store.EnsureCollection(p.Username, "cal-"+cal.Name); err != nil {
|
if err := b.store.EnsureCollection(p.Username, "col/calendars/"+cal.Name); err != nil {
|
||||||
b.logger.Warn("ensuring calendar directory", "calendar", cal.Name, "error", err)
|
b.logger.Warn("ensuring calendar directory", "calendar", cal.Name, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -116,11 +116,11 @@ func (b *Backend) ListCalendars(ctx context.Context) ([]caldav.Calendar, error)
|
|||||||
disk, _ := b.store.ListCollections(p.Username)
|
disk, _ := b.store.ListCollections(p.Username)
|
||||||
configured := make(map[string]bool)
|
configured := make(map[string]bool)
|
||||||
for _, cal := range names {
|
for _, cal := range names {
|
||||||
configured["cal-"+cal.Name] = true
|
configured["col/calendars/"+cal.Name] = true
|
||||||
}
|
}
|
||||||
for _, dir := range disk {
|
for _, dir := range disk {
|
||||||
if strings.HasPrefix(dir, "cal-") && !configured[dir] {
|
if strings.HasPrefix(dir, "col/calendars/") && !configured[dir] {
|
||||||
name := strings.TrimPrefix(dir, "cal-")
|
name := strings.TrimPrefix(dir, "col/calendars/")
|
||||||
cals = append(cals, b.calendarMeta(p.Username, name, name))
|
cals = append(cals, b.calendarMeta(p.Username, name, name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func (b *Backend) ListAddressBooks(ctx context.Context) ([]carddav.AddressBook,
|
|||||||
|
|
||||||
var books []carddav.AddressBook
|
var books []carddav.AddressBook
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
if err := b.store.EnsureCollection(p.Username, "card-"+name); err != nil {
|
if err := b.store.EnsureCollection(p.Username, "col/addressbooks/"+name); err != nil {
|
||||||
b.logger.Warn("ensuring address book directory", "book", name, "error", err)
|
b.logger.Warn("ensuring address book directory", "book", name, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -88,8 +88,8 @@ func (b *Backend) ListAddressBooks(ctx context.Context) ([]carddav.AddressBook,
|
|||||||
configured["card-"+n] = true
|
configured["card-"+n] = true
|
||||||
}
|
}
|
||||||
for _, dir := range disk {
|
for _, dir := range disk {
|
||||||
if strings.HasPrefix(dir, "card-") && !configured[dir] {
|
if strings.HasPrefix(dir, "col/addressbooks/") && !configured[dir] {
|
||||||
name := strings.TrimPrefix(dir, "card-")
|
name := strings.TrimPrefix(dir, "col/addressbooks/")
|
||||||
books = append(books, b.bookMeta(p.Username, name, name))
|
books = append(books, b.bookMeta(p.Username, name, name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,6 +195,111 @@ func (s *Store) DeleteCollection(user, collection string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanupLegacy moves a user's data from the old flat layout (cal-*/card-* dirs
|
||||||
|
// directly under user/root, and files/<username>) into the new structured col/
|
||||||
|
// subdirectory layout (<user>/col/calendars/*, <user>/col/addressbooks/*,
|
||||||
|
// <user>/col/files).
|
||||||
|
func cleanupLegacy(dataDir, username string) error {
|
||||||
|
username = filepath.Base(username) // sanitize path traversal
|
||||||
|
userRoot := filepath.Join(dataDir, username)
|
||||||
|
|
||||||
|
// 1 - Move cal-*/card-* flat directories into col/calendars/ / col/addressbooks/
|
||||||
|
entries, err := os.ReadDir(userRoot)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() || e.Name() == "col" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := e.Name()
|
||||||
|
var dst string
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(name, "cal-"):
|
||||||
|
dst = filepath.Join(userRoot, "col", "calendars", strings.TrimPrefix(name, "cal-"))
|
||||||
|
case strings.HasPrefix(name, "card-"):
|
||||||
|
dst = filepath.Join(userRoot, "col", "addressbooks", strings.TrimPrefix(name, "card-"))
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(dst); err == nil {
|
||||||
|
continue // already migrated
|
||||||
|
}
|
||||||
|
os.MkdirAll(filepath.Dir(dst), 0o755)
|
||||||
|
if err := os.Rename(filepath.Join(userRoot, name), dst); err != nil {
|
||||||
|
return fmt.Errorf("moving %s to %q: %w", name, dst, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2 - Move files/<username>/* into <user>/col/files/
|
||||||
|
oldFilesRoot := filepath.Join(dataDir, "files", username)
|
||||||
|
if stat, err := os.Stat(oldFilesRoot); err == nil && stat.IsDir() {
|
||||||
|
dstFiles := filepath.Join(userRoot, "col", "files")
|
||||||
|
os.MkdirAll(dstFiles, 0o755)
|
||||||
|
|
||||||
|
subEntries, err := os.ReadDir(oldFilesRoot)
|
||||||
|
if err == nil {
|
||||||
|
for _, s := range subEntries {
|
||||||
|
src := filepath.Join(oldFilesRoot, s.Name())
|
||||||
|
dst := filepath.Join(dstFiles, s.Name())
|
||||||
|
if s.IsDir() {
|
||||||
|
os.MkdirAll(filepath.Dir(dst), 0o755)
|
||||||
|
}
|
||||||
|
os.Rename(src, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.RemoveAll(oldFilesRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateDataDir iterates all user data directories under dataDir and calls
|
||||||
|
// cleanupLegacy on each, handling both old flat-layout users (data/<username>/cal-*/...)
|
||||||
|
// and old files-direct-layout users (data/files/<username>/).
|
||||||
|
func MigrateDataDir(dataDir string) error {
|
||||||
|
users, err := os.ReadDir(dataDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) || len(users) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dirs []string
|
||||||
|
// Collect all directories under data/ (excluding "col" which is the new layout, and non-dirs like nidus.db)
|
||||||
|
for _, e := range users {
|
||||||
|
if !e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := e.Name()
|
||||||
|
if name == "files" {
|
||||||
|
// Collect all user dirs under data/files/ (legacy flat layout)
|
||||||
|
fileUsers, err2 := os.ReadDir(filepath.Join(dataDir, "files"))
|
||||||
|
if err2 == nil {
|
||||||
|
for _, fu := range fileUsers {
|
||||||
|
if fu.IsDir() {
|
||||||
|
dirs = append(dirs, fu.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if name != "col" {
|
||||||
|
dirs = append(dirs, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range dirs {
|
||||||
|
if err := cleanupLegacy(dataDir, user); err != nil {
|
||||||
|
return fmt.Errorf("migrating %q: %w", user, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// sanitize removes path-traversal characters from a path segment.
|
// sanitize removes path-traversal characters from a path segment.
|
||||||
func sanitize(s string) string {
|
func sanitize(s string) string {
|
||||||
s = filepath.Base(s)
|
s = filepath.Base(s)
|
||||||
|
|||||||
@@ -117,9 +117,9 @@ func (s *Server) handleResource(w http.ResponseWriter, r *http.Request, kind str
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
collPrefix := "cal-"
|
collPrefix := "col/calendars/"
|
||||||
if kind == "addressbook" {
|
if kind == "addressbook" {
|
||||||
collPrefix = "card-"
|
collPrefix = "col/addressbooks/"
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/yourusername/caldav-server/internal/auth"
|
"github.com/yourusername/caldav-server/internal/auth"
|
||||||
@@ -12,9 +13,19 @@ import (
|
|||||||
xwebdav "golang.org/x/net/webdav"
|
xwebdav "golang.org/x/net/webdav"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// sanitize removes path-traversal characters from a path segment.
|
||||||
|
func sanitize(s string) string {
|
||||||
|
s = filepath.Base(s)
|
||||||
|
s = strings.ReplaceAll(s, "..", "")
|
||||||
|
if s == "." || s == "" {
|
||||||
|
return "_"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// NewHandler returns an http.Handler that provides standard WebDAV file access,
|
// NewHandler returns an http.Handler that provides standard WebDAV file access,
|
||||||
// mounted at the fixed URL /files/ for every user and rooted at
|
// mounted at the fixed URL /files/ for every user and rooted at
|
||||||
// dataDir/files/<username>/ on disk. The URL is the same for all users —
|
// dataDir/<username>/col/files/ on disk. The URL is the same for all users —
|
||||||
// which user's directory is served is resolved from the Basic Auth identity
|
// which user's directory is served is resolved from the Basic Auth identity
|
||||||
// in the request context, not from the URL.
|
// in the request context, not from the URL.
|
||||||
//
|
//
|
||||||
@@ -38,7 +49,7 @@ func NewHandler(cfg *config.Config, dataDir string, logger *slog.Logger) http.Ha
|
|||||||
h, ok := handlers[p.Username]
|
h, ok := handlers[p.Username]
|
||||||
if !ok {
|
if !ok {
|
||||||
username := p.Username
|
username := p.Username
|
||||||
userDir := filepath.Join(dataDir, "files", username)
|
userDir := filepath.Join(dataDir, sanitize(username), "col", "files")
|
||||||
if err := os.MkdirAll(userDir, 0o755); err != nil {
|
if err := os.MkdirAll(userDir, 0o755); err != nil {
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
logger.Error("creating user WebDAV dir", "user", username, "error", err)
|
logger.Error("creating user WebDAV dir", "user", username, "error", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user