core/file: Writes go to a temporary file first
And then rename the temporary file when closed. This protects users data in case Ocarina gets killed. Implements issue #31: Make file writes seem atomic Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
This commit is contained in:
parent
c308ba7f8e
commit
ee4f0d4c89
47
core/file.c
47
core/file.c
|
@ -5,6 +5,7 @@
|
||||||
#include <core/string.h>
|
#include <core/string.h>
|
||||||
#include <core/version.h>
|
#include <core/version.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#define REPORT_ERROR(fname) \
|
#define REPORT_ERROR(fname) \
|
||||||
printf("%s (%s:%d): %s: %s\n", __func__, __FILE__, __LINE__, fname, strerror(errno))
|
printf("%s (%s:%d): %s: %s\n", __func__, __FILE__, __LINE__, fname, strerror(errno))
|
||||||
|
@ -35,6 +36,29 @@ static bool __file_mkdir()
|
||||||
return ret == 0;
|
return ret == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool __file_can_write(struct file *file)
|
||||||
|
{
|
||||||
|
gchar *path = file_path(file);
|
||||||
|
bool ret = true;
|
||||||
|
|
||||||
|
if (g_access(path, F_OK) == 0 && g_access(path, W_OK) < 0)
|
||||||
|
ret = false;
|
||||||
|
|
||||||
|
g_free(path);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void __file_rename_tmp(struct file *file)
|
||||||
|
{
|
||||||
|
gchar *path = file_path(file);
|
||||||
|
gchar *real = file_write_path(file);
|
||||||
|
|
||||||
|
g_rename(real, path);
|
||||||
|
|
||||||
|
g_free(real);
|
||||||
|
g_free(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void file_init(struct file *file, const gchar *name, unsigned int version)
|
void file_init(struct file *file, const gchar *name, unsigned int version)
|
||||||
{
|
{
|
||||||
|
@ -52,6 +76,20 @@ gchar *file_path(struct file *file)
|
||||||
return g_strdup("");
|
return g_strdup("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gchar *file_write_path(struct file *file)
|
||||||
|
{
|
||||||
|
gchar *fname, *res;
|
||||||
|
|
||||||
|
if (string_length(file->f_name) == 0)
|
||||||
|
return g_strdup("");
|
||||||
|
|
||||||
|
fname = g_strdup_printf(".%s.tmp", file->f_name);
|
||||||
|
res = __file_path(fname);
|
||||||
|
g_free(fname);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
const unsigned int file_version(struct file *file)
|
const unsigned int file_version(struct file *file)
|
||||||
{
|
{
|
||||||
if (file->f_file && (file->f_mode == OPEN_READ))
|
if (file->f_file && (file->f_mode == OPEN_READ))
|
||||||
|
@ -85,8 +123,10 @@ static bool __file_open_write(struct file *file)
|
||||||
{
|
{
|
||||||
if (!__file_mkdir())
|
if (!__file_mkdir())
|
||||||
return false;
|
return false;
|
||||||
|
if (!__file_can_write(file))
|
||||||
|
return false;
|
||||||
|
|
||||||
file->f_file = __file_open(file_path(file), "w");
|
file->f_file = __file_open(file_write_path(file), "w");
|
||||||
if (!file->f_file)
|
if (!file->f_file)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
@ -106,8 +146,11 @@ bool file_open(struct file *file, enum open_mode mode)
|
||||||
|
|
||||||
void file_close(struct file *file)
|
void file_close(struct file *file)
|
||||||
{
|
{
|
||||||
if (file->f_file)
|
if (file->f_file) {
|
||||||
fclose(file->f_file);
|
fclose(file->f_file);
|
||||||
|
if (file->f_mode == OPEN_WRITE)
|
||||||
|
__file_rename_tmp(file);
|
||||||
|
}
|
||||||
file->f_file = NULL;
|
file->f_file = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,12 @@ void file_init(struct file *, const gchar *, unsigned int);
|
||||||
*/
|
*/
|
||||||
gchar *file_path(struct file *);
|
gchar *file_path(struct file *);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns the path to the temporary file used for writes.
|
||||||
|
* This function allocates a new string that MUST be freed with g_free().
|
||||||
|
*/
|
||||||
|
gchar *file_write_path(struct file *);
|
||||||
|
|
||||||
/* Returns the version number of the file. */
|
/* Returns the version number of the file. */
|
||||||
const unsigned int file_version(struct file *);
|
const unsigned int file_version(struct file *);
|
||||||
|
|
||||||
|
@ -75,13 +81,17 @@ bool file_exists(struct file *);
|
||||||
*
|
*
|
||||||
* When opening a file for writing (OPEN_WRITE):
|
* When opening a file for writing (OPEN_WRITE):
|
||||||
* - Create missing directories as needed.
|
* - Create missing directories as needed.
|
||||||
|
* - Open a temporary file to protect data if Ocarina crashes.
|
||||||
* - Write file->_version to the start of the file.
|
* - Write file->_version to the start of the file.
|
||||||
*
|
*
|
||||||
* Returns true if the open was successful and false otherwise.
|
* Returns true if the open was successful and false otherwise.
|
||||||
*/
|
*/
|
||||||
bool file_open(struct file *, enum open_mode);
|
bool file_open(struct file *, enum open_mode);
|
||||||
|
|
||||||
/* Close an open file, setting file->f_file to NULL. */
|
/*
|
||||||
|
* Closes an open file, setting file->f_file to NULL. If the file was opened
|
||||||
|
* with OPEN_WRITE, then rename the temporary file to file_path().
|
||||||
|
*/
|
||||||
void file_close(struct file *);
|
void file_close(struct file *);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -2,25 +2,23 @@
|
||||||
* Copyright 2014 (c) Anna Schumaker.
|
* Copyright 2014 (c) Anna Schumaker.
|
||||||
*/
|
*/
|
||||||
#include <core/file.h>
|
#include <core/file.h>
|
||||||
|
#include <core/version.h>
|
||||||
#include <tests/test.h>
|
#include <tests/test.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
|
||||||
static void test_verify_constructor(struct file *file, gchar *fpath)
|
static void test_verify_constructor(struct file *file, gchar *fpath, gchar *ftmp)
|
||||||
{
|
{
|
||||||
gchar *path = file_path(file);
|
|
||||||
|
|
||||||
test_equal((void *)file->f_file, NULL);
|
test_equal((void *)file->f_file, NULL);
|
||||||
test_equal(file_version(file), 0);
|
test_equal(file_version(file), 0);
|
||||||
test_equal(file->f_mode, OPEN_READ);
|
test_equal(file->f_mode, OPEN_READ);
|
||||||
test_equal(path, fpath);
|
test_str_equal(file_path(file), fpath);
|
||||||
|
test_str_equal(file_write_path(file), ftmp);
|
||||||
g_free(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_invalid_file(struct file *file)
|
static void test_invalid_file(struct file *file)
|
||||||
{
|
{
|
||||||
test_verify_constructor(file, "");
|
test_verify_constructor(file, "", "");
|
||||||
|
|
||||||
test_equal(file_open(file, OPEN_READ), (bool)false);
|
test_equal(file_open(file, OPEN_READ), (bool)false);
|
||||||
test_equal((void *)file->f_file, NULL);
|
test_equal((void *)file->f_file, NULL);
|
||||||
|
@ -49,9 +47,13 @@ static void test_empty()
|
||||||
static void test_file()
|
static void test_file()
|
||||||
{
|
{
|
||||||
struct file file = FILE_INIT("file.txt", 0);
|
struct file file = FILE_INIT("file.txt", 0);
|
||||||
gchar *filepath = test_data_file("file.txt");
|
gchar *basepath, *filepath, *realpath;
|
||||||
|
|
||||||
test_verify_constructor(&file, filepath);
|
basepath = g_strjoin("/", g_get_user_data_dir(), OCARINA_NAME, NULL);
|
||||||
|
filepath = g_strjoin("/", basepath, "file.txt", NULL);
|
||||||
|
realpath = g_strjoin("/", basepath, ".file.txt.tmp", NULL);
|
||||||
|
|
||||||
|
test_verify_constructor(&file, filepath, realpath);
|
||||||
test_equal(file_exists(&file), (bool)false);
|
test_equal(file_exists(&file), (bool)false);
|
||||||
test_equal(test_data_file_exists(NULL), (bool)false);
|
test_equal(test_data_file_exists(NULL), (bool)false);
|
||||||
|
|
||||||
|
@ -61,6 +63,7 @@ static void test_file()
|
||||||
test_equal(file.f_mode, OPEN_WRITE);
|
test_equal(file.f_mode, OPEN_WRITE);
|
||||||
test_equal(file_open(&file, OPEN_WRITE), (bool)false);
|
test_equal(file_open(&file, OPEN_WRITE), (bool)false);
|
||||||
|
|
||||||
|
test_equal(file_exists(&file), (bool)false);
|
||||||
file_close(&file);
|
file_close(&file);
|
||||||
test_equal((void *)file.f_file, NULL);
|
test_equal((void *)file.f_file, NULL);
|
||||||
test_equal(file.f_mode, OPEN_WRITE);
|
test_equal(file.f_mode, OPEN_WRITE);
|
||||||
|
|
Loading…
Reference in New Issue