Ziele waren mir:
Keine Fehler schlucken; alles melden.
Temporäre Datei nur so kurz wie möglich im Dateisystem lassen.
Auch verwendbar, wenn es bisher keine Datei zum Ersetzen gibt. Sprich eine neue Datei wird angelegt.
Bei irgendwelchen Vorfällen (Stromausfall, Betriebsystemabsturz, etc) die original Datei unverändert lassen (falls vorhanden).
Ist ein bisschen abstrakter als ich zu Anfang brauchte, aber tut seinen Dienst (zumindest bei mir ;))
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
enum SaveAtomicError
{
SAE_SUCCESS,
SAE_ERROR_RESOLVE_DIR,
SAE_ERROR_OPEN_DIR,
SAE_ERROR_OPEN_TMPFILE,
SAE_ERROR_FSYNC,
SAE_ERROR_NAME_TOO_LONG,
SAE_ERROR_SIGPROCMASK,
SAE_ERROR_LINK,
SAE_ERROR_RENAME,
SAE_ERROR_CLOSE,
};
struct SaveAtomic
{
char const* filename;
int dir;
int src;
int dest;
};
// Returns the absolute path for the directory.
static bool get_file_directory(char directory[const static PATH_MAX], char const* const filename)
{
directory[0] = 0;
if (filename[0] != '/') // a relative path
{
if (!getcwd(directory, PATH_MAX))
return false;
strcat(directory, "/");
}
strcat(directory, filename);
char* const last_slash = strrchr(directory, '/');
if (last_slash)
*last_slash = 0;
return true;
}
// It is not an error if there is no file with the name 'filename' yet. But if there is, it will be opened for reading.
// ASSUME: filename must be valid until either save_atomic_finalize or save_atomic_cancel has been called.
static enum SaveAtomicError save_atomic_begin(struct SaveAtomic* const sa, char const* const filename)
{
assert(sa);
assert(filename);
mode_t const mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
// Initialize the struct.
sa->filename = filename;
sa->dir = sa->src = sa->dest = -1;
// Get the (absolute) directory path where all the work will take place.
char dir[PATH_MAX];
if (!get_file_directory(dir, filename))
return SAE_ERROR_RESOLVE_DIR;
// Open a file descriptor to our working directory. This is needed in case something happens (renamed, moved, ...)
// to the directory while we are working on the files inside.
sa->dir = open(dir, O_DIRECTORY, 0);
if (sa->dir == -1)
return SAE_ERROR_OPEN_DIR;
// If there is already a file with the supplied filename, open it for further use by the user.
sa->src = openat(sa->dir, filename, O_RDONLY, 0);
// Open the temporary file.
sa->dest = openat(sa->dir, ".", O_WRONLY | O_TMPFILE, mode);
if (sa->dest == -1)
{
if (sa->src != -1)
close(sa->src);
close(sa->dir);
return SAE_ERROR_OPEN_TMPFILE;
}
return SAE_SUCCESS;
}
static void save_atomic_cancel(struct SaveAtomic const* const sa)
{
assert(sa);
if (sa->dest != -1)
close(sa->dest);
if (sa->src != -1)
close(sa->src);
if (sa->dir != -1)
close(sa->dir);
}
static enum SaveAtomicError save_atomic_finalize(struct SaveAtomic const* const sa)
{
assert(sa);
enum SaveAtomicError ret = SAE_SUCCESS;
// Make sure everything is written to disk.
if (fsync(sa->dest) == -1)
{
ret = SAE_ERROR_FSYNC;
goto out;
}
// Create the temporary filename.
char path[PATH_MAX];
int const name_len = snprintf(path, sizeof path, "%s~", sa->filename);
assert(name_len >= 0);
if ((size_t)name_len > (sizeof path - 1))
{
ret = SAE_ERROR_NAME_TOO_LONG;
goto out;
}
// Create the path from where to get the temporary file.
char proc_path[PATH_MAX]; // TODO: Reduce the array size. Maybe to 14+(MAX_NUM_INT_DIGITS)+1.
int const proc_path_len = snprintf(proc_path, sizeof proc_path, "/proc/self/fd/%d", sa->dest);
assert(proc_path_len >= 0);
assert((size_t)proc_path_len < (sizeof proc_path - 1));
// Block all signals so that the following linkat and renameat won't get interrupted by a signal. Unfortunately
// SIGKILL will still terminate us and leave the temporary file behind.
sigset_t set;
sigfillset(&set);
if (sigprocmask(SIG_BLOCK, &set, 0) == -1)
{
ret = SAE_ERROR_SIGPROCMASK;
goto out;
}
// Link the temporary file into the filesystem with the name we created above.
if (linkat(AT_FDCWD, proc_path, sa->dir, path, AT_SYMLINK_FOLLOW) == -1)
{
ret = SAE_ERROR_LINK;
sigprocmask(SIG_UNBLOCK, &set, 0); // Restore the signal mask before exiting.
goto out;
}
// Overwrite the old file with our temporary file.
if (renameat(sa->dir, path, sa->dir, sa->filename) == -1)
{
ret = SAE_ERROR_RENAME;
sigprocmask(SIG_UNBLOCK, &set, 0); // Restore the signal mask before exiting.
goto out;
}
// Restore the signal mask.
if (sigprocmask(SIG_UNBLOCK, &set, 0) == -1)
ret = SAE_ERROR_SIGPROCMASK;
out:
if (sa->dest != -1)
{
// Check for error or SILENT data loss is possible.
if (close(sa->dest) == -1)
ret = SAE_ERROR_CLOSE;
}
if (sa->src != -1)
close(sa->src);
assert(sa->dir != -1);
close(sa->dir);
return ret;
}
int main(int argc, char* argv[])
{
if (argc < 2)
return EXIT_FAILURE;
struct SaveAtomic sa;
if (save_atomic_begin(&sa, argv[1]))
{
perror("save_atomic_begin");
return EXIT_FAILURE;
}
char const str[] = "Hallo Welt!\n";
if (write(sa.dest, str, sizeof str))
{
perror("write");
save_atomic_cancel(&sa);
return EXIT_FAILURE;
}
if (save_atomic_finalize(&sa))
{
perror("save_atomic_finalize");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
TL;DR:
Verzeichnis der Datei ermitteln
File descriptor zu diesem Verzeichnis offen halten
Datei öffnen, relativ zum Verzeichnis
Temporäre Datei öffnen, relativ zum Verzeichnis
Irgendwas in die temporäre Datei schreiben
Daten auf die Platte schreiben
Alle Signale blocken
Temporäre Datei ins Dateisystem einhängen, relativ zum Verzeichnis
Originale Datei durch temporäre Datei ersetzen, relativ zum Verzeichnis
Signale wieder herstellen
Alle file descriptoren wieder schließen