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>
diff --git a/webdav/file.go b/webdav/file.go
index eb6abd5..4069f24 100644
--- a/webdav/file.go
+++ b/webdav/file.go
@@ -547,3 +547,88 @@
 	f.n.modTime = time.Now()
 	return lenp, nil
 }
+
+// copyFiles copies files and/or directories from src to dst.
+//
+// See section 9.8.5 for when various HTTP status codes apply.
+func copyFiles(fs FileSystem, src, dst string, overwrite bool, depth int, recursion int) (status int, err error) {
+	if recursion == 1000 {
+		return http.StatusInternalServerError, errRecursionTooDeep
+	}
+	recursion++
+
+	// TODO: section 9.8.3 says that "Note that an infinite-depth COPY of /A/
+	// into /A/B/ could lead to infinite recursion if not handled correctly."
+
+	srcFile, err := fs.OpenFile(src, os.O_RDONLY, 0)
+	if err != nil {
+		return http.StatusNotFound, err
+	}
+	defer srcFile.Close()
+	srcStat, err := srcFile.Stat()
+	if err != nil {
+		return http.StatusNotFound, err
+	}
+	srcPerm := srcStat.Mode() & os.ModePerm
+
+	created := false
+	if _, err := fs.Stat(dst); err != nil {
+		if os.IsNotExist(err) {
+			created = true
+		} else {
+			return http.StatusForbidden, err
+		}
+	} else {
+		if !overwrite {
+			return http.StatusPreconditionFailed, os.ErrExist
+		}
+		if err := fs.RemoveAll(dst); err != nil && !os.IsNotExist(err) {
+			return http.StatusForbidden, err
+		}
+	}
+
+	if srcStat.IsDir() {
+		if err := fs.Mkdir(dst, srcPerm); err != nil {
+			return http.StatusForbidden, err
+		}
+		if depth == infiniteDepth {
+			children, err := srcFile.Readdir(-1)
+			if err != nil {
+				return http.StatusForbidden, err
+			}
+			for _, c := range children {
+				name := c.Name()
+				s := path.Join(src, name)
+				d := path.Join(dst, name)
+				cStatus, cErr := copyFiles(fs, s, d, overwrite, depth, recursion)
+				if cErr != nil {
+					// TODO: MultiStatus.
+					return cStatus, cErr
+				}
+			}
+		}
+
+	} else {
+		dstFile, err := fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, srcPerm)
+		if err != nil {
+			if os.IsNotExist(err) {
+				return http.StatusConflict, err
+			}
+			return http.StatusForbidden, err
+
+		}
+		_, copyErr := io.Copy(dstFile, srcFile)
+		closeErr := dstFile.Close()
+		if copyErr != nil {
+			return http.StatusForbidden, copyErr
+		}
+		if closeErr != nil {
+			return http.StatusForbidden, closeErr
+		}
+	}
+
+	if created {
+		return http.StatusCreated, nil
+	}
+	return http.StatusNoContent, nil
+}