/* Copyright (c) 2010-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "buffer.h"
#include "str.h"
#include "guid.h"
#include "istream.h"
#include "ostream.h"
#include "safe-mkstemp.h"
#include "mkdir-parents.h"
#include "write-full.h"
#include "file-lock.h"
#include "file-dotlock.h"
#include "time-util.h"
#include "settings.h"
#include "fs-api-private.h"

#include <stdio.h>
#include <unistd.h>
#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>

#define FS_POSIX_DOTLOCK_STALE_TIMEOUT_SECS (60*10)
#define MAX_MKDIR_RETRY_COUNT 5

enum fs_posix_lock_method {
	FS_POSIX_LOCK_METHOD_FLOCK,
	FS_POSIX_LOCK_METHOD_DOTLOCK
};

struct fs_posix_settings {
	pool_t pool;
	const char *fs_posix_lock_method;
	const char *fs_posix_prefix;
	unsigned int fs_posix_mode;
	bool fs_posix_autodelete_empty_directories;
	bool fs_posix_fsync;
	bool fs_posix_accurate_mtime;

	enum fs_posix_lock_method parsed_lock_method;
};

struct posix_fs {
	struct fs fs;
	char *temp_file_prefix, *root_path;
	size_t temp_file_prefix_len;
	const struct fs_posix_settings *set;
};

struct posix_fs_file {
	struct fs_file file;
	char *temp_path, *full_path;
	int fd;
	enum fs_open_mode open_mode;
	enum fs_open_flags open_flags;

	buffer_t *write_buf;

	bool seek_to_beginning;
};

struct posix_fs_lock {
	struct fs_lock lock;
	struct file_lock *file_lock;
	struct dotlock *dotlock;
};

struct posix_fs_iter {
	struct fs_iter iter;
	char *path;
	DIR *dir;
	int err;
};

static bool fs_posix_settings_check(void *_set, pool_t pool, const char **error_r);

#undef DEF
#define DEF(type, name) \
	SETTING_DEFINE_STRUCT_##type(#name, name, struct fs_posix_settings)
static const struct setting_define fs_posix_setting_defines[] = {
	DEF(ENUM, fs_posix_lock_method),
	DEF(STR, fs_posix_prefix),
	DEF(UINT_OCT, fs_posix_mode),
	DEF(BOOL_HIDDEN, fs_posix_autodelete_empty_directories),
	DEF(BOOL, fs_posix_fsync),
	DEF(BOOL, fs_posix_accurate_mtime),

	SETTING_DEFINE_LIST_END
};
static const struct fs_posix_settings fs_posix_default_settings = {
	.fs_posix_lock_method = "flock:dotlock",
	.fs_posix_prefix = "",
	.fs_posix_mode = 0600,
	.fs_posix_autodelete_empty_directories = FALSE,
	.fs_posix_fsync = TRUE,
	.fs_posix_accurate_mtime = FALSE,
};

const struct setting_parser_info fs_posix_setting_parser_info = {
	.name = "fs_posix",

	.defines = fs_posix_setting_defines,
	.defaults = &fs_posix_default_settings,

	.struct_size = sizeof(struct fs_posix_settings),
	.pool_offset1 = 1 + offsetof(struct fs_posix_settings, pool),
	.check_func = fs_posix_settings_check,
};

static struct fs *fs_posix_alloc(void)
{
	struct posix_fs *fs;

	fs = i_new(struct posix_fs, 1);
	fs->fs = fs_class_posix;
	return &fs->fs;
}

static bool fs_posix_settings_check(void *_set, pool_t pool ATTR_UNUSED,
				    const char **error_r)
{
	struct fs_posix_settings *set = _set;

	if (strcmp(set->fs_posix_lock_method, "flock") == 0)
		set->parsed_lock_method = FS_POSIX_LOCK_METHOD_FLOCK;
	else if (strcmp(set->fs_posix_lock_method, "dotlock") == 0)
		set->parsed_lock_method = FS_POSIX_LOCK_METHOD_DOTLOCK;
	else {
		*error_r = "Invalid fs_posix_lock_method";
		return FALSE;
	}
	return TRUE;
}

static void fs_posix_init_common(struct posix_fs *fs,
				 const struct fs_parameters *params)
{
	fs->temp_file_prefix = params->temp_file_prefix != NULL ?
		i_strdup(params->temp_file_prefix) : i_strdup("temp.dovecot.");
	fs->temp_file_prefix_len = strlen(fs->temp_file_prefix);
	fs->root_path = i_strdup(params->root_path);
}

static int
fs_posix_init(struct fs *_fs, const struct fs_parameters *params,
	      const char **error_r)
{
	struct posix_fs *fs = container_of(_fs, struct posix_fs, fs);

	if (settings_get(_fs->event, &fs_posix_setting_parser_info, 0,
			 &fs->set, error_r) < 0)
		return -1;

	fs_posix_init_common(fs, params);
	return 0;
}

static void fs_posix_free(struct fs *_fs)
{
	struct posix_fs *fs = container_of(_fs, struct posix_fs, fs);

	settings_free(fs->set);
	i_free(fs->temp_file_prefix);
	i_free(fs->root_path);
	i_free(fs);
}

static enum fs_properties fs_posix_get_properties(struct fs *_fs)
{
	struct posix_fs *fs = container_of(_fs, struct posix_fs, fs);
	enum fs_properties props =
		FS_PROPERTY_LOCKS | FS_PROPERTY_FASTCOPY | FS_PROPERTY_RENAME |
		FS_PROPERTY_STAT | FS_PROPERTY_ITER | FS_PROPERTY_RELIABLEITER;

	/* FS_PROPERTY_DIRECTORIES is not returned normally because fs_delete()
	   automatically rmdir()s parents. For backwards compatibility
	   (especially with SIS code) we'll do it that way, but optionally with
	   "dirs" parameter enable them. This is especially important to be
	   able to use doveadm fs commands to delete empty directories. */
	if (!fs->set->fs_posix_autodelete_empty_directories)
		props |= FS_PROPERTY_DIRECTORIES;
	return props;
}

