changeset 22272:651fa80715a3

lib: file_create_locked() - Add settings to mkdir() missing parent directories
author Timo Sirainen <timo.sirainen@dovecot.fi>
date Wed, 28 Jun 2017 17:48:01 +0300
parents 5c7987bec546
children a8b639453bee
files src/lib/file-create-locked.c src/lib/file-create-locked.h src/lib/test-file-create-locked.c
diffstat 3 files changed, 103 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/src/lib/file-create-locked.c	Wed Jun 28 17:40:20 2017 +0300
+++ b/src/lib/file-create-locked.c	Wed Jun 28 17:48:01 2017 +0300
@@ -3,6 +3,7 @@
 #include "lib.h"
 #include "str.h"
 #include "safe-mkstemp.h"
+#include "mkdir-parents.h"
 #include "file-lock.h"
 #include "file-create-locked.h"
 
@@ -10,6 +11,10 @@
 #include <fcntl.h>
 #include <sys/stat.h>
 
+/* Try mkdir() + lock creation multiple times. This allows the lock file
+   creation to work even while the directory is simultaneously being
+   rmdir()ed. */
+#define MAX_MKDIR_COUNT 10
 #define MAX_RETRY_COUNT 1000
 
 static int
@@ -44,24 +49,61 @@
 }
 
 static int
+try_mkdir(const char *path, const struct file_create_settings *set,
+	  const char **error_r)
+{
+	uid_t uid = set->mkdir_uid != 0 ? set->mkdir_uid : (uid_t)-1;
+	gid_t gid = set->mkdir_gid != 0 ? set->mkdir_gid : (gid_t)-1;
+	const char *p = strrchr(path, '/');
+	if (p == NULL)
+		return 0;
+
+	const char *dir = t_strdup_until(path, p);
+	int ret;
+	if (uid != (uid_t)-1)
+		ret = mkdir_parents_chown(dir, set->mkdir_mode, uid, gid);
+	else {
+		ret = mkdir_parents_chgrp(dir, set->mkdir_mode,
+					  gid, set->gid_origin);
+	}
+	if (ret < 0) {
+		*error_r = t_strdup_printf("mkdir_parents(%s) failed: %m", dir);
+		return -1;
+	}
+	return 1;
+}
+
+static int
 try_create_new(const char *path, const struct file_create_settings *set,
 	       int *fd_r, struct file_lock **lock_r,  const char **error_r)
 {
 	string_t *temp_path = t_str_new(128);
-	int fd, orig_errno, ret = -1;
+	int fd, orig_errno, ret = 1;
 	int mode = set->mode != 0 ? set->mode : 0600;
 	uid_t uid = set->uid != 0 ? set->uid : (uid_t)-1;
 	uid_t gid = set->gid != 0 ? set->gid : (gid_t)-1;
 
 	str_append(temp_path, path);
-	if (uid != (uid_t)-1)
-		fd = safe_mkstemp(temp_path, mode, uid, gid);
-	else
-		fd = safe_mkstemp_group(temp_path, mode, gid, set->gid_origin);
+	for (unsigned int i = 0; ret > 0; i++) {
+		if (uid != (uid_t)-1)
+			fd = safe_mkstemp(temp_path, mode, uid, gid);
+		else
+			fd = safe_mkstemp_group(temp_path, mode, gid, set->gid_origin);
+		if (fd != -1 || errno != ENOENT || set->mkdir_mode == 0 ||
+		    i >= MAX_MKDIR_COUNT)
+			break;
+
+		int orig_errno = errno;
+		if ((ret = try_mkdir(path, set, error_r)) < 0)
+			return -1;
+		errno = orig_errno;
+	}
 	if (fd == -1) {
 		*error_r = t_strdup_printf("safe_mkstemp(%s) failed: %m", path);
 		return -1;
 	}
+
+	ret = -1;
 	if (file_try_lock_error(fd, str_c(temp_path), F_WRLCK,
 				set->lock_method, lock_r, error_r) <= 0) {
 	} else if (link(str_c(temp_path), path) < 0) {
--- a/src/lib/file-create-locked.h	Wed Jun 28 17:40:20 2017 +0300
+++ b/src/lib/file-create-locked.h	Wed Jun 28 17:48:01 2017 +0300
@@ -15,6 +15,17 @@
 	/* 0 = default */
 	gid_t gid;
 	const char *gid_origin;
+
+	/* If parent directory doesn't exist, mkdir() it with this mode.
+	   0 = don't mkdir(). The parent directories are assumed to be
+	   potentially rmdir() simultaneously, so the mkdir()+locking may be
+	   attempted multiple times. */
+	int mkdir_mode;
+	/* 0 = default */
+	uid_t mkdir_uid;
+	/* 0 = default */
+	gid_t mkdir_gid;
+	const char *mkdir_gid_origin;
 };
 
 /* Either open an existing file and lock it, or create the file locked.
--- a/src/lib/test-file-create-locked.c	Wed Jun 28 17:40:20 2017 +0300
+++ b/src/lib/test-file-create-locked.c	Wed Jun 28 17:48:01 2017 +0300
@@ -1,6 +1,7 @@
 /* Copyright (c) 2017 Dovecot authors, see the included COPYING file */
 
 #include "test-lib.h"
+#include "unlink-directory.h"
 #include "file-create-locked.h"
 
 #include <fcntl.h>
@@ -85,7 +86,51 @@
 	test_end();
 }
 
+static void test_file_create_locked_mkdir(void)
+{
+	struct file_create_settings set = {
+		.lock_timeout_secs = 0,
+		.lock_method = FILE_LOCK_METHOD_FCNTL,
+	};
+	const char *path = ".test-file-create-locked";
+	struct file_lock *lock;
+	const char *error, *dir;
+	bool created;
+	int fd;
+
+	test_begin("file_create_locked() with mkdir");
+
+	dir = ".test-temp-file-create-locked-dir";
+	if (unlink_directory(dir, UNLINK_DIRECTORY_FLAG_RMDIR) < 0)
+		i_fatal("unlink_directory(%s) failed: %m", dir);
+	path = t_strconcat(dir, "/lockfile", NULL);
+
+	/* try without mkdir enabled */
+	test_assert(file_create_locked(path, &set, &lock, &created, &error) == -1);
+	test_assert(errno == ENOENT);
+
+	/* try with mkdir enabled */
+	set.mkdir_mode = 0700;
+	fd = file_create_locked(path, &set, &lock, &created, &error);
+	test_assert(fd > 0);
+	test_assert(created);
+	i_close_fd(&fd);
+
+	struct stat st;
+	if (stat(dir, &st) < 0)
+		i_error("stat(%s) failed: %m", dir);
+	test_assert((st.st_mode & 0777) == 0700);
+	i_unlink(path);
+	file_lock_free(&lock);
+
+	if (unlink_directory(dir, UNLINK_DIRECTORY_FLAG_RMDIR) < 0)
+		i_fatal("unlink_directory(%s) failed: %m", dir);
+
+	test_end();
+}
+
 void test_file_create_locked(void)
 {
 	test_file_create_locked_basic();
+	test_file_create_locked_mkdir();
 }