| |
| |
| |
|
|
| |
| package webdav |
|
|
| import ( |
| "context" |
| "fmt" |
| "io" |
| "net/http" |
| "net/url" |
| "os" |
| "path" |
| "strconv" |
| "strings" |
| "time" |
|
|
| "github.com/OpenListTeam/OpenList/v4/internal/conf" |
| "github.com/OpenListTeam/OpenList/v4/internal/net" |
| "github.com/OpenListTeam/OpenList/v4/internal/op" |
| "github.com/OpenListTeam/OpenList/v4/internal/setting" |
| "github.com/OpenListTeam/OpenList/v4/internal/stream" |
| "github.com/pkg/errors" |
|
|
| "github.com/OpenListTeam/OpenList/v4/internal/errs" |
| "github.com/OpenListTeam/OpenList/v4/internal/fs" |
| "github.com/OpenListTeam/OpenList/v4/internal/model" |
| "github.com/OpenListTeam/OpenList/v4/pkg/utils" |
| "github.com/OpenListTeam/OpenList/v4/server/common" |
| ) |
|
|
| type Handler struct { |
| |
| Prefix string |
| |
| LockSystem LockSystem |
| |
| |
| Logger func(*http.Request, error) |
| } |
|
|
| func (h *Handler) stripPrefix(p string) (string, int, error) { |
| if h.Prefix == "" { |
| return p, http.StatusOK, nil |
| } |
| if r := strings.TrimPrefix(p, h.Prefix); len(r) < len(p) { |
| return r, http.StatusOK, nil |
| } |
| return p, http.StatusNotFound, errPrefixMismatch |
| } |
|
|
| func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| status, err := http.StatusBadRequest, errUnsupportedMethod |
| brw := newBufferedResponseWriter() |
| useBufferedWriter := true |
| if h.LockSystem == nil { |
| status, err = http.StatusInternalServerError, errNoLockSystem |
| } else { |
| switch r.Method { |
| case "OPTIONS": |
| status, err = h.handleOptions(brw, r) |
| case "GET", "HEAD", "POST": |
| useBufferedWriter = false |
| Writer := &common.WrittenResponseWriter{ResponseWriter: w} |
| status, err = h.handleGetHeadPost(Writer, r) |
| if status != 0 && Writer.IsWritten() { |
| status = 0 |
| } |
| case "DELETE": |
| status, err = h.handleDelete(brw, r) |
| case "PUT": |
| status, err = h.handlePut(brw, r) |
| case "MKCOL": |
| status, err = h.handleMkcol(brw, r) |
| case "COPY", "MOVE": |
| status, err = h.handleCopyMove(brw, r) |
| case "LOCK": |
| status, err = h.handleLock(brw, r) |
| case "UNLOCK": |
| status, err = h.handleUnlock(brw, r) |
| case "PROPFIND": |
| status, err = h.handlePropfind(brw, r) |
| |
| if err != nil { |
| status = http.StatusNotFound |
| } |
| case "PROPPATCH": |
| status, err = h.handleProppatch(brw, r) |
| } |
| } |
|
|
| if status != 0 { |
| w.WriteHeader(status) |
| if status != http.StatusNoContent { |
| w.Write([]byte(StatusText(status))) |
| } |
| } else if useBufferedWriter { |
| brw.WriteToResponse(w) |
| } |
| if h.Logger != nil && err != nil { |
| h.Logger(r, err) |
| } |
| } |
|
|
| func (h *Handler) lock(now time.Time, root string) (token string, status int, err error) { |
| token, err = h.LockSystem.Create(now, LockDetails{ |
| Root: root, |
| Duration: infiniteTimeout, |
| ZeroDepth: true, |
| }) |
| if err != nil { |
| if err == ErrLocked { |
| return "", StatusLocked, err |
| } |
| return "", http.StatusInternalServerError, err |
| } |
| return token, 0, nil |
| } |
|
|
| func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) { |
| hdr := r.Header.Get("If") |
| if hdr == "" { |
| |
| |
| |
| |
| |
| now, srcToken, dstToken := time.Now(), "", "" |
| if src != "" { |
| srcToken, status, err = h.lock(now, src) |
| if err != nil { |
| return nil, status, err |
| } |
| } |
| if dst != "" { |
| dstToken, status, err = h.lock(now, dst) |
| if err != nil { |
| if srcToken != "" { |
| h.LockSystem.Unlock(now, srcToken) |
| } |
| return nil, status, err |
| } |
| } |
|
|
| return func() { |
| if dstToken != "" { |
| h.LockSystem.Unlock(now, dstToken) |
| } |
| if srcToken != "" { |
| h.LockSystem.Unlock(now, srcToken) |
| } |
| }, 0, nil |
| } |
|
|
| ih, ok := parseIfHeader(hdr) |
| if !ok { |
| return nil, http.StatusBadRequest, errInvalidIfHeader |
| } |
| |
| for _, l := range ih.lists { |
| lsrc := l.resourceTag |
| if lsrc == "" { |
| lsrc = src |
| } else { |
| u, err := url.Parse(lsrc) |
| if err != nil { |
| continue |
| } |
| if u.Host != r.Host { |
| continue |
| } |
| lsrc, status, err = h.stripPrefix(u.Path) |
| if err != nil { |
| return nil, status, err |
| } |
| } |
| release, err = h.LockSystem.Confirm(time.Now(), lsrc, dst, l.conditions...) |
| if err == ErrConfirmationFailed { |
| continue |
| } |
| if err != nil { |
| return nil, http.StatusInternalServerError, err |
| } |
| return release, 0, nil |
| } |
| |
| |
| |
| |
| return nil, http.StatusPreconditionFailed, ErrLocked |
| } |
|
|
| func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) { |
| reqPath, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
| ctx := r.Context() |
| user := ctx.Value(conf.UserKey).(*model.User) |
| reqPath, err = user.JoinPath(reqPath) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
| allow := "OPTIONS, LOCK, PUT, MKCOL" |
| if fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err == nil { |
| if fi.IsDir() { |
| allow = "OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND" |
| } else { |
| allow = "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT" |
| } |
| } |
| w.Header().Set("Allow", allow) |
| |
| w.Header().Set("DAV", "1, 2") |
| |
| w.Header().Set("MS-Author-Via", "DAV") |
| return 0, nil |
| } |
|
|
| func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) { |
| reqPath, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
| |
| ctx := r.Context() |
| user := ctx.Value(conf.UserKey).(*model.User) |
| password, _ := ctx.Value(conf.MetaPassKey).(string) |
| reqPath, err = user.JoinPath(reqPath) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
| meta, err := op.GetNearestMeta(reqPath) |
| if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { |
| return http.StatusInternalServerError, err |
| } |
| if !common.CanAccess(user, meta, reqPath, password) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
| fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}) |
| if err != nil { |
| return http.StatusNotFound, err |
| } |
| if fi.IsDir() { |
| if r.Method == http.MethodHead { |
| w.Header().Set("Content-Type", "httpd/unix-directory") |
| w.Header().Set("Content-Length", "0") |
| return http.StatusOK, nil |
| } |
| return http.StatusMethodNotAllowed, nil |
| } |
| |
| storage, _ := fs.GetStorage(reqPath, &fs.GetStoragesArgs{}) |
| if storage.GetStorage().Webdav302() { |
| link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: utils.ClientIP(r), Header: r.Header, Redirect: true}) |
| if err != nil { |
| return http.StatusInternalServerError, err |
| } |
| defer link.Close() |
| http.Redirect(w, r, link.URL, http.StatusFound) |
| return 0, nil |
| } |
|
|
| if storage.GetStorage().WebdavProxyURL() { |
| if url := common.GenerateDownProxyURL(storage.GetStorage(), reqPath); url != "" { |
| w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate") |
| http.Redirect(w, r, url, http.StatusFound) |
| return 0, nil |
| } |
| } |
|
|
| link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{Header: r.Header}) |
| if err != nil { |
| return http.StatusInternalServerError, err |
| } |
| defer link.Close() |
|
|
| if storage.GetStorage().ProxyRange { |
| link = common.ProxyRange(ctx, link, fi.GetSize()) |
| } |
| err = common.Proxy(w, r, link, fi) |
| if err != nil { |
| if statusCode, ok := errs.UnwrapOrSelf(err).(net.HttpStatusCodeError); ok { |
| return int(statusCode), err |
| } |
| return http.StatusInternalServerError, fmt.Errorf("webdav proxy error: %+v", err) |
| } |
| return 0, nil |
| } |
|
|
| func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) { |
| reqPath, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
| release, status, err := h.confirmLocks(r, reqPath, "") |
| if err != nil { |
| return status, err |
| } |
| defer release() |
|
|
| ctx := r.Context() |
| user := ctx.Value(conf.UserKey).(*model.User) |
| if !user.CanRemove() { |
| return http.StatusForbidden, nil |
| } |
| reqPath, err = user.JoinPath(reqPath) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
| |
|
|
| |
| |
| |
| if _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err != nil { |
| if errs.IsObjectNotFound(err) { |
| return http.StatusNotFound, err |
| } |
| return http.StatusMethodNotAllowed, err |
| } |
| parentPath := path.Dir(reqPath) |
| parentMeta, err := op.GetNearestMeta(parentPath) |
| if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { |
| return http.StatusInternalServerError, err |
| } |
| if !common.CanWrite(user, parentMeta, parentPath) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
| if err := fs.Remove(ctx, reqPath); err != nil { |
| return http.StatusMethodNotAllowed, err |
| } |
| |
| return http.StatusNoContent, nil |
| } |
|
|
| func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) { |
| defer func() { |
| if n, _ := io.ReadFull(r.Body, []byte{0}); n == 1 { |
| _, _ = utils.CopyWithBuffer(io.Discard, r.Body) |
| } |
| _ = r.Body.Close() |
| }() |
| reqPath, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
| if reqPath == "" { |
| return http.StatusMethodNotAllowed, nil |
| } |
| release, status, err := h.confirmLocks(r, reqPath, "") |
| if err != nil { |
| return status, err |
| } |
| defer release() |
| |
| |
| ctx := r.Context() |
| user := ctx.Value(conf.UserKey).(*model.User) |
| reqPath, err = user.JoinPath(reqPath) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
| size := r.ContentLength |
| if size < 0 { |
| sizeStr := r.Header.Get("X-File-Size") |
| if sizeStr != "" { |
| size, err = strconv.ParseInt(sizeStr, 10, 64) |
| if err != nil { |
| return http.StatusBadRequest, err |
| } |
| } |
| } |
| obj := model.Object{ |
| Name: path.Base(reqPath), |
| Size: size, |
| Modified: h.getModTime(r), |
| Ctime: h.getCreateTime(r), |
| } |
| |
| if setting.GetBool(conf.IgnoreSystemFiles) && utils.IsSystemFile(obj.Name) { |
| return http.StatusForbidden, errs.IgnoredSystemFile |
| } |
| parentPath := path.Dir(reqPath) |
| parentMeta, err := op.GetNearestMeta(parentPath) |
| if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { |
| return http.StatusInternalServerError, err |
| } |
| if !user.CanWriteContent() && !common.CanWriteContentBypassUserPerms(parentMeta, parentPath) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
| if !common.CanWrite(user, parentMeta, parentPath) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
| fsStream := &stream.FileStream{ |
| Obj: &obj, |
| Reader: r.Body, |
| Mimetype: r.Header.Get("Content-Type"), |
| } |
| if fsStream.Mimetype == "" { |
| fsStream.Mimetype = utils.GetMimeType(reqPath) |
| } |
| err = fs.PutDirectly(ctx, path.Dir(reqPath), fsStream) |
| if errs.IsNotFoundError(err) { |
| return http.StatusNotFound, err |
| } |
|
|
| |
| if err != nil { |
| return http.StatusMethodNotAllowed, err |
| } |
| fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}) |
| if err != nil { |
| fi = &obj |
| } |
| etag, err := findETag(ctx, h.LockSystem, reqPath, fi) |
| if err != nil { |
| return http.StatusInternalServerError, err |
| } |
| w.Header().Set("Etag", etag) |
| return http.StatusCreated, nil |
| } |
|
|
| func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) { |
| reqPath, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
| release, status, err := h.confirmLocks(r, reqPath, "") |
| if err != nil { |
| return status, err |
| } |
| defer release() |
|
|
| ctx := r.Context() |
| user := ctx.Value(conf.UserKey).(*model.User) |
| reqPath, err = user.JoinPath(reqPath) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
|
|
| if r.ContentLength > 0 { |
| return http.StatusUnsupportedMediaType, nil |
| } |
|
|
| |
| |
| if _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err == nil { |
| return http.StatusMethodNotAllowed, err |
| } |
| |
| |
| parentPath := path.Dir(reqPath) |
| if _, err := fs.Get(ctx, parentPath, &fs.GetArgs{}); err != nil { |
| if errs.IsObjectNotFound(err) { |
| return http.StatusConflict, err |
| } |
| return http.StatusMethodNotAllowed, err |
| } |
| parentMeta, err := op.GetNearestMeta(parentPath) |
| if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { |
| return http.StatusInternalServerError, err |
| } |
| if !user.CanWriteContent() && !common.CanWriteContentBypassUserPerms(parentMeta, parentPath) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
| if !common.CanWrite(user, parentMeta, parentPath) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
| if err := fs.MakeDir(ctx, reqPath); err != nil { |
| if os.IsNotExist(err) { |
| return http.StatusConflict, err |
| } |
| return http.StatusMethodNotAllowed, err |
| } |
| return http.StatusCreated, nil |
| } |
|
|
| func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) { |
| hdr := r.Header.Get("Destination") |
| if hdr == "" { |
| return http.StatusBadRequest, errInvalidDestination |
| } |
| u, err := url.Parse(hdr) |
| if err != nil { |
| return http.StatusBadRequest, errInvalidDestination |
| } |
| if u.Host != "" && u.Host != r.Host { |
| return http.StatusBadGateway, errInvalidDestination |
| } |
|
|
| src, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
|
|
| dst, status, err := h.stripPrefix(u.Path) |
| if err != nil { |
| return status, err |
| } |
|
|
| if dst == "" { |
| return http.StatusBadGateway, errInvalidDestination |
| } |
| if dst == src { |
| return http.StatusForbidden, errDestinationEqualsSource |
| } |
|
|
| ctx := r.Context() |
| user := ctx.Value(conf.UserKey).(*model.User) |
| src, err = user.JoinPath(src) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
| dst, err = user.JoinPath(dst) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
|
|
| if r.Method == "COPY" { |
| |
| |
| |
| |
| |
| release, status, err := h.confirmLocks(r, "", dst) |
| if err != nil { |
| return status, err |
| } |
| defer release() |
|
|
| |
| |
| depth := infiniteDepth |
| if hdr := r.Header.Get("Depth"); hdr != "" { |
| depth = parseDepth(hdr) |
| if depth != 0 && depth != infiniteDepth { |
| |
| |
| return http.StatusBadRequest, errInvalidDepth |
| } |
| } |
| return copyFiles(ctx, src, dst, r.Header.Get("Overwrite") != "F") |
| } |
|
|
| release, status, err := h.confirmLocks(r, src, dst) |
| if err != nil { |
| return status, err |
| } |
| defer release() |
|
|
| |
| |
| |
| if hdr := r.Header.Get("Depth"); hdr != "" { |
| if parseDepth(hdr) != infiniteDepth { |
| return http.StatusBadRequest, errInvalidDepth |
| } |
| } |
| return moveFiles(ctx, src, dst, r.Header.Get("Overwrite") == "T") |
| } |
|
|
| func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) { |
| duration, err := parseTimeout(r.Header.Get("Timeout")) |
| if err != nil { |
| return http.StatusBadRequest, err |
| } |
| li, status, err := readLockInfo(r.Body) |
| if err != nil { |
| return status, err |
| } |
|
|
| ctx := r.Context() |
| user := ctx.Value(conf.UserKey).(*model.User) |
| token, ld, now, created := "", LockDetails{}, time.Now(), false |
| if li == (lockInfo{}) { |
| |
| ih, ok := parseIfHeader(r.Header.Get("If")) |
| if !ok { |
| return http.StatusBadRequest, errInvalidIfHeader |
| } |
| if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 { |
| token = ih.lists[0].conditions[0].Token |
| } |
| if token == "" { |
| return http.StatusBadRequest, errInvalidLockToken |
| } |
| ld, err = h.LockSystem.Refresh(now, token, duration) |
| if err != nil { |
| if err == ErrNoSuchLock { |
| return http.StatusPreconditionFailed, err |
| } |
| return http.StatusInternalServerError, err |
| } |
|
|
| } else { |
| |
| |
| depth := infiniteDepth |
| if hdr := r.Header.Get("Depth"); hdr != "" { |
| depth = parseDepth(hdr) |
| if depth != 0 && depth != infiniteDepth { |
| |
| |
| return http.StatusBadRequest, errInvalidDepth |
| } |
| } |
| reqPath, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
| reqPath, err = user.JoinPath(reqPath) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
| meta, err := op.GetNearestMeta(reqPath) |
| if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { |
| return http.StatusInternalServerError, err |
| } |
| if !common.CanWrite(user, meta, reqPath) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
| ld = LockDetails{ |
| Root: reqPath, |
| Duration: duration, |
| OwnerXML: li.Owner.InnerXML, |
| ZeroDepth: depth == 0, |
| } |
| token, err = h.LockSystem.Create(now, ld) |
| if err != nil { |
| if err == ErrLocked { |
| return StatusLocked, err |
| } |
| return http.StatusInternalServerError, err |
| } |
| defer func() { |
| if retErr != nil { |
| h.LockSystem.Unlock(now, token) |
| } |
| }() |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| w.Header().Set("Lock-Token", "<"+token+">") |
| } |
|
|
| w.Header().Set("Content-Type", "application/xml; charset=utf-8") |
| if created { |
| |
| |
| |
| w.WriteHeader(http.StatusCreated) |
| } |
| writeLockInfo(w, token, ld) |
| return 0, nil |
| } |
|
|
| func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) { |
| |
| |
| t := r.Header.Get("Lock-Token") |
| if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' { |
| return http.StatusBadRequest, errInvalidLockToken |
| } |
| t = t[1 : len(t)-1] |
|
|
| reqPath, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
| ctx := r.Context() |
| user := ctx.Value(conf.UserKey).(*model.User) |
| reqPath, err = user.JoinPath(reqPath) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
| meta, err := op.GetNearestMeta(reqPath) |
| if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { |
| return http.StatusInternalServerError, err |
| } |
| if !common.CanWrite(user, meta, reqPath) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
|
|
| switch err = h.LockSystem.Unlock(time.Now(), t); err { |
| case nil: |
| return http.StatusNoContent, err |
| case ErrForbidden: |
| return http.StatusForbidden, err |
| case ErrLocked: |
| return StatusLocked, err |
| case ErrNoSuchLock: |
| return http.StatusConflict, err |
| default: |
| return http.StatusInternalServerError, err |
| } |
| } |
|
|
| func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status int, err error) { |
| reqPath, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
| ctx := r.Context() |
| userAgent := r.Header.Get("User-Agent") |
| ctx = context.WithValue(ctx, conf.UserAgentKey, userAgent) |
| user := ctx.Value(conf.UserKey).(*model.User) |
| password, _ := ctx.Value(conf.MetaPassKey).(string) |
| reqPath, err = user.JoinPath(reqPath) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
| meta, err := op.GetNearestMeta(reqPath) |
| if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { |
| return http.StatusInternalServerError, err |
| } |
| if !common.CanAccess(user, meta, reqPath, password) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
| fi, err := fs.Get(ctx, reqPath, &fs.GetArgs{}) |
| if err != nil { |
| if errs.IsNotFoundError(err) { |
| return http.StatusNotFound, err |
| } |
| return http.StatusMethodNotAllowed, err |
| } |
| depth := infiniteDepth |
| if hdr := r.Header.Get("Depth"); hdr != "" { |
| depth = parseDepth(hdr) |
| if depth == invalidDepth { |
| return http.StatusBadRequest, errInvalidDepth |
| } |
| } |
| pf, status, err := readPropfind(r.Body) |
| if err != nil { |
| return status, err |
| } |
|
|
| mw := multistatusWriter{w: w} |
|
|
| walkFn := func(reqPath string, info model.Obj, err error) error { |
| if err != nil { |
| return err |
| } |
| var pstats []Propstat |
| if pf.Propname != nil { |
| pnames, err := propnames(ctx, h.LockSystem, info) |
| if err != nil { |
| return err |
| } |
| pstat := Propstat{Status: http.StatusOK} |
| for _, xmlname := range pnames { |
| pstat.Props = append(pstat.Props, Property{XMLName: xmlname}) |
| } |
| pstats = append(pstats, pstat) |
| } else if pf.Allprop != nil { |
| pstats, err = allprop(ctx, h.LockSystem, info, pf.Prop) |
| } else { |
| pstats, err = props(ctx, h.LockSystem, info, pf.Prop) |
| } |
| if err != nil { |
| return err |
| } |
| href := path.Join(h.Prefix, strings.TrimPrefix(reqPath, user.BasePath)) |
| if href != "/" && info.IsDir() { |
| href += "/" |
| } |
| return mw.write(makePropstatResponse(href, pstats)) |
| } |
|
|
| walkErr := walkFS(ctx, depth, reqPath, fi, walkFn) |
| closeErr := mw.close() |
| if walkErr != nil { |
| return http.StatusInternalServerError, walkErr |
| } |
| if closeErr != nil { |
| return http.StatusInternalServerError, closeErr |
| } |
| return 0, nil |
| } |
|
|
| func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (status int, err error) { |
| reqPath, status, err := h.stripPrefix(r.URL.Path) |
| if err != nil { |
| return status, err |
| } |
| release, status, err := h.confirmLocks(r, reqPath, "") |
| if err != nil { |
| return status, err |
| } |
| defer release() |
|
|
| ctx := r.Context() |
| user := ctx.Value(conf.UserKey).(*model.User) |
| reqPath, err = user.JoinPath(reqPath) |
| if err != nil { |
| return http.StatusForbidden, err |
| } |
| meta, err := op.GetNearestMeta(reqPath) |
| if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) { |
| return http.StatusInternalServerError, err |
| } |
| if !common.CanWrite(user, meta, reqPath) { |
| return http.StatusForbidden, errs.PermissionDenied |
| } |
| if _, err := fs.Get(ctx, reqPath, &fs.GetArgs{}); err != nil { |
| if errs.IsObjectNotFound(err) { |
| return http.StatusNotFound, err |
| } |
| return http.StatusMethodNotAllowed, err |
| } |
| patches, status, err := readProppatch(r.Body) |
| if err != nil { |
| return status, err |
| } |
| pstats, err := patch(ctx, h.LockSystem, reqPath, patches) |
| if err != nil { |
| return http.StatusInternalServerError, err |
| } |
| mw := multistatusWriter{w: w} |
| writeErr := mw.write(makePropstatResponse(r.URL.Path, pstats)) |
| closeErr := mw.close() |
| if writeErr != nil { |
| return http.StatusInternalServerError, writeErr |
| } |
| if closeErr != nil { |
| return http.StatusInternalServerError, closeErr |
| } |
| return 0, nil |
| } |
|
|
| func makePropstatResponse(href string, pstats []Propstat) *response { |
| resp := response{ |
| Href: []string{(&url.URL{Path: href}).EscapedPath()}, |
| Propstat: make([]propstat, 0, len(pstats)), |
| } |
| for _, p := range pstats { |
| var xmlErr *xmlError |
| if p.XMLError != "" { |
| xmlErr = &xmlError{InnerXML: []byte(p.XMLError)} |
| } |
| resp.Propstat = append(resp.Propstat, propstat{ |
| Status: fmt.Sprintf("HTTP/1.1 %d %s", p.Status, StatusText(p.Status)), |
| Prop: p.Props, |
| ResponseDescription: p.ResponseDescription, |
| Error: xmlErr, |
| }) |
| } |
| return &resp |
| } |
|
|
| const ( |
| infiniteDepth = -1 |
| invalidDepth = -2 |
| ) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func parseDepth(s string) int { |
| switch s { |
| case "0": |
| return 0 |
| case "1": |
| return 1 |
| case "infinity": |
| return infiniteDepth |
| } |
| return invalidDepth |
| } |
|
|
| |
| const ( |
| StatusMulti = 207 |
| StatusUnprocessableEntity = 422 |
| StatusLocked = 423 |
| StatusFailedDependency = 424 |
| StatusInsufficientStorage = 507 |
| ) |
|
|
| func StatusText(code int) string { |
| switch code { |
| case StatusMulti: |
| return "Multi-Status" |
| case StatusUnprocessableEntity: |
| return "Unprocessable Entity" |
| case StatusLocked: |
| return "Locked" |
| case StatusFailedDependency: |
| return "Failed Dependency" |
| case StatusInsufficientStorage: |
| return "Insufficient Storage" |
| } |
| return http.StatusText(code) |
| } |
|
|
| var ( |
| errDestinationEqualsSource = errors.New("webdav: destination equals source") |
| errDirectoryNotEmpty = errors.New("webdav: directory not empty") |
| errInvalidDepth = errors.New("webdav: invalid depth") |
| errInvalidDestination = errors.New("webdav: invalid destination") |
| errInvalidIfHeader = errors.New("webdav: invalid If header") |
| errInvalidLockInfo = errors.New("webdav: invalid lock info") |
| errInvalidLockToken = errors.New("webdav: invalid lock token") |
| errInvalidPropfind = errors.New("webdav: invalid propfind") |
| errInvalidProppatch = errors.New("webdav: invalid proppatch") |
| errInvalidResponse = errors.New("webdav: invalid response") |
| errInvalidTimeout = errors.New("webdav: invalid timeout") |
| errNoFileSystem = errors.New("webdav: no file system") |
| errNoLockSystem = errors.New("webdav: no lock system") |
| errNotADirectory = errors.New("webdav: not a directory") |
| errPrefixMismatch = errors.New("webdav: prefix mismatch") |
| errRecursionTooDeep = errors.New("webdav: recursion too deep") |
| errUnsupportedLockInfo = errors.New("webdav: unsupported lock info") |
| errUnsupportedMethod = errors.New("webdav: unsupported method") |
| ) |
|
|