Skip to content

Commit cd21676

Browse files
committed
webdav: implement COPY and MOVE.
Also add a -port flag to litmus_test_server. 13 of 13 copymove tests from the litmus suite pass, as does 16 of 16 basic tests. Change-Id: Idf92cad281e15db7d4d62e28e366ea7bfa89e564 Reviewed-on: https://go-review.googlesource.com/3470 Reviewed-by: Nick Cooper <nmvc@google.com> Reviewed-by: Robert Stepanek <robert.stepanek@gmail.com> Reviewed-by: Nigel Tao <nigeltao@golang.org>
1 parent 01e7a75 commit cd21676

File tree

4 files changed

+259
-23
lines changed

4 files changed

+259
-23
lines changed

webdav/file.go

+85
Original file line numberDiff line numberDiff line change
@@ -547,3 +547,88 @@ func (f *memFile) Write(p []byte) (int, error) {
547547
f.n.modTime = time.Now()
548548
return lenp, nil
549549
}
550+
551+
// copyFiles copies files and/or directories from src to dst.
552+
//
553+
// See section 9.8.5 for when various HTTP status codes apply.
554+
func copyFiles(fs FileSystem, src, dst string, overwrite bool, depth int, recursion int) (status int, err error) {
555+
if recursion == 1000 {
556+
return http.StatusInternalServerError, errRecursionTooDeep
557+
}
558+
recursion++
559+
560+
// TODO: section 9.8.3 says that "Note that an infinite-depth COPY of /A/
561+
// into /A/B/ could lead to infinite recursion if not handled correctly."
562+
563+
srcFile, err := fs.OpenFile(src, os.O_RDONLY, 0)
564+
if err != nil {
565+
return http.StatusNotFound, err
566+
}
567+
defer srcFile.Close()
568+
srcStat, err := srcFile.Stat()
569+
if err != nil {
570+
return http.StatusNotFound, err
571+
}
572+
srcPerm := srcStat.Mode() & os.ModePerm
573+
574+
created := false
575+
if _, err := fs.Stat(dst); err != nil {
576+
if os.IsNotExist(err) {
577+
created = true
578+
} else {
579+
return http.StatusForbidden, err
580+
}
581+
} else {
582+
if !overwrite {
583+
return http.StatusPreconditionFailed, os.ErrExist
584+
}
585+
if err := fs.RemoveAll(dst); err != nil && !os.IsNotExist(err) {
586+
return http.StatusForbidden, err
587+
}
588+
}
589+
590+
if srcStat.IsDir() {
591+
if err := fs.Mkdir(dst, srcPerm); err != nil {
592+
return http.StatusForbidden, err
593+
}
594+
if depth == infiniteDepth {
595+
children, err := srcFile.Readdir(-1)
596+
if err != nil {
597+
return http.StatusForbidden, err
598+
}
599+
for _, c := range children {
600+
name := c.Name()
601+
s := path.Join(src, name)
602+
d := path.Join(dst, name)
603+
cStatus, cErr := copyFiles(fs, s, d, overwrite, depth, recursion)
604+
if cErr != nil {
605+
// TODO: MultiStatus.
606+
return cStatus, cErr
607+
}
608+
}
609+
}
610+
611+
} else {
612+
dstFile, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, srcPerm)
613+
if err != nil {
614+
if os.IsNotExist(err) {
615+
return http.StatusConflict, err
616+
}
617+
return http.StatusForbidden, err
618+
619+
}
620+
_, copyErr := io.Copy(dstFile, srcFile)
621+
closeErr := dstFile.Close()
622+
if copyErr != nil {
623+
return http.StatusForbidden, copyErr
624+
}
625+
if closeErr != nil {
626+
return http.StatusForbidden, closeErr
627+
}
628+
}
629+
630+
if created {
631+
return http.StatusCreated, nil
632+
}
633+
return http.StatusNoContent, nil
634+
}

webdav/file_test.go

