#! /bin/sh /usr/share/dpatch/dpatch-run
##
## All lines beginning with `## DP:' are a description of the patch.
## DP: Various bug fixes for mod_dav/mod_dav_fs
## DP: upstream svn revs 834018:835092 and 928403 in modules/dav
## DP:
## DP: *) mod_dav: Include uri when logging a PUT error due to connection abort.
## DP:    PR 38149. [Stefan Fritsch]
## DP:
## DP: *) mod_dav: Return 409 instead of 500 for a LOCK request if the parent
## DP:    resource does not exist or is not a collection. PR 43465. [Stefan Fritsch]
## DP:
## DP: *) mod_dav_fs: Return 409 instead of 500 for Litmus test case copy_nodestcoll
## DP:    (a COPY request where the parent of the destination resource does not
## DP:    exist). PR 39299. [Stefan Fritsch]
## DP:
## DP: *) mod_dav_fs: Don't delete the whole file if a PUT with content-range failed.
## DP:    PR 42896. [Stefan Fritsch]
## DP:
## DP: *) mod_dav_fs: Make PUT create files atomically and no longer destroy the
## DP:    old file if the transfer aborted. PR 39815. [Paul Querna, Stefan Fritsch]
## DP:
## DP: *) mod_dav_fs: Remove inode keyed locking as this conflicts with atomically
## DP:    creating files. On systems with inode numbers, this is a format change of
## DP:    the DavLockDB. The old DavLockDB must be deleted on upgrade.
## DP:    [Stefan Fritsch]
@DPATCH@
--- a/modules/dav/fs/lock.c
+++ b/modules/dav/fs/lock.c
@@ -48,9 +48,8 @@
 **
 ** KEY
 **
-** The database is keyed by a key_type unsigned char (DAV_TYPE_INODE or
-** DAV_TYPE_FNAME) followed by inode and device number if possible,
-** otherwise full path (in the case of Win32 or lock-null resources).
+** The database is keyed by a key_type unsigned char (DAV_TYPE_FNAME)
+** followed by the full path. The key_type DAV_TYPE_INODE is not used anymore.
 **
 ** VALUE
 **
@@ -82,7 +81,10 @@
 #define DAV_LOCK_DIRECT         1
 #define DAV_LOCK_INDIRECT       2
 
-#define DAV_TYPE_INODE          10
+/*
+ * not used anymore
+ * #define DAV_TYPE_INODE          10
+ */
 #define DAV_TYPE_FNAME          11
 
 
@@ -372,12 +374,13 @@
 }
 
 /*
-** dav_fs_build_fname_key
-**
-** Given a pathname, build a DAV_TYPE_FNAME lock database key.
+** dav_fs_build_key:  Given a resource, return a apr_datum_t key
+**    to look up lock information for this file.
 */