static int
fs_posix_get_mode(struct posix_fs_file *file, const char *path, mode_t *mode_r)
{
	struct posix_fs *fs = (struct posix_fs *)file->file.fs;
	struct stat st;
	const char *p;

	*mode_r = fs->set->fs_posix_mode;

	/* This function is used to get mode of the parent directory, so path
	   is never the same as file->path. The file is used just to set the
	   errors. */
	while (stat(path, &st) < 0) {
		if (errno != ENOENT) {
			fs_set_error_errno(file->file.event,
					   "stat(%s) failed: %m", path);
			return -1;
		}
		p = strrchr(path, '/');
		if (p != NULL)
			path = t_strdup_until(path, p);
		else if (strcmp(path, ".") != 0)
			path = ".";
		else
			return 0;
	}
	if ((st.st_mode & S_ISGID) != 0) {
		/* setgid set - copy mode from parent */
		*mode_r = st.st_mode & 0666;
	}
	return 0;
}

static int fs_posix_mkdir_parents(struct posix_fs_file *file, const char *path)
{
	const char *dir, *fname;
	mode_t mode, dir_mode;

	fname = strrchr(path, '/');
	if (fname == NULL)
		return 1;
	dir = t_strdup_until(path, fname);

	if (fs_posix_get_mode(file, dir, &mode) < 0)
		return -1;
	dir_mode = mode;
	if ((dir_mode & 0600) != 0) dir_mode |= 0100;
	if ((dir_mode & 0060) != 0) dir_mode |= 0010;
	if ((dir_mode & 0006) != 0) dir_mode |= 0001;

	if (mkdir_parents(dir, dir_mode) == 0)
		return 0;
	else if (errno == EEXIST)
		return 1;
	else {
		fs_set_error_errno(file->file.event,
				   "mkdir_parents(%s) failed: %m", dir);
		return -1;
	}
}

static int fs_posix_rmdir_parents(struct posix_fs_file *file, const char *path)
{
	struct posix_fs *fs = (struct posix_fs *)file->file.fs;
	const char *p;

	if (!fs->set->fs_posix_autodelete_empty_directories)
		return 0;
	if (fs->root_path == NULL && fs->set->fs_posix_prefix[0] == '\0')
		return 0;

	while ((p = strrchr(path, '/')) != NULL) {
		path = t_strdup_until(path, p);
		if ((fs->root_path != NULL && strcmp(path, fs->root_path) == 0) ||
		    str_begins_with(fs->set->fs_posix_prefix, path))
			break;
		if (rmdir(path) == 0) {
			/* success, continue to parent */
		} else if (errno == ENOTEMPTY || errno == EEXIST) {
			/* there are other entries in this directory */
			break;
		} else if (errno == EBUSY || errno == ENOENT) {
			/* some other not-unexpected error */
			break;
		} else {
			fs_set_error_errno(file->file.event,
					   "rmdir(%s) failed: %m", path);
			return -1;
		}
	}
	return 0;
}