+58-4
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ func testFS(t *testing.T, fs FileSystem) {
335335
" stat /d/m want errNotExist",
336336
" stat /d/n want dir",
337337
" stat /d/n/q want 4",
338-
"rename /d /d/n/x want err",
338+
"rename /d /d/n/z want err",
339339
"rename /c /d/n/q want ok",
340340
" stat /c want errNotExist",
341341
" stat /d/n/q want 2",
@@ -358,8 +358,50 @@ func testFS(t *testing.T, fs FileSystem) {
358358
"rename /t / want err",
359359
"rename /t /u/v want ok",
360360
" stat /u/v/r want 5",
361-
"rename / /x want err",
361+
"rename / /z want err",
362362
" find / /a /d /u /u/v /u/v/q /u/v/r",
363+
" stat /a want 1",
364+
" stat /b want errNotExist",
365+
" stat /c want errNotExist",
366+
" stat /u/v/r want 5",
367+
"copy__ o=F d=0 /a /b want ok",
368+
"copy__ o=T d=0 /a /c want ok",
369+
" stat /a want 1",
370+
" stat /b want 1",
371+
" stat /c want 1",
372+
" stat /u/v/r want 5",
373+
"copy__ o=F d=0 /u/v/r /b want errExist",
374+
" stat /b want 1",
375+
"copy__ o=T d=0 /u/v/r /b want ok",
376+
" stat /a want 1",
377+
" stat /b want 5",
378+
" stat /u/v/r want 5",
379+
"rm-all /a want ok",
380+
"rm-all /b want ok",
381+
"mk-dir /u/v/w want ok",
382+
"create /u/v/w/s SSSSSSSS want ok",
383+
" stat /d want dir",
384+
" stat /d/x want errNotExist",
385+
" stat /d/y want errNotExist",
386+
" stat /u/v/r want 5",
387+
" stat /u/v/w/s want 8",
388+
" find / /c /d /u /u/v /u/v/q /u/v/r /u/v/w /u/v/w/s",
389+
"copy__ o=T d=0 /u/v /d/x want ok",
390+
"copy__ o=T d=∞ /u/v /d/y want ok",
391+
"rm-all /u want ok",
392+
" stat /d/x want dir",
393+
" stat /d/x/q want errNotExist",
394+
" stat /d/x/r want errNotExist",
395+
" stat /d/x/w want errNotExist",
396+
" stat /d/x/w/s want errNotExist",
397+
" stat /d/y want dir",
398+
" stat /d/y/q want 2",
399+
" stat /d/y/r want 5",
400+
" stat /d/y/w want dir",
401+
" stat /d/y/w/s want 8",
402+
" stat /u want errNotExist",
403+
" find / /c /d /d/x /d/y /d/y/q /d/y/r /d/y/w /d/y/w/s",
404+
"copy__ o=F d=∞ /d/y /d/x want errExist",
363405
}
364406

365407
for i, tc := range testCases {
@@ -403,9 +445,12 @@ func testFS(t *testing.T, fs FileSystem) {
403445
t.Fatalf("test case #%d %q:\ngot %s\nwant %s", i, tc, got, want)
404446
}
405447

406-
case "mk-dir", "rename", "rm-all", "stat":
448+
case "copy__", "mk-dir", "rename", "rm-all", "stat":
407449
nParts := 3
408-
if op == "rename" {
450+
switch op {
451+
case "copy__":
452+
nParts = 6
453+
case "rename":
409454
nParts = 4
410455
}
411456
parts := strings.Split(arg, " ")
@@ -415,6 +460,15 @@ func testFS(t *testing.T, fs FileSystem) {
415460

416461
got, opErr := "", error(nil)
417462
switch op {
463+
case "copy__":
464+
overwrite, depth := false, 0
465+
if parts[0] == "o=T" {
466+
overwrite = true
467+
}
468+
if parts[1] == "d=∞" {
469+
depth = infiniteDepth
470+
}
471+
_, opErr = copyFiles(fs, parts[2], parts[3], overwrite, depth, 0)
418472
case "mk-dir":
419473
opErr = fs.Mkdir(parts[0], 0777)
420474
case "rename":

webdav/litmus_test_server.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@ make URL=http://localhost:9999/ check
1818
package main
1919

2020
import (
21+
"flag"
22+
"fmt"
2123
"log"
2224
"net/http"
2325
"net/url"
2426

2527
"golang.org/x/net/webdav"
2628
)
2729

30+
var port = flag.Int("port", 9999, "server port")
31+
2832
func main() {
33+
flag.Parse()
2934
http.Handle("/", &webdav.Handler{
3035
FileSystem: webdav.NewMemFS(),
3136
LockSystem: webdav.NewMemLS(),
@@ -36,15 +41,15 @@ func main() {
3641
if u, err := url.Parse(r.Header.Get("Destination")); err == nil {
3742
dst = u.Path
3843
}
39-
ow := r.Header.Get("Overwrite")
40-
log.Printf("%-8s%-25s%-25sow=%-2s%v", r.Method, r.URL.Path, dst, ow, err)
44+
o := r.Header.Get("Overwrite")
45+
log.Printf("%-10s%-25s%-25so=%-2s%v", r.Method, r.URL.Path, dst, o, err)
4146
default:
42-
log.Printf("%-8s%-30s%v", r.Method, r.URL.Path, err)
47+
log.Printf("%-10s%-30s%v", r.Method, r.URL.Path, err)
4348
}
4449
},
4550
})
4651

47-
const addr = ":9999"
52+
addr := fmt.Sprintf(":%d", *port)
4853
log.Printf("Serving %v", addr)
4954
log.Fatal(http.ListenAndServe(addr, nil))
5055
}

webdav/webdav.go

+107-15
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"errors"
1212
"io"
1313
"net/http"
14+
"net/url"
1415
"os"
1516
"time"
1617
)
@@ -37,8 +38,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
3738
} else if h.LockSystem == nil {
3839
status, err = http.StatusInternalServerError, errNoLockSystem
3940
} else {
40-
// TODO: COPY, MOVE, PROPFIND, PROPPATCH methods.
41-
// MOVE needs to enforce its Depth constraint. See the parseDepth comment.
41+
// TODO: PROPFIND, PROPPATCH methods.
4242
switch r.Method {
4343
case "OPTIONS":
4444
status, err = h.handleOptions(w, r)
@@ -50,6 +50,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5050
status, err = h.handlePut(w, r)
5151
case "MKCOL":
5252
status, err = h.handleMkcol(w, r)
53+
case "COPY", "MOVE":
54+
status, err = h.handleCopyMove(w, r)
5355
case "LOCK":
5456
status, err = h.handleLock(w, r)
5557
case "UNLOCK":
@@ -193,6 +195,91 @@ func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status in
193195
return http.StatusCreated, nil
194196
}
195197

198+
func (h *Handler) handleCopyMove(w http.ResponseWriter, r *http.Request) (status int, err error) {
199+
// TODO: COPY/MOVE for Properties, as per sections 9.8.2 and 9.9.1.
200+
201+
hdr := r.Header.Get("Destination")
202+
if hdr == "" {
203+
return http.StatusBadRequest, errInvalidDestination
204+
}
205+
u, err := url.Parse(hdr)
206+
if err != nil {
207+
return http.StatusBadRequest, errInvalidDestination
208+
}
209+
if u.Host != r.Host {
210+
return http.StatusBadGateway, errInvalidDestination
211+
}
212+
// TODO: do we need a webdav.StripPrefix HTTP handler that's like the
213+
// standard library's http.StripPrefix handler, but also strips the
214+
// prefix in the Destination header?
215+
216+
dst, src := u.Path, r.URL.Path
217+
if dst == src {
218+
return http.StatusForbidden, errDestinationEqualsSource
219+
}
220+
221+
// TODO: confirmLocks should also check dst.
222+
releaser, status, err := h.confirmLocks(r)
223+
if err != nil {
224+
return status, err
225+
}
226+
defer releaser.Release()
227+
228+
if r.Method == "COPY" {
229+
// Section 9.8.3 says that "The COPY method on a collection without a Depth
230+
// header must act as if a Depth header with value "infinity" was included".
231+
depth := infiniteDepth
232+
if hdr := r.Header.Get("Depth"); hdr != "" {
233+
depth = parseDepth(hdr)
234+
if depth != 0 && depth != infiniteDepth {
235+
// Section 9.8.3 says that "A client may submit a Depth header on a
236+
// COPY on a collection with a value of "0" or "infinity"."
237+
return http.StatusBadRequest, errInvalidDepth
238+
}
239+
}
240+
return copyFiles(h.FileSystem, src, dst, r.Header.Get("Overwrite") != "F", depth, 0)
241+
}
242+
243+
// Section 9.9.2 says that "The MOVE method on a collection must act as if
244+
// a "Depth: infinity" header was used on it. A client must not submit a
245+
// Depth header on a MOVE on a collection with any value but "infinity"."
246+
if hdr := r.Header.Get("Depth"); hdr != "" {
247+
if parseDepth(hdr) != infiniteDepth {
248+
return http.StatusBadRequest, errInvalidDepth
249+
}
250+
}
251+
252+
created := false
253+
if _, err := h.FileSystem.Stat(dst); err != nil {
254+
if !os.IsNotExist(err) {
255+
return http.StatusForbidden, err
256+
}
257+
created = true
258+
} else {
259+
switch r.Header.Get("Overwrite") {
260+
case "T":
261+
// Section 9.9.3 says that "If a resource exists at the destination
262+
// and the Overwrite header is "T", then prior to performing the move,
263+
// the server must perform a DELETE with "Depth: infinity" on the
264+
// destination resource.
265+
if err := h.FileSystem.RemoveAll(dst); err != nil {
266+
return http.StatusForbidden, err
267+
}
268+
case "F":
269+
return http.StatusPreconditionFailed, os.ErrExist
270+
default:
271+
return http.StatusBadRequest, errInvalidOverwrite
272+
}
273+
}
274+
if err := h.FileSystem.Rename(src, dst); err != nil {
275+
return http.StatusForbidden, err
276+
}
277+
if created {
278+
return http.StatusCreated, nil
279+
}
280+
return http.StatusNoContent, nil
281+
}
282+
196283
func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) {
197284
duration, err := parseTimeout(r.Header.Get("Timeout"))
198285
if err != nil {
@@ -308,7 +395,8 @@ const (
308395
//
309396
// Different WebDAV methods have further constraints on valid depths:
310397
// - PROPFIND has no further restrictions, as per section 9.1.
311-
// - MOVE accepts only "infinity", as per section 9.2.2.
398+
// - COPY accepts only "0" or "infinity", as per section 9.8.3.
399+
// - MOVE accepts only "infinity", as per section 9.9.2.
312400
// - LOCK accepts only "0" or "infinity", as per section 9.10.3.
313401
// These constraints are enforced by the handleXxx methods.
314402
func parseDepth(s string) int {
@@ -349,16 +437,20 @@ func StatusText(code int) string {
349437
}
350438

351439
var (
352-
errDirectoryNotEmpty = errors.New("webdav: directory not empty")
353-
errInvalidDepth = errors.New("webdav: invalid depth")
354-
errInvalidIfHeader = errors.New("webdav: invalid If header")
355-
errInvalidLockInfo = errors.New("webdav: invalid lock info")
356-
errInvalidLockToken = errors.New("webdav: invalid lock token")
357-
errInvalidPropfind = errors.New("webdav: invalid propfind")
358-
errInvalidResponse = errors.New("webdav: invalid response")
359-
errInvalidTimeout = errors.New("webdav: invalid timeout")
360-
errNoFileSystem = errors.New("webdav: no file system")
361-
errNoLockSystem = errors.New("webdav: no lock system")
362-
errNotADirectory = errors.New("webdav: not a directory")
363-
errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
440+
errDestinationEqualsSource = errors.New("webdav: destination equals source")
441+
errDirectoryNotEmpty = errors.New("webdav: directory not empty")
442+
errInvalidDepth = errors.New("webdav: invalid depth")
443+
errInvalidDestination = errors.New("webdav: invalid destination")
444+
errInvalidIfHeader = errors.New("webdav: invalid If header")
445+
errInvalidLockInfo = errors.New("webdav: invalid lock info")
446+
errInvalidLockToken = errors.New("webdav: invalid lock token")
447+
errInvalidOverwrite = errors.New("webdav: invalid overwrite")
448+
errInvalidPropfind = errors.New("webdav: invalid propfind")
449+
errInvalidResponse = errors.New("webdav: invalid response")
450+
errInvalidTimeout = errors.New("webdav: invalid timeout")
451+
errNoFileSystem = errors.New("webdav: no file system")
452+
errNoLockSystem = errors.New("webdav: no lock system")
453+
errNotADirectory = errors.New("webdav: not a directory")
454+
errRecursionTooDeep = errors.New("webdav: recursion too deep")
455+
errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
364456
)

0 commit comments

Comments
 (0)