Commit a84f241d authored by Philip Withnall's avatar Philip Withnall

Merge branch '1302-file-set-contents-fsync' into 'master'

gfileutils: Add g_file_set_contents_full() and GFileSetContentsFlags

Closes #1203 and #1302

See merge request !369
parents 5b49df3b 5bf38490
Pipeline #199183 failed with stages
in 12 minutes and 18 seconds
......@@ -1613,7 +1613,9 @@ G_FILE_ERROR
GFileTest
g_file_error_from_errno
g_file_get_contents
GFileSetContentsFlags
g_file_set_contents
g_file_set_contents_full
g_file_test
g_mkstemp
g_mkstemp_full
......
......@@ -830,10 +830,12 @@ keyring_generate_entry (const gchar *cookie_context,
/* and now actually write the cookie file if there are changes (this is atomic) */
if (changed_file)
{
if (!g_file_set_contents (path,
new_contents->str,
-1,
error))
if (!g_file_set_contents_full (path,
new_contents->str,
-1,
G_FILE_SET_CONTENTS_CONSISTENT,
0600,
error))
{
*out_id = 0;
*out_cookie = 0;
......
......@@ -3630,7 +3630,9 @@ update_mimeapps_list (const char *desktop_id,
data = g_key_file_to_data (key_file, &data_size, error);
g_key_file_free (key_file);
res = g_file_set_contents (filename, data, data_size, error);
res = g_file_set_contents_full (filename, data, data_size,
G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING,
0600, error);
desktop_file_dirs_invalidate_user_config ();
......@@ -3774,7 +3776,9 @@ g_desktop_app_info_set_as_default_for_extension (GAppInfo *appinfo,
" </mime-type>\n"
"</mime-info>\n", mimetype, extension, extension);
g_file_set_contents (filename, contents, -1, NULL);
g_file_set_contents_full (filename, contents, -1,
G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING,
0600, NULL);
g_free (contents);
run_update_command ("update-mime-database", "mime");
......@@ -3923,7 +3927,9 @@ g_desktop_app_info_ensure_saved (GDesktopAppInfo *info,
/* FIXME - actually handle error */
(void) g_close (fd, NULL);
res = g_file_set_contents (filename, data, data_size, error);
res = g_file_set_contents_full (filename, data, data_size,
G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING,
0600, error);
g_free (data);
if (!res)
{
......
......@@ -2211,7 +2211,9 @@ g_local_file_trash (GFile *file,
original_name_escaped, delete_time);
g_free (delete_time);
g_file_set_contents (infofile, data, -1, NULL);
g_file_set_contents_full (infofile, data, -1,
G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING,
0600, NULL);
/* TODO: Maybe we should verify that you can delete the file from the trash
* before moving it? OTOH, that is hard, as it needs a recursive scan
......
......@@ -313,9 +313,8 @@ _g_local_file_output_stream_really_close (GLocalFileOutputStream *file,
{
GLocalFileStat final_stat;
#ifdef HAVE_FSYNC
if (file->priv->sync_on_close &&
fsync (file->priv->fd) != 0)
g_fsync (file->priv->fd) != 0)
{
int errsv = errno;
......@@ -325,8 +324,7 @@ _g_local_file_output_stream_really_close (GLocalFileOutputStream *file,
g_strerror (errsv));
goto err_out;
}
#endif
#ifdef G_OS_WIN32
/* Must close before renaming on Windows, so just do the close first
......
......@@ -573,7 +573,9 @@ write_config_file (GTestDBus *self)
"</busconfig>\n");
close (fd);
g_file_set_contents (path, contents->str, contents->len, &error);
g_file_set_contents_full (path, contents->str, contents->len,
G_FILE_SET_CONTENTS_NONE,
0600, &error);
g_assert_no_error (error);
g_string_free (contents, TRUE);
......
......@@ -46,6 +46,10 @@
#define O_BINARY 0
#endif
#ifndef O_CLOEXEC
#define O_CLOEXEC 0
#endif
#include "gfileutils.h"
#include "gstdio.h"
......@@ -1023,8 +1027,9 @@ g_file_get_contents (const gchar *filename,
static gboolean
rename_file (const char *old_name,
const char *new_name,
GError **err)
const char *new_name,
gboolean do_fsync,
GError **err)
{
errno = 0;
if (g_rename (old_name, new_name) == -1)
......@@ -1046,36 +1051,99 @@ rename_file (const char *old_name,
return FALSE;
}
/* In order to guarantee that the *new* contents of the file are seen in
* future, fsync() the directory containing the file. Otherwise if the file
* system was unmounted cleanly now, it would be undefined whether the old
* or new contents of the file were visible after recovery.
*
* This assumes the @old_name and @new_name are in the same directory. */
#ifdef HAVE_FSYNC
if (do_fsync)
{
gchar *dir = g_path_get_dirname (new_name);
int dir_fd = g_open (dir, O_RDONLY, 0);
if (dir_fd >= 0)
{
g_fsync (dir_fd);
g_close (dir_fd, NULL);
}
g_free (dir);
}
#endif /* HAVE_FSYNC */
return TRUE;
}
static gchar *
write_to_temp_file (const gchar *contents,
gssize length,
const gchar *dest_file,
GError **err)
static gboolean
fd_should_be_fsynced (int fd,
const gchar *test_file,
GFileSetContentsFlags flags)
{
gchar *tmp_name;
gchar *retval;
gint fd;
#ifdef HAVE_FSYNC
struct stat statbuf;
retval = NULL;
#ifdef BTRFS_SUPER_MAGIC
{
struct statfs buf;
tmp_name = g_strdup_printf ("%s.XXXXXX", dest_file);
/* On Linux, on btrfs, skip the fsync since rename-over-existing is
* guaranteed to be atomic and this is the only case in which we
* would fsync() anyway.
*
* See https://btrfs.wiki.kernel.org/index.php/FAQ#What_are_the_crash_guarantees_of_overwrite-by-rename.3F
*/
errno = 0;
fd = g_mkstemp_full (tmp_name, O_RDWR | O_BINARY, 0666);
if ((flags & G_FILE_SET_CONTENTS_CONSISTENT) &&
fstatfs (fd, &buf) == 0 && buf.f_type == BTRFS_SUPER_MAGIC)
return FALSE;
}
#endif /* BTRFS_SUPER_MAGIC */
if (fd == -1)
/* If the final destination exists and is > 0 bytes, we want to sync the
* newly written file to ensure the data is on disk when we rename over
* the destination. Otherwise if we get a system crash we can lose both
* the new and the old file on some filesystems. (I.E. those that don't
* guarantee the data is written to the disk before the metadata.)
*
* There is no difference (in file system terms) if the old file doesn’t
* already exist, apart from the fact that if the system crashes and the new
* data hasn’t been fsync()ed, there is only one bit of old data to lose (that
* the file didn’t exist in the first place). In some situations, such as
* trashing files, the old file never exists, so it seems reasonable to avoid
* the fsync(). This is not a widely applicable optimisation though.
*/
if ((flags & (G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_DURABLE)) &&
(flags & G_FILE_SET_CONTENTS_ONLY_EXISTING))
{
int saved_errno = errno;
set_file_error (err,
tmp_name, _("Failed to create file “%s”: %s"),
saved_errno);
goto out;
errno = 0;
if (g_lstat (test_file, &statbuf) == 0)
return (statbuf.st_size > 0);
else if (errno == ENOENT)
return FALSE;
else
return TRUE; /* lstat() failed; be cautious */
}
else
{
return (flags & (G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_DURABLE));
}
#else /* if !HAVE_FSYNC */
return FALSE;
#endif /* !HAVE_FSYNC */
}
/* closes @fd once it’s finished (on success or error) */
static gboolean
write_to_file (const gchar *contents,
gsize length,
int fd,
const gchar *dest_file,
gboolean do_fsync,
GError **err)
{
#ifdef HAVE_FALLOCATE
if (length > 0)
{
......@@ -1098,12 +1166,11 @@ write_to_temp_file (const gchar *contents,
continue;
set_file_error (err,
tmp_name, _("Failed to write file “%s”: write() failed: %s"),
dest_file, _("Failed to write file “%s”: write() failed: %s"),
saved_errno);
close (fd);
g_unlink (tmp_name);
goto out;
return FALSE;
}
g_assert (s <= length);
......@@ -1112,63 +1179,34 @@ write_to_temp_file (const gchar *contents,
length -= s;
}
#ifdef BTRFS_SUPER_MAGIC
{
struct statfs buf;
/* On Linux, on btrfs, skip the fsync since rename-over-existing is
* guaranteed to be atomic and this is the only case in which we
* would fsync() anyway.
*/
if (fstatfs (fd, &buf) == 0 && buf.f_type == BTRFS_SUPER_MAGIC)
goto no_fsync;
}
#endif
#ifdef HAVE_FSYNC
{
struct stat statbuf;
errno = 0;
/* If the final destination exists and is > 0 bytes, we want to sync the
* newly written file to ensure the data is on disk when we rename over
* the destination. Otherwise if we get a system crash we can lose both
* the new and the old file on some filesystems. (I.E. those that don't
* guarantee the data is written to the disk before the metadata.)
*/
if (g_lstat (dest_file, &statbuf) == 0 && statbuf.st_size > 0 && fsync (fd) != 0)
{
int saved_errno = errno;
set_file_error (err,
tmp_name, _("Failed to write file “%s”: fsync() failed: %s"),
saved_errno);
close (fd);
g_unlink (tmp_name);
goto out;
}
}
#endif
#ifdef BTRFS_SUPER_MAGIC
no_fsync:
#endif
errno = 0;
if (!g_close (fd, err))
if (do_fsync && g_fsync (fd) != 0)
{
g_unlink (tmp_name);
int saved_errno = errno;
set_file_error (err,
dest_file, _("Failed to write file “%s”: fsync() failed: %s"),
saved_errno);
close (fd);
goto out;
return FALSE;
}
#endif
retval = g_strdup (tmp_name);
errno = 0;
if (!g_close (fd, err))
return FALSE;
out:
g_free (tmp_name);
return TRUE;
}
return retval;
static inline int
steal_fd (int *fd_ptr)
{
int fd = *fd_ptr;
*fd_ptr = -1;
return fd;
}
/**
......@@ -1179,11 +1217,54 @@ write_to_temp_file (const gchar *contents,
* @length: length of @contents, or -1 if @contents is a nul-terminated string
* @error: return location for a #GError, or %NULL
*
* Writes all of @contents to a file named @filename. This is a convenience
* wrapper around calling g_file_set_contents() with `flags` set to
* `G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING` and
* `mode` set to `0666`.
*
* Returns: %TRUE on success, %FALSE if an error occurred
*
* Since: 2.8
*/
gboolean
g_file_set_contents (const gchar *filename,
const gchar *contents,
gssize length,
GError **error)
{
return g_file_set_contents_full (filename, contents, length,
G_FILE_SET_CONTENTS_CONSISTENT |
G_FILE_SET_CONTENTS_ONLY_EXISTING,
0666, error);
}
/**
* g_file_set_contents_full:
* @filename: (type filename): name of a file to write @contents to, in the GLib file name
* encoding
* @contents: (array length=length) (element-type guint8): string to write to the file
* @length: length of @contents, or -1 if @contents is a nul-terminated string
* @flags: flags controlling the safety vs speed of the operation
* @mode: file mode, as passed to `open()`; typically this will be `0666`
* @error: return location for a #GError, or %NULL
*
* Writes all of @contents to a file named @filename, with good error checking.
* If a file called @filename already exists it will be overwritten.
*
* This write is atomic in the sense that it is first written to a temporary
* file which is then renamed to the final name. Notes:
* @flags control the properties of the write operation: whether it’s atomic,
* and what the tradeoff is between returning quickly or being resilient to
* system crashes.
*
* As this function performs file I/O, it is recommended to not call it anywhere
* where blocking would cause problems, such as in the main loop of a graphical
* application. In particular, if @flags has any value other than
* %G_FILE_SET_CONTENTS_NONE then this function may call `fsync()`.
*
* If %G_FILE_SET_CONTENTS_CONSISTENT is set in @flags, the operation is atomic
* in the sense that it is first written to a temporary file which is then
* renamed to the final name.
*
* Notes:
*
* - On UNIX, if @filename already exists hard links to @filename will break.
* Also since the file is recreated, existing permissions, access control
......@@ -1191,15 +1272,17 @@ write_to_temp_file (const gchar *contents,
* the link itself will be replaced, not the linked file.
*
* - On UNIX, if @filename already exists and is non-empty, and if the system
* supports it (via a journalling filesystem or equivalent), the fsync()
* call (or equivalent) will be used to ensure atomic replacement: @filename
* supports it (via a journalling filesystem or equivalent), and if
* %G_FILE_SET_CONTENTS_CONSISTENT is set in @flags, the `fsync()` call (or
* equivalent) will be used to ensure atomic replacement: @filename
* will contain either its old contents or @contents, even in the face of
* system power loss, the disk being unsafely removed, etc.
*
* - On UNIX, if @filename does not already exist or is empty, there is a
* possibility that system power loss etc. after calling this function will
* leave @filename empty or full of NUL bytes, depending on the underlying
* filesystem.
* filesystem, unless %G_FILE_SET_CONTENTS_DURABLE and
* %G_FILE_SET_CONTENTS_CONSISTENT are set in @flags.
*
* - On Windows renaming a file will not remove an existing file with the
* new name, so on Windows there is a race condition between the existing
......@@ -1216,88 +1299,171 @@ write_to_temp_file (const gchar *contents,
* Note that the name for the temporary file is constructed by appending up
* to 7 characters to @filename.
*
* If the file didn’t exist before and is created, it will be given the
* permissions from @mode. Otherwise, the permissions of the existing file may
* be changed to @mode depending on @flags, or they may remain unchanged.
*
* Returns: %TRUE on success, %FALSE if an error occurred
*
* Since: 2.8
* Since: 2.66
*/
gboolean
g_file_set_contents (const gchar *filename,
const gchar *contents,
gssize length,
GError **error)
g_file_set_contents_full (const gchar *filename,
const gchar *contents,
gssize length,
GFileSetContentsFlags flags,
int mode,
GError **error)
{
gchar *tmp_filename;
gboolean retval;
GError *rename_error = NULL;
g_return_val_if_fail (filename != NULL, FALSE);
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
g_return_val_if_fail (contents != NULL || length == 0, FALSE);
g_return_val_if_fail (length >= -1, FALSE);
if (length == -1)
/* @flags are handled as follows:
* - %G_FILE_SET_CONTENTS_NONE: write directly to @filename, no fsync()s
* - %G_FILE_SET_CONTENTS_CONSISTENT: write to temp file, fsync() it, rename()
* - %G_FILE_SET_CONTENTS_CONSISTENT | ONLY_EXISTING: as above, but skip the
* fsync() if @filename doesn’t exist or is empty
* - %G_FILE_SET_CONTENTS_DURABLE: write directly to @filename, fsync() it
* - %G_FILE_SET_CONTENTS_DURABLE | ONLY_EXISTING: as above, but skip the
* fsync() if @filename doesn’t exist or is empty
* - %G_FILE_SET_CONTENTS_CONSISTENT | DURABLE: write to temp file, fsync()
* it, rename(), fsync() containing directory
* - %G_FILE_SET_CONTENTS_CONSISTENT | DURABLE | ONLY_EXISTING: as above, but
* skip both fsync()s if @filename doesn’t exist or is empty
*/
if (length < 0)
length = strlen (contents);
tmp_filename = write_to_temp_file (contents, length, filename, error);
if (!tmp_filename)
if (flags & G_FILE_SET_CONTENTS_CONSISTENT)
{
retval = FALSE;
goto out;
}
gchar *tmp_filename = NULL;
GError *rename_error = NULL;
gboolean retval;
int fd;
gboolean do_fsync;
if (!rename_file (tmp_filename, filename, &rename_error))
{
tmp_filename = g_strdup_printf ("%s.XXXXXX", filename);
errno = 0;
fd = g_mkstemp_full (tmp_filename, O_RDWR | O_BINARY, mode);
if (fd == -1)
{
int saved_errno = errno;
set_file_error (error,
tmp_filename, _("Failed to create file “%s”: %s"),
saved_errno);
retval = FALSE;
goto consistent_out;
}
do_fsync = fd_should_be_fsynced (fd, filename, flags);
if (!write_to_file (contents, length, steal_fd (&fd), tmp_filename, do_fsync, error))
{
g_unlink (tmp_filename);
retval = FALSE;
goto consistent_out;
}
if (!rename_file (tmp_filename, filename, do_fsync, &rename_error))
{
#ifndef G_OS_WIN32
g_unlink (tmp_filename);
g_propagate_error (error, rename_error);
retval = FALSE;
goto out;
g_unlink (tmp_filename);
g_propagate_error (error, rename_error);
retval = FALSE;
goto consistent_out;
#else /* G_OS_WIN32 */
/* Renaming failed, but on Windows this may just mean
* the file already exists. So if the target file
* exists, try deleting it and do the rename again.
*/
if (!g_file_test (filename, G_FILE_TEST_EXISTS))
{
g_unlink (tmp_filename);
g_propagate_error (error, rename_error);
retval = FALSE;
goto out;
}
g_error_free (rename_error);
if (g_unlink (filename) == -1)
{
/* Renaming failed, but on Windows this may just mean
* the file already exists. So if the target file
* exists, try deleting it and do the rename again.
*/
if (!g_file_test (filename, G_FILE_TEST_EXISTS))
{
g_unlink (tmp_filename);
g_propagate_error (error, rename_error);
retval = FALSE;
goto consistent_out;
}
g_error_free (rename_error);
if (g_unlink (filename) == -1)
{
int saved_errno = errno;
set_file_error (error,
filename,
_("Existing file “%s” could not be removed: g_unlink() failed: %s"),
saved_errno);
g_unlink (tmp_filename);
retval = FALSE;
goto consistent_out;
}
if (!rename_file (tmp_filename, filename, flags, error))
{
g_unlink (tmp_filename);
retval = FALSE;
goto consistent_out;
}
#endif /* G_OS_WIN32 */
}
retval = TRUE;
consistent_out:
g_free (tmp_filename);
return retval;
}
else
{
int direct_fd;
int open_flags;
gboolean do_fsync;
open_flags = O_RDWR | O_BINARY | O_CREAT | O_CLOEXEC;
#ifdef O_NOFOLLOW
/* Windows doesn’t have symlinks, so O_NOFOLLOW is unnecessary there. */
open_flags |= O_NOFOLLOW;
#endif
errno = 0;
direct_fd = g_open (filename, open_flags, mode);
if (direct_fd < 0)
{
int saved_errno = errno;
#ifdef O_NOFOLLOW
/* ELOOP indicates that @filename is a symlink, since we used
* O_NOFOLLOW (alternately it could indicate that @filename contains
* looping or too many symlinks). In either case, try again on the
* %G_FILE_SET_CONTENTS_CONSISTENT code path. */
if (saved_errno == ELOOP)
return g_file_set_contents_full (filename, contents, length,
flags | G_FILE_SET_CONTENTS_CONSISTENT,
mode, error);
#endif
set_file_error (error,
filename,
_("Existing file “%s” could not be removed: g_unlink() failed: %s"),
filename, _("Failed to open file “%s”: %s"),
saved_errno);
g_unlink (tmp_filename);
retval = FALSE;
goto out;
}
if (!rename_file (tmp_filename, filename, error))
{
g_unlink (tmp_filename);
retval = FALSE;
goto out;
}
return FALSE;
}
#endif
do_fsync = fd_should_be_fsynced (direct_fd, filename, flags);
if (!write_to_file (contents, length, steal_fd (&direct_fd), filename,
do_fsync, error))
return FALSE;
}
retval = TRUE;
out:
g_free (tmp_filename);
return retval;
return TRUE;
}
/*
...