-static apr_datum_t dav_fs_build_fname_key(apr_pool_t *p, const char *pathname)
+static apr_datum_t dav_fs_build_key(apr_pool_t *p,
+                                    const dav_resource *resource)
 {
+    const char *pathname = dav_fs_pathname(resource);
     apr_datum_t key;
 
     /* ### does this allocation have a proper lifetime? need to check */
@@ -394,46 +397,6 @@
 }
 
 /*
-** dav_fs_build_key:  Given a resource, return a apr_datum_t key
-**    to look up lock information for this file.
-**
-**    (inode/dev not supported or file is lock-null):
-**       apr_datum_t->dvalue = full path
-**
-**    (inode/dev supported and file exists ):
-**       apr_datum_t->dvalue = inode, dev
-*/
-static apr_datum_t dav_fs_build_key(apr_pool_t *p,
-                                    const dav_resource *resource)
-{
-    const char *file = dav_fs_pathname(resource);
-    apr_datum_t key;
-    apr_finfo_t finfo;
-    apr_status_t rv;
-
-    /* ### use lstat() ?? */
-    /*
-     * XXX: What for platforms with no IDENT (dev/inode)?
-     */
-    rv = apr_stat(&finfo, file, APR_FINFO_IDENT, p);
-    if ((rv == APR_SUCCESS || rv == APR_INCOMPLETE)
-        && ((finfo.valid & APR_FINFO_IDENT) == APR_FINFO_IDENT))
-    {
-        /* ### can we use a buffer for this? */
-        key.dsize = 1 + sizeof(finfo.inode) + sizeof(finfo.device);
-        key.dptr = apr_palloc(p, key.dsize);
-        *key.dptr = DAV_TYPE_INODE;
-        memcpy(key.dptr + 1, &finfo.inode, sizeof(finfo.inode));
-        memcpy(key.dptr + 1 + sizeof(finfo.inode), &finfo.device,
-               sizeof(finfo.device));
-
-        return key;
-    }
-
-    return dav_fs_build_fname_key(p, file);
-}
-
-/*
 ** dav_fs_lock_expired:  return 1 (true) if the given timeout is in the past
 **    or present (the lock has expired), or 0 (false) if in the future
 **    (the lock has not yet expired).
@@ -989,13 +952,8 @@
 
 /*
 ** dav_fs_remove_locknull_state:  Given a request, check to see if r->filename
-**    is/was a lock-null resource.  If so, return it to an existant state.
-**
-**    ### this function is broken... it doesn't check!
-**
-**    In this implementation, this involves two things:
-**    (a) remove it from the list in the appropriate .DAV/locknull file
-**    (b) on *nix, convert the key from a filename to an inode.
+**    is/was a lock-null resource.  If so, return it to an existant state, i.e.
+**    remove it from the list in the appropriate .DAV/locknull file.
 */
 static dav_error * dav_fs_remove_locknull_state(
     dav_lockdb *lockdb,
@@ -1011,35 +969,6 @@
         return err;
     }
 
-    {
-        dav_lock_discovery *ld;
-        dav_lock_indirect  *id;
-        apr_datum_t key;
-
-        /*
-        ** Fetch the lock(s) that made the resource lock-null. Remove
-        ** them under the filename key. Obtain the new inode key, and
-        ** save the same lock information under it.
-        */
-        key = dav_fs_build_fname_key(p, pathname);
-        if ((err = dav_fs_load_lock_record(lockdb, key, DAV_CREATE_LIST,
-                                           &ld, &id)) != NULL) {
-            /* ### insert a higher-level error description */
-            return err;
-        }
-
-        if ((err = dav_fs_save_lock_record(lockdb, key, NULL, NULL)) != NULL) {
-            /* ### insert a higher-level error description */
-            return err;
-        }
-
-        key = dav_fs_build_key(p, resource);
-        if ((err = dav_fs_save_lock_record(lockdb, key, ld, id)) != NULL) {
-            /* ### insert a higher-level error description */
-            return err;
-        }
-    }
-
     return NULL;
 }
 
--- a/modules/dav/fs/repos.c
+++ b/modules/dav/fs/repos.c
@@ -27,6 +27,10 @@
 #include <stdio.h>              /* for sprintf() */
 #endif
 
+#if APR_HAVE_UNISTD_H
+#include <unistd.h>             /* for getpid() */
+#endif
+
 #include "httpd.h"
 #include "http_log.h"
 #include "http_protocol.h"      /* for ap_set_* (in dav_fs_set_headers) */
@@ -139,6 +143,11 @@
 */
 #define DAV_PROPID_FS_executable        1
 
+/*
+ * prefix for temporary files
+ */
+#define DAV_FS_TMP_PREFIX ".davfs.tmp" 
+
 static const dav_liveprop_spec dav_fs_props[] =
 {
     /* standard DAV properties */
@@ -191,11 +200,14 @@
     apr_pool_t *p;
     apr_file_t *f;
     const char *pathname;       /* we may need to remove it at close time */
+    char *temppath;
+    int unlink_on_error;
 };
 
 /* returns an appropriate HTTP status code given an APR status code for a
  * failed I/O operation.  ### use something besides 500? */
 #define MAP_IO2HTTP(e) (APR_STATUS_IS_ENOSPC(e) ? HTTP_INSUFFICIENT_STORAGE : \
+                        APR_STATUS_IS_ENOENT(e) ? HTTP_CONFLICT : \
                         HTTP_INTERNAL_SERVER_ERROR)
 
 /* forward declaration for internal treewalkers */
@@ -414,11 +426,24 @@
     apr_file_close(inf);
     apr_file_close(outf);
 
-    if (is_move && apr_file_remove(src, p) != APR_SUCCESS) {
+    if (is_move && (status = apr_file_remove(src, p)) != APR_SUCCESS) {
         dav_error *err;
         int save_errno = errno;   /* save the errno that got us here */
 
-        if (apr_file_remove(dst, p) != APR_SUCCESS) {
+        if (APR_STATUS_IS_ENOENT(status)) {
+            /*
+             * Something is wrong here but the result is what we wanted.
+             * We definitely should not remove the destination file.
+             */
+             err = dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0,
+                                 apr_psprintf(p, "Could not remove source "
+                                              "file %s after move to %s. The "
+                                              "server may be in an "
+                                              "inconsistent state.", src, dst));
+            err->save_errno = save_errno;
+            return err;
+        } 
+        else if (apr_file_remove(dst, p) != APR_SUCCESS) {
             /* ### ACK. this creates an inconsistency. do more!? */
 
             /* ### use something besides 500? */
@@ -500,9 +525,13 @@
     dst = apr_pstrcat(p, dst, "/", dst_file, NULL);
 
     /* copy/move the file now */
-    if (is_move && src_finfo.device == dst_state_finfo.device) {
-        /* simple rename is possible since it is on the same device */
-        if (apr_file_rename(src, dst, p) != APR_SUCCESS) {
+    if (is_move) {
+        /* try simple rename first */
+        rv = apr_file_rename(src, dst, p);
+        if (APR_STATUS_IS_EXDEV(rv)) {
+            return dav_fs_copymove_file(is_move, p, src, dst, NULL, NULL, pbuf);
+        }
+        if (rv != APR_SUCCESS) {
             /* ### use something besides 500? */
             return dav_new_error(p, HTTP_INTERNAL_SERVER_ERROR, 0,
                                  "Could not move state file.");
@@ -841,6 +870,34 @@
             && ctx2->pathname[len1] == '/');
 }
 
+static apr_status_t tmpfile_cleanup(void *data) {
+        dav_stream *ds = data;
+        if (ds->temppath) {
+                apr_file_remove(ds->temppath, ds->p);
+        }
+        return APR_SUCCESS;
+}
+
+/* custom mktemp that creates the file with APR_OS_DEFAULT permissions */
+static apr_status_t dav_fs_mktemp(apr_file_t **fp, char *templ, apr_pool_t *p)
+{
+    apr_status_t rv;
+    int num = ((getpid() << 7) + (int)templ % (1 << 16) ) % ( 1 << 23 ) ;
+    char *numstr = templ + strlen(templ) - 6;
+
+    ap_assert(numstr >= templ);
+
+    do {
+        num = (num + 1) % ( 1 << 23 );
+        snprintf(numstr, 7, "%06x", num);
+        rv = apr_file_open(fp, templ,
+                           APR_WRITE | APR_CREATE | APR_BINARY | APR_EXCL,
+                           APR_OS_DEFAULT, p);
+    } while (APR_STATUS_IS_EEXIST(rv));
+        
+    return rv;
+}
+
 static dav_error * dav_fs_open_stream(const dav_resource *resource,
                                       dav_stream_mode mode,
                                       dav_stream **stream)
@@ -865,7 +922,32 @@
 
     ds->p = p;
     ds->pathname = resource->info->pathname;
-    rv = apr_file_open(&ds->f, ds->pathname, flags, APR_OS_DEFAULT, ds->p);
+    ds->temppath = NULL;
+    ds->unlink_on_error = 0;
+
+    if (mode == DAV_MODE_WRITE_TRUNC) {
+        ds->temppath = apr_pstrcat(p, ap_make_dirstr_parent(p, ds->pathname),
+                                   DAV_FS_TMP_PREFIX "XXXXXX", NULL);
+        rv = dav_fs_mktemp(&ds->f, ds->temppath, ds->p);
+        apr_pool_cleanup_register(p, ds, tmpfile_cleanup,
+                                  apr_pool_cleanup_null);
+    }
+    else if (mode == DAV_MODE_WRITE_SEEKABLE) {
+        rv = apr_file_open(&ds->f, ds->pathname, flags | APR_FOPEN_EXCL,
+                           APR_OS_DEFAULT, ds->p);
+        if (rv == APR_SUCCESS) {
+            /* we have created a new file */
+            ds->unlink_on_error = 1;
+        }
+        else if (APR_STATUS_IS_EEXIST(rv)) {
+            rv = apr_file_open(&ds->f, ds->pathname, flags, APR_OS_DEFAULT,
+                               ds->p);
+        }
+    }
+    else {
+        rv = apr_file_open(&ds->f, ds->pathname, flags, APR_OS_DEFAULT, ds->p);
+    }
+
     if (rv != APR_SUCCESS) {
         return dav_new_error(p, MAP_IO2HTTP(rv), 0,
                              "An error occurred while opening a resource.");
@@ -879,17 +961,33 @@
 
 static dav_error * dav_fs_close_stream(dav_stream *stream, int commit)
 {
+    apr_status_t rv;
+
     apr_file_close(stream->f);
 
     if (!commit) {
-        if (apr_file_remove(stream->pathname, stream->p) != APR_SUCCESS) {
-            /* ### use a better description? */
-            return dav_new_error(stream->p, HTTP_INTERNAL_SERVER_ERROR, 0,
-                                 "There was a problem removing (rolling "
-                                 "back) the resource "
-                                 "when it was being closed.");
+        if (stream->temppath) {
+            apr_pool_cleanup_run(stream->p, stream, tmpfile_cleanup);
+        }
+        else if (stream->unlink_on_error) {
+            if (apr_file_remove(stream->pathname, stream->p) != APR_SUCCESS) {
+                /* ### use a better description? */
+                return dav_new_error(stream->p, HTTP_INTERNAL_SERVER_ERROR, 0,
+                                     "There was a problem removing (rolling "
+                                     "back) the resource "
+                                     "when it was being closed.");
+            }
         }
     }
+    else if (stream->temppath) {
+        rv = apr_file_rename(stream->temppath, stream->pathname, stream->p);
+        if (rv) {
+            return dav_new_error(stream->p, HTTP_INTERNAL_SERVER_ERROR, rv,
+                                 "There was a problem writing the file "
+                                 "atomically after writes.");
+        }
+        apr_pool_cleanup_kill(stream->p, stream, tmpfile_cleanup);
+    }
 
     return NULL;
 }
@@ -1201,7 +1299,7 @@
     dav_resource_private *srcinfo = src->info;
     dav_resource_private *dstinfo = dst->info;
     dav_error *err;
-    int can_rename = 0;
+    apr_status_t rv;
 
 #if DAV_DEBUG
     if (src->hooks != dst->hooks) {
@@ -1215,39 +1313,12 @@
     }
 #endif
 
-    /* determine whether a simple rename will work.
-     * Assume source exists, else we wouldn't get called.
-     */
-    if (dstinfo->finfo.filetype != 0) {
-        if (dstinfo->finfo.device == srcinfo->finfo.device) {
-            /* target exists and is on the same device. */
-            can_rename = 1;
-        }
-    }
-    else {
-        const char *dirpath;
-        apr_finfo_t finfo;
-        apr_status_t rv;
 
-        /* destination does not exist, but the parent directory should,
-         * so try it
-         */
-        dirpath = ap_make_dirstr_parent(dstinfo->pool, dstinfo->pathname);
-        /*
-         * XXX: If missing dev ... then what test?
-         * Really need a try and failover for those platforms.
-         *
-         */
-        rv = apr_stat(&finfo, dirpath, APR_FINFO_DEV, dstinfo->pool);
-        if ((rv == APR_SUCCESS || rv == APR_INCOMPLETE)
-            && (finfo.valid & srcinfo->finfo.valid & APR_FINFO_DEV)
-            && (finfo.device == srcinfo->finfo.device)) {
-            can_rename = 1;
-        }
-    }
+    /* try rename first */
+    rv = apr_file_rename(srcinfo->pathname, dstinfo->pathname, srcinfo->pool);
 
     /* if we can't simply rename, then do it the hard way... */
-    if (!can_rename) {
+    if (APR_STATUS_IS_EXDEV(rv)) {
         if ((err = dav_fs_copymove_resource(1, src, dst, DAV_INFINITY,
                                             response)) == NULL) {
             /* update resource states */
@@ -1260,20 +1331,16 @@
         return err;
     }
 
-    /* a rename should work. do it, and move properties as well */
-
     /* no multistatus response */
     *response = NULL;
 
-    /* ### APR has no rename? */
-    if (apr_file_rename(srcinfo->pathname, dstinfo->pathname,
-                       srcinfo->pool) != APR_SUCCESS) {
+    if (rv != APR_SUCCESS) {
         /* ### should have a better error than this. */
         return dav_new_error(srcinfo->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
                              "Could not rename resource.");
     }
 
-    /* update resource states */
+    /* Rename did work. Update resource states and move properties as well */
     dst->exists = 1;
     dst->collection = src->collection;
     src->exists = 0;
@@ -1475,14 +1542,18 @@
             /* ### need to authorize each file */
             /* ### example: .htaccess is normally configured to fail auth */
 
-            /* stuff in the state directory is never authorized! */
-            if (!strcmp(dirent.name, DAV_FS_STATE_DIR)) {
+            /* stuff in the state directory and temp files are never authorized! */
+            if (!strcmp(dirent.name, DAV_FS_STATE_DIR) ||
+                !strncmp(dirent.name, DAV_FS_TMP_PREFIX,
+                         strlen(DAV_FS_TMP_PREFIX))) {
                 continue;
             }
         }
-        /* skip the state dir unless a HIDDEN is performed */
+        /* skip the state dir and temp files unless a HIDDEN is performed */
         if (!(params->walk_type & DAV_WALKTYPE_HIDDEN)
-            && !strcmp(dirent.name, DAV_FS_STATE_DIR)) {
+            && (!strcmp(dirent.name, DAV_FS_STATE_DIR) ||
+                !strncmp(dirent.name, DAV_FS_TMP_PREFIX,
+                         strlen(DAV_FS_TMP_PREFIX)))) {
             continue;
         }
 
--- a/modules/dav/main/mod_dav.c
+++ b/modules/dav/main/mod_dav.c
@@ -998,7 +998,10 @@
 
             if (rc != APR_SUCCESS) {
                 err = dav_new_error(r->pool, HTTP_INTERNAL_SERVER_ERROR, 0,
-                                    "Could not get next bucket brigade");
+                                    apr_psprintf(r->pool,
+                                                 "Could not get next bucket "
+                                                 "brigade (URI: %s)",
+                                                 ap_escape_html(r->pool, r->uri)));
                 break;
             }
 
@@ -1021,8 +1024,10 @@
                 rc = apr_bucket_read(b, &data, &len, APR_BLOCK_READ);
                 if (rc != APR_SUCCESS) {
                     err = dav_new_error(r->pool, HTTP_BAD_REQUEST, 0,
-                                        "An error occurred while reading "
-                                        "the request body.");
+                                        apr_psprintf(r->pool,
+                                                    "An error occurred while reading"
+                                                    " the request body (URI: %s)",
+                                                    ap_escape_html(r->pool, r->uri)));
                     break;
                 }
 
@@ -2978,6 +2983,7 @@
 {
     dav_error *err;
     dav_resource *resource;
+    dav_resource *parent;
     const dav_hooks_locks *locks_hooks;
     int result;
     int depth;
@@ -3009,6 +3015,20 @@
     if (err != NULL)
         return dav_handle_err(r, err, NULL);
 
+    /* Check if parent collection exists */
+    if ((err = resource->hooks->get_parent_resource(resource, &parent)) != NULL) {
+        /* ### add a higher-level description? */
+        return dav_handle_err(r, err, NULL);
+    }
+    if (parent && (!parent->exists || parent->collection != 1)) {
+        err = dav_new_error(r->pool, HTTP_CONFLICT, 0,
+                           apr_psprintf(r->pool,
+                                        "The parent resource of %s does not "
+                                        "exist or is not a collection.", 
+                                        ap_escape_html(r->pool, r->uri)));
+        return dav_handle_err(r, err, NULL);
+    }
+
     /*
      * Open writable. Unless an error occurs, we'll be
      * writing into the database.
