photoprism/internal/server/webdav.go
Michael Mayer 63ad161b96 WebDAV: Refactor authentication and HTTP request handlers #3942 #3959
Signed-off-by: Michael Mayer <michael@photoprism.app>
2024-01-10 09:56:46 +01:00

192 lines
6.3 KiB
Go

package server
import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/net/webdav"
"github.com/photoprism/photoprism/internal/auto"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/txt"
)
// WebDAVHandler wraps the http request handler so that it can be customized.
var WebDAVHandler = func(c *gin.Context, router *gin.RouterGroup, srv *webdav.Handler) {
srv.ServeHTTP(c.Writer, c.Request)
}
// WebDAV handles requests to the "/originals" and "/import" endpoints.
func WebDAV(filePath string, router *gin.RouterGroup, conf *config.Config) {
if router == nil {
log.Error("webdav: router is nil")
return
}
if conf == nil {
log.Error("webdav: conf is nil")
return
}
// Native file system restricted to a specific directory.
fileSystem := webdav.Dir(filePath)
// Request logger function.
loggerFunc := func(request *http.Request, err error) {
if err != nil {
switch request.Method {
case MethodPut, MethodPost, MethodPatch, MethodDelete, MethodCopy, MethodMove:
log.Errorf("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
case MethodPropfind:
log.Tracef("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
default:
log.Debugf("webdav: %s in %s %s", clean.Error(err), clean.Log(request.Method), clean.Log(request.URL.String()))
}
} else {
// Determine the filename if it is an uploaded file and process custom request headers, if any.
if fileName := WebDAVFileName(request, router, conf); fileName != "" {
// Flag the uploaded file as favorite if the "X-Favorite" header is set to "1".
if request.Header.Get(header.XFavorite) == "1" {
WebDAVSetFavoriteFlag(fileName)
}
// Set the file modification time based on the Unix timestamp found in the "X-OC-MTime" header.
if fileMtime := txt.Int64(request.Header.Get(header.XModTime)); fileMtime > 0 {
WebDAVSetFileMtime(fileName, fileMtime)
}
}
switch request.Method {
case MethodPut, MethodPost, MethodPatch, MethodDelete, MethodCopy, MethodMove:
log.Infof("webdav: %s %s", clean.Log(request.Method), clean.Log(request.URL.String()))
if router.BasePath() == conf.BaseUri(WebDAVOriginals) {
auto.ShouldIndex()
} else if router.BasePath() == conf.BaseUri(WebDAVImport) {
auto.ShouldImport()
}
default:
log.Tracef("webdav: %s %s", clean.Log(request.Method), clean.Log(request.URL.String()))
}
}
}
// WebDAV request handler.
srv := &webdav.Handler{
Prefix: router.BasePath(),
FileSystem: fileSystem,
LockSystem: webdav.NewMemLS(),
Logger: loggerFunc,
}
// Request handler wrapper function.
handlerFunc := func(c *gin.Context) {
WebDAVHandler(c, router, srv)
}
// handleRead registers WebDAV methods used for browsing and downloading.
handleRead := func(h func(*gin.Context)) {
router.Handle(MethodHead, "/*path", h)
router.Handle(MethodGet, "/*path", h)
router.Handle(MethodOptions, "/*path", h)
router.Handle(MethodLock, "/*path", h)
router.Handle(MethodUnlock, "/*path", h)
router.Handle(MethodPropfind, "/*path", h)
}
// handleWrite registers WebDAV methods to may modify the file system.
handleWrite := func(h func(*gin.Context)) {
router.Handle(MethodPut, "/*path", h)
router.Handle(MethodPost, "/*path", h)
router.Handle(MethodPatch, "/*path", h)
router.Handle(MethodDelete, "/*path", h)
router.Handle(MethodMkcol, "/*path", h)
router.Handle(MethodCopy, "/*path", h)
router.Handle(MethodMove, "/*path", h)
router.Handle(MethodProppatch, "/*path", h)
}
// Handle supported WebDAV request methods.
handleRead(handlerFunc)
// Only supported with read-only mode disabled.
if conf.ReadOnly() {
handleWrite(func(c *gin.Context) {
_ = c.AbortWithError(http.StatusForbidden, fmt.Errorf("forbidden in read-only mode"))
})
} else {
handleWrite(handlerFunc)
}
}
// WebDAVFileName determines the name and path of an uploaded file and returns its name if it exists.
func WebDAVFileName(request *http.Request, router *gin.RouterGroup, conf *config.Config) (fileName string) {
// Check if this is a PUT request, as used for file uploads.
if request.Method != MethodPut {
return ""
}
basePath := router.BasePath()
// Determine the absolute file path based on the request URL and the configuration.
switch basePath {
case conf.BaseUri(WebDAVOriginals):
fileName = filepath.Join(conf.OriginalsPath(), strings.TrimPrefix(request.URL.Path, basePath))
case conf.BaseUri(WebDAVImport):
fileName = filepath.Join(conf.ImportPath(), strings.TrimPrefix(request.URL.Path, basePath))
default:
return ""
}
// Check if the file actually exists and return an empty string otherwise.
if !fs.FileExists(fileName) {
return ""
}
return fileName
}
// WebDAVSetFavoriteFlag adds the favorite flag to files uploaded via WebDAV.
func WebDAVSetFavoriteFlag(fileName string) {
yamlName := fs.AbsPrefix(fileName, false) + fs.ExtYAML
// Abort if YAML file already exists to avoid overwriting metadata.
if fs.FileExists(yamlName) {
log.Warnf("webdav: %s already exists", clean.Log(filepath.Base(yamlName)))
return
}
// Make sure directory exists.
if err := os.MkdirAll(filepath.Dir(yamlName), fs.ModeDir); err != nil {
log.Errorf("webdav: %s", err.Error())
return
}
// Write YAML data to file.
if err := os.WriteFile(yamlName, []byte("Favorite: true\n"), fs.ModeFile); err != nil {
log.Errorf("webdav: %s", err.Error())
return
}
// Log success.
log.Infof("webdav: flagged %s as favorite", clean.Log(filepath.Base(fileName)))
}
// WebDAVSetFileMtime updaters the file modification time based on a Unix timestamp string.
func WebDAVSetFileMtime(fileName string, mtimeUnix int64) {
if mtime := time.Unix(mtimeUnix, 0); mtimeUnix <= 0 || mtime.IsZero() || time.Now().Before(mtime) {
log.Warnf("webdav: invalid mtime provided for %s", clean.Log(filepath.Base(fileName)))
} else if mtimeErr := os.Chtimes(fileName, time.Time{}, mtime); mtimeErr != nil {
log.Warnf("webdav: failed to set mtime for %s", clean.Log(filepath.Base(fileName)))
} else {
log.Infof("webdav: set mtime for %s", clean.Log(filepath.Base(fileName)))
}
}