static int fs_posix_create(struct posix_fs_file *file)
{
	struct posix_fs *fs = container_of(file->file.fs, struct posix_fs, fs);
	string_t *str = t_str_new(256);
	const char *slash;
	unsigned int try_count = 0;
	mode_t mode;
	int fd;

	i_assert(file->temp_path == NULL);

	if ((slash = strrchr(file->full_path, '/')) != NULL) {
		str_append_data(str, file->full_path, slash - file->full_path);
		if (fs_posix_get_mode(file, str_c(str), &mode) < 0)
			return -1;
		str_append_c(str, '/');
	} else {
		if (fs_posix_get_mode(file, ".", &mode) < 0)
			return -1;
	}
	str_append(str, fs->temp_file_prefix);

	fd = safe_mkstemp_hostpid(str, mode, (uid_t)-1, (gid_t)-1);
	while (fd == -1 && errno == ENOENT &&
	       try_count <= MAX_MKDIR_RETRY_COUNT) {
		if (fs_posix_mkdir_parents(file, str_c(str)) < 0)
			return -1;
		fd = safe_mkstemp_hostpid(str, mode, (uid_t)-1, (gid_t)-1);
		try_count++;
	}
	if (fd == -1) {
		fs_set_error_errno(file->file.event,
				   "safe_mkstemp(%s) failed: %m", str_c(str));
		return -1;
	}
	file->temp_path = i_strdup(str_c(str));
	return fd;
}

static int fs_posix_open(struct posix_fs_file *file)
{
	const char *path = file->full_path;

	i_assert(file->fd == -1);

	switch (file->open_mode) {
	case FS_OPEN_MODE_READONLY:
		file->fd = open(path, O_RDONLY);
		if (file->fd == -1)
			fs_set_error_errno(file->file.event,
					   "open(%s) failed: %m", path);
		break;
	case FS_OPEN_MODE_APPEND:
		file->fd = open(path, O_RDWR | O_APPEND);
		if (file->fd == -1)
			fs_set_error_errno(file->file.event,
					   "open(%s) failed: %m", path);
		break;
	case FS_OPEN_MODE_CREATE_UNIQUE_128:
	case FS_OPEN_MODE_CREATE:
	case FS_OPEN_MODE_REPLACE:
		T_BEGIN {
			file->fd = fs_posix_create(file);
		} T_END;
		break;
	}
	if (file->fd == -1)
		return -1;
	return 0;
}

static struct fs_file *fs_posix_file_alloc(void)
{
	struct posix_fs_file *file = i_new(struct posix_fs_file, 1);
	return &file->file;
}

static void
fs_posix_file_init(struct fs_file *_file, const char *path,
		   enum fs_open_mode mode, enum fs_open_flags flags)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);
	struct posix_fs *fs = container_of(_file->fs, struct posix_fs, fs);
	guid_128_t guid;
	size_t path_len = strlen(path);

	if (path_len > 0 && path[path_len-1] == '/') {
		/* deleting "path/" (used e.g. by doveadm fs delete) - strip
		   out the trailing "/" since it doesn't work well with NFS. */
		path = t_strndup(path, path_len-1);
	}

	if (mode != FS_OPEN_MODE_CREATE_UNIQUE_128)
		file->file.path = i_strdup(path);
	else {
		guid_128_generate(guid);
		file->file.path = i_strdup_printf("%s/%s", path,
						  guid_128_to_string(guid));
	}
	file->full_path = i_strconcat(fs->set->fs_posix_prefix,
				      file->file.path, NULL);
	file->open_mode = mode;
	file->open_flags = flags;
	file->fd = -1;
}

static void fs_posix_file_close(struct fs_file *_file)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);

	if (file->fd != -1 && file->file.output == NULL) {
		if (close(file->fd) < 0) {
			e_error(_file->event, "close(%s) failed: %m",
				file->full_path);
		}
		file->fd = -1;
	}
}

static bool fs_posix_file_equals(struct fs_file *_file1, struct fs_file *_file2)
{
	struct posix_fs_file *file1 =
		container_of(_file1, struct posix_fs_file, file);
	struct posix_fs_file *file2 =
		container_of(_file2, struct posix_fs_file, file);
	return strcmp(file1->full_path, file2->full_path) == 0;
}

static void fs_posix_file_deinit(struct fs_file *_file)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);

	i_assert(_file->output == NULL);

	switch (file->open_mode) {
	case FS_OPEN_MODE_READONLY:
	case FS_OPEN_MODE_APPEND:
		break;
	case FS_OPEN_MODE_CREATE_UNIQUE_128:
	case FS_OPEN_MODE_CREATE:
	case FS_OPEN_MODE_REPLACE:
		if (file->temp_path == NULL)
			break;
		/* failed to create/replace this. delete the temp file */
		if (unlink(file->temp_path) < 0) {
			e_error(_file->event, "unlink(%s) failed: %m",
				file->temp_path);
		}
		break;
	}

	fs_file_free(_file);
	i_free(file->temp_path);
	i_free(file->full_path);
	i_free(file->file.path);
	i_free(file);
}

static int fs_posix_open_for_read(struct posix_fs_file *file)
{
	i_assert(file->file.output == NULL);
	i_assert(file->temp_path == NULL);

	if (file->fd == -1) {
		if (fs_posix_open(file) < 0)
			return -1;
	}
	return 0;
}

static bool fs_posix_prefetch(struct fs_file *_file, uoff_t length ATTR_UNUSED)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);

	if (fs_posix_open_for_read(file) < 0)
		return TRUE;

/* HAVE_POSIX_FADVISE alone isn't enough for CentOS 4.9 */
#if defined(HAVE_POSIX_FADVISE) && defined(POSIX_FADV_WILLNEED)
	if ((errno = posix_fadvise(file->fd, 0, (off_t)length, POSIX_FADV_WILLNEED)) != 0) {
		e_error(_file->event, "posix_fadvise(%s) failed: %m", file->full_path);
		return TRUE;
	}
#endif
	return FALSE;
}

static ssize_t fs_posix_read(struct fs_file *_file, void *buf, size_t size)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);
	ssize_t ret;

	if (fs_posix_open_for_read(file) < 0)
		return -1;

	if (file->seek_to_beginning) {
		file->seek_to_beginning = FALSE;
		if (lseek(file->fd, 0, SEEK_SET) < 0) {
			fs_set_error_errno(_file->event,
					   "lseek(%s, 0) failed: %m",
					   file->full_path);
			return -1;
		}
	}

	ret = read(file->fd, buf, size);
	if (ret < 0)
		fs_set_error_errno(_file->event, "read(%s) failed: %m",
				   file->full_path);
	fs_posix_file_close(_file);
	return ret;
}

static struct istream *
fs_posix_read_stream(struct fs_file *_file, size_t max_buffer_size)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);
	struct istream *input;
	int fd_dup;

	if (fs_posix_open_for_read(file) < 0)
		input = i_stream_create_error_str(errno, "%s", fs_file_last_error(_file));
	else if ((fd_dup = dup(file->fd)) == -1)
		input = i_stream_create_error_str(errno, "dup() failed: %m");
	else {
		/* The stream could live even after the fs_file.
		   Don't use file->fd directly, because the fd may still be
		   used for other purposes. It's especially important for files
		   that were just created. */
		input = i_stream_create_fd_autoclose(&fd_dup, max_buffer_size);
	}
	i_stream_set_name(input, file->full_path);
	return input;
}

static void fs_posix_write_rename_if_needed(struct posix_fs_file *file)
{
	struct posix_fs *fs = container_of(file->file.fs, struct posix_fs, fs);
	const char *new_fname;

	new_fname = fs_metadata_find(&file->file.metadata, FS_METADATA_WRITE_FNAME);
	if (new_fname == NULL)
		return;

	i_free(file->file.path);
	file->file.path = i_strdup(new_fname);

	i_free(file->full_path);
	file->full_path = i_strconcat(fs->set->fs_posix_prefix,
				      file->file.path, NULL);
}

static int fs_posix_write_finish_link(struct posix_fs_file *file)
{
	unsigned int try_count = 0;
	int ret;

	ret = link(file->temp_path, file->full_path);
	while (ret < 0 && errno == ENOENT &&
	       try_count <= MAX_MKDIR_RETRY_COUNT) {
		if (fs_posix_mkdir_parents(file, file->full_path) < 0)
			return -1;
		ret = link(file->temp_path, file->full_path);
		try_count++;
	}
	if (ret < 0) {
		fs_set_error_errno(file->file.event, "link(%s, %s) failed: %m",
				   file->temp_path, file->full_path);
	}
	return ret;
}

static int fs_posix_write_finish(struct posix_fs_file *file)
{
	struct posix_fs *fs = container_of(file->file.fs, struct posix_fs, fs);
	unsigned int try_count = 0;
	int ret, old_errno;

	if ((file->open_flags & FS_OPEN_FLAG_FSYNC) != 0 &&
	    fs->set->fs_posix_fsync) {
		if (fdatasync(file->fd) < 0) {
			fs_set_error_errno(file->file.event,
					   "fdatasync(%s) failed: %m",
					   file->full_path);
			return -1;
		}
	}
	if (fs->set->fs_posix_accurate_mtime) {
		/* Linux updates the mtime timestamp only on timer interrupts.
		   This isn't anywhere close to being microsecond precision.
		   If requested, use utimes() to explicitly set a more accurate
		   mtime. */
		struct timeval tv[2];
		i_gettimeofday(&tv[0]);
		tv[1] = tv[0];
		if ((utimes(file->temp_path, tv)) < 0) {
			fs_set_error_errno(file->file.event,
					   "utimes(%s) failed: %m",
					   file->temp_path);
			return -1;
		}
	}

	fs_posix_write_rename_if_needed(file);
	switch (file->open_mode) {
	case FS_OPEN_MODE_CREATE_UNIQUE_128:
	case FS_OPEN_MODE_CREATE:
		ret = fs_posix_write_finish_link(file);
		old_errno = errno;
		if (unlink(file->temp_path) < 0) {
			fs_set_error_errno(file->file.event,
					   "unlink(%s) failed: %m",
					   file->temp_path);
		}
		errno = old_errno;
		if (ret < 0) {
			fs_posix_file_close(&file->file);
			i_free_and_null(file->temp_path);
			return -1;
		}
		break;
	case FS_OPEN_MODE_REPLACE:
		ret = rename(file->temp_path, file->full_path);
		while (ret < 0 && errno == ENOENT &&
		       try_count <= MAX_MKDIR_RETRY_COUNT) {
			if (fs_posix_mkdir_parents(file, file->full_path) < 0)
				return -1;
			ret = rename(file->temp_path, file->full_path);
			try_count++;
		}
		if (ret < 0) {
			fs_set_error_errno(file->file.event,
					   "rename(%s, %s) failed: %m",
					   file->temp_path, file->full_path);
			return -1;
		}
		break;
	default:
		i_unreached();
	}
	i_free_and_null(file->temp_path);
	file->seek_to_beginning = TRUE;
	/* allow opening the file after writing to it */
	file->open_mode = FS_OPEN_MODE_READONLY;
	return 0;
}

static int fs_posix_write(struct fs_file *_file, const void *data, size_t size)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);
	ssize_t ret;

	if (file->fd == -1) {
		if (fs_posix_open(file) < 0)
			return -1;
		i_assert(file->fd != -1);
	}

	if (file->open_mode != FS_OPEN_MODE_APPEND) {
		if (write_full(file->fd, data, size) < 0) {
			fs_set_error_errno(_file->event, "write(%s) failed: %m",
					   file->full_path);
			return -1;
		}
		return fs_posix_write_finish(file);
	}

	/* atomic append - it should either succeed or fail */
	ret = write(file->fd, data, size);
	if (ret < 0) {
		fs_set_error_errno(_file->event, "write(%s) failed: %m",
				   file->full_path);
		return -1;
	}
	if ((size_t)ret != size) {
		fs_set_error(_file->event, ENOSPC,
			     "write(%s) returned %zu/%zu",
			     file->full_path, (size_t)ret, size);
		return -1;
	}
	return 0;
}

static void fs_posix_write_stream(struct fs_file *_file)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);

	i_assert(_file->output == NULL);

	if (file->open_mode == FS_OPEN_MODE_APPEND) {
		file->write_buf = buffer_create_dynamic(default_pool, 1024*32);
		_file->output = o_stream_create_buffer(file->write_buf);
	} else if (file->fd == -1 && fs_posix_open(file) < 0) {
		_file->output = o_stream_create_error_str(errno, "%s",
			fs_file_last_error(_file));
	} else {
		i_assert(file->fd != -1);
		_file->output = o_stream_create_fd_file(file->fd,
							UOFF_T_MAX, FALSE);
	}
	o_stream_set_name(_file->output, file->full_path);
}

static int fs_posix_write_stream_finish(struct fs_file *_file, bool success)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);
	int ret = success ? 0 : -1;

	o_stream_destroy(&_file->output);

	switch (file->open_mode) {
	case FS_OPEN_MODE_APPEND:
		if (ret == 0) {
			ret = fs_posix_write(_file, file->write_buf->data,
					     file->write_buf->used);
		}
		buffer_free(&file->write_buf);
		break;
	case FS_OPEN_MODE_CREATE:
	case FS_OPEN_MODE_CREATE_UNIQUE_128:
	case FS_OPEN_MODE_REPLACE:
		if (ret == 0)
			ret = fs_posix_write_finish(file);
		break;
	case FS_OPEN_MODE_READONLY:
		i_unreached();
	}
	return ret < 0 ? -1 : 1;
}

static int
fs_posix_lock(struct fs_file *_file, unsigned int secs, struct fs_lock **lock_r)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);
	struct posix_fs *fs = container_of(_file->fs, struct posix_fs, fs);
	struct dotlock_settings dotlock_set;
	struct posix_fs_lock fs_lock, *ret_lock;
	int ret = -1;

	i_zero(&fs_lock);
	fs_lock.lock.file = _file;

	switch (fs->set->parsed_lock_method) {
	case FS_POSIX_LOCK_METHOD_FLOCK: {
#ifndef HAVE_FLOCK
		fs_set_error(_file->event, ENOTSUP,
			     "flock() not supported by OS (for file %s)",
			     file->full_path);
#else
		const char *error;
		struct file_lock_settings lock_set = {
			.lock_method = FILE_LOCK_METHOD_FLOCK,
		};
		if (secs == 0) {
			ret = file_try_lock(file->fd, file->full_path, F_WRLCK,
					    &lock_set, &fs_lock.file_lock,
					    &error);
		} else {
			ret = file_wait_lock(file->fd, file->full_path, F_WRLCK,
					     &lock_set, secs,
					     &fs_lock.file_lock, &error);
		}
		if (ret < 0) {
			fs_set_error_errno(_file->event, "flock(%s) failed: %s",
					   file->full_path, error);
		}
#endif
		break;
	}
	case FS_POSIX_LOCK_METHOD_DOTLOCK:
		i_zero(&dotlock_set);
		dotlock_set.stale_timeout = FS_POSIX_DOTLOCK_STALE_TIMEOUT_SECS;
		dotlock_set.use_excl_lock = TRUE;
		dotlock_set.timeout = secs;

		ret = file_dotlock_create(&dotlock_set, file->full_path,
					  secs == 0 ? 0 :
					  DOTLOCK_CREATE_FLAG_NONBLOCK,
					  &fs_lock.dotlock);
		if (ret < 0) {
			fs_set_error_errno(_file->event,
					   "file_dotlock_create(%s) failed: %m",
					   file->full_path);
		}
		break;
	}
	if (ret <= 0)
		return ret;

	ret_lock = i_new(struct posix_fs_lock, 1);
	*ret_lock = fs_lock;
	*lock_r = &ret_lock->lock;
	return 1;
}

static void fs_posix_unlock(struct fs_lock *_lock)
{
	struct posix_fs_lock *lock =
		container_of(_lock, struct posix_fs_lock, lock);

	if (lock->file_lock != NULL)
		file_unlock(&lock->file_lock);
	if (lock->dotlock != NULL)
		file_dotlock_delete(&lock->dotlock);
	i_free(lock);
}

static int fs_posix_exists(struct fs_file *_file)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);
	struct stat st;

	if (stat(file->full_path, &st) < 0) {
		if (errno != ENOENT) {
			fs_set_error_errno(_file->event, "stat(%s) failed: %m",
					   file->full_path);
			return -1;
		}
		return 0;
	}
	return 1;
}

static int fs_posix_stat(struct fs_file *_file, struct stat *st_r)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);

	/* in case output != NULL it means that we're still writing to the file
	   and fs_stat() shouldn't stat the unfinished file. this is done by
	   fs-sis after fs_copy(). */
	if (file->fd != -1 && _file->output == NULL) {
		if (fstat(file->fd, st_r) < 0) {
			fs_set_error_errno(_file->event, "fstat(%s) failed: %m",
					   file->full_path);
			return -1;
		}
	} else {
		if (stat(file->full_path, st_r) < 0) {
			fs_set_error_errno(_file->event, "stat(%s) failed: %m",
					   file->full_path);
			return -1;
		}
	}
	/* Used by obox to set MAIL_FETCH_REFCOUNT_ID for lazy_expunge: */
	fs_default_set_metadata(_file, FS_METADATA_OBJECTID,
		t_strdup_printf("%u:%u:%llu",
				major(st_r->st_dev), minor(st_r->st_dev),
				(unsigned long long)st_r->st_ino));
	return 0;
}

static int fs_posix_copy(struct fs_file *_src, struct fs_file *_dest)
{
	struct posix_fs_file *src =
		container_of(_src, struct posix_fs_file, file);
	struct posix_fs_file *dest =
		container_of(_dest, struct posix_fs_file, file);
	unsigned int try_count = 0;
	int ret;

	fs_posix_write_rename_if_needed(dest);
	ret = link(src->full_path, dest->full_path);
	if (ret < 0 && errno == EEXIST &&
	    dest->open_mode == FS_OPEN_MODE_REPLACE) {
		/* destination file already exists - replace it */
		i_unlink_if_exists(dest->full_path);
		ret = link(src->full_path, dest->full_path);
	}
	while (ret < 0 && errno == ENOENT &&
	       try_count <= MAX_MKDIR_RETRY_COUNT) {
		if (fs_posix_mkdir_parents(dest, dest->full_path) < 0)
			return -1;
		ret = link(src->full_path, dest->full_path);
		try_count++;
	}
	if (ret < 0) {
		fs_set_error_errno(_src->event, "link(%s, %s) failed: %m",
				   src->full_path, dest->full_path);
		return -1;
	}
	return 0;
}

static int fs_posix_rename(struct fs_file *_src, struct fs_file *_dest)
{
	struct posix_fs_file *src =
		container_of(_src, struct posix_fs_file, file);
	struct posix_fs_file *dest =
		container_of(_dest, struct posix_fs_file, file);
	unsigned int try_count = 0;
	int ret;

	ret = rename(src->full_path, dest->full_path);
	while (ret < 0 && errno == ENOENT &&
	       try_count <= MAX_MKDIR_RETRY_COUNT) {
		if (fs_posix_mkdir_parents(dest, dest->full_path) < 0)
			return -1;
		ret = rename(src->full_path, dest->full_path);
		try_count++;
	}
	if (ret < 0) {
		fs_set_error_errno(_src->event, "rename(%s, %s) failed: %m",
				   src->full_path, dest->full_path);
		return -1;
	}
	return 0;
}

static int fs_posix_delete(struct fs_file *_file)
{
	struct posix_fs_file *file =
		container_of(_file, struct posix_fs_file, file);

	if (unlink(file->full_path) < 0) {
		if (!UNLINK_EISDIR(errno)) {
			fs_set_error_errno(_file->event, "unlink(%s) failed: %m",
					   file->full_path);
			return -1;
		}
		/* attempting to delete a directory. convert it to rmdir()
		   automatically. */
		if (rmdir(file->full_path) < 0) {
			fs_set_error_errno(_file->event, "rmdir(%s) failed: %m",
					   file->full_path);
			return -1;
		}
	}
	(void)fs_posix_rmdir_parents(file, file->full_path);
	return 0;
}

static struct fs_iter *fs_posix_iter_alloc(void)
{
	struct posix_fs_iter *iter = i_new(struct posix_fs_iter, 1);
	return &iter->iter;
}

static void
fs_posix_iter_init(struct fs_iter *_iter, const char *path,
		   enum fs_iter_flags flags ATTR_UNUSED)
{
	struct posix_fs_iter *iter =
		container_of(_iter, struct posix_fs_iter, iter);
	struct posix_fs *fs = container_of(_iter->fs, struct posix_fs, fs);

	iter->path = i_strconcat(fs->set->fs_posix_prefix, path, NULL);
	if (iter->path[0] == '\0') {
		i_free(iter->path);
		iter->path = i_strdup(".");
	}
	iter->dir = opendir(iter->path);
	if (iter->dir == NULL && errno != ENOENT) {
		iter->err = errno;
		fs_set_error_errno(_iter->event,
				   "opendir(%s) failed: %m", iter->path);
	}
}

static bool fs_posix_iter_want(struct posix_fs_iter *iter, const char *fname)
{
	bool ret;

	T_BEGIN {
		const char *path = t_strdup_printf("%s/%s", iter->path, fname);
		struct stat st;

		if (stat(path, &st) < 0 &&
		    lstat(path, &st) < 0)
			ret = FALSE;
		else if (!S_ISDIR(st.st_mode))
			ret = (iter->iter.flags & FS_ITER_FLAG_DIRS) == 0;
		else
			ret = (iter->iter.flags & FS_ITER_FLAG_DIRS) != 0;
	} T_END;
	return ret;
}

static const char *fs_posix_iter_next(struct fs_iter *_iter)
{
	struct posix_fs_iter *iter =
		container_of(_iter, struct posix_fs_iter, iter);
	struct posix_fs *fs = container_of(_iter->fs, struct posix_fs, fs);
	struct dirent *d;

	if (iter->dir == NULL)
		return NULL;

	errno = 0;
	for (; (d = readdir(iter->dir)) != NULL; errno = 0) {
		if (strcmp(d->d_name, ".") == 0 ||
		    strcmp(d->d_name, "..") == 0)
			continue;
		if (strncmp(d->d_name, fs->temp_file_prefix,
			    fs->temp_file_prefix_len) == 0)
			continue;
#ifdef HAVE_DIRENT_D_TYPE
		switch (d->d_type) {
		case DT_UNKNOWN:
			if (fs_posix_iter_want(iter, d->d_name))
				return d->d_name;
			break;
		case DT_DIR:
			if ((iter->iter.flags & FS_ITER_FLAG_DIRS) != 0)
				return d->d_name;
			break;
		default:
			if ((iter->iter.flags & FS_ITER_FLAG_DIRS) == 0)
				return d->d_name;
			break;
		}
#else
		if (fs_posix_iter_want(iter, d->d_name))
			return d->d_name;
#endif
	}
	if (errno != 0) {
		iter->err = errno;
		fs_set_error_errno(_iter->event,
				   "readdir(%s) failed: %m", iter->path);
	}
	return NULL;
}

static int fs_posix_iter_deinit(struct fs_iter *_iter)
{
	struct posix_fs_iter *iter =
		container_of(_iter, struct posix_fs_iter, iter);
	int ret = 0;

	if (iter->dir != NULL && closedir(iter->dir) < 0 && iter->err == 0) {
		iter->err = errno;
		fs_set_error_errno(_iter->event,
				   "closedir(%s) failed: %m", iter->path);
	}
	if (iter->err != 0) {
		errno = iter->err;
		ret = -1;
	}
	i_free(iter->path);
	return ret;
}

const struct fs fs_class_posix = {
	.name = "posix",
	.v = {
		.alloc = fs_posix_alloc,
		.init = fs_posix_init,
		.deinit = NULL,
		.free = fs_posix_free,
		.get_properties = fs_posix_get_properties,
		.file_alloc = fs_posix_file_alloc,
		.file_init = fs_posix_file_init,
		.file_deinit = fs_posix_file_deinit,
		.file_close = fs_posix_file_close,
		.file_equals = fs_posix_file_equals,
		.get_path = NULL,
		.set_async_callback = NULL,
		.wait_async = NULL,
		.set_metadata = fs_default_set_metadata,
		.get_metadata = NULL,
		.prefetch = fs_posix_prefetch,
		.read = fs_posix_read,
		.read_stream = fs_posix_read_stream,
		.write = fs_posix_write,
		.write_stream = fs_posix_write_stream,
		.write_stream_finish = fs_posix_write_stream_finish,
		.lock = fs_posix_lock,
		.unlock = fs_posix_unlock,
		.exists = fs_posix_exists,
		.stat = fs_posix_stat,
		.copy = fs_posix_copy,
		.rename = fs_posix_rename,
		.delete_file = fs_posix_delete,
		.iter_alloc = fs_posix_iter_alloc,
		.iter_init = fs_posix_iter_init,
		.iter_next = fs_posix_iter_next,
		.iter_deinit = fs_posix_iter_deinit,
		.switch_ioloop = NULL,
		.get_nlinks = NULL,
	}
};
