From 4dae2c875bf70d87ace53d7b03c4a9289b2bb241 Mon Sep 17 00:00:00 2001 From: Maxime Coste Date: Tue, 12 Feb 2019 20:54:37 +1100 Subject: [PATCH] Introduce a writemethod option to either overwrite or replace files This permit to choose if files should be written by overwriting their content (the default), or by writing to a separate temporary file and rename it to the current file. As discussed in #2036 --- doc/pages/options.asciidoc | 7 +++++ src/commands.cc | 6 ++-- src/file.cc | 62 +++++++++++++++++++++++++------------- src/file.hh | 16 +++++++++- src/main.cc | 7 +++-- 5 files changed, 72 insertions(+), 26 deletions(-) diff --git a/doc/pages/options.asciidoc b/doc/pages/options.asciidoc index ea3df5061..b67d71f35 100644 --- a/doc/pages/options.asciidoc +++ b/doc/pages/options.asciidoc @@ -240,6 +240,13 @@ are exclusively available to built-in options. _default_ ask + auto reload the buffers when an external modification is detected +*writemethod* `enum(overwrite|replace)`:: + _default_ overwrite + + method used to write buffers to file, `overwrite` will open the + existing file and write on top of the previous data, `replace` + will open a temporary file next to the target file, write it and + then rename it to the target file. + *debug* `flags(hooks|shell|profile|keys|commands)`:: dump various debug information in the '\*debug*' buffer diff --git a/src/commands.cc b/src/commands.cc index 6cc48dbf5..3bc145bcb 100644 --- a/src/commands.cc +++ b/src/commands.cc @@ -342,9 +342,10 @@ void do_write_buffer(Context& context, Optional filename, WriteFlags fla throw runtime_error("cannot overwrite the buffer when in readonly mode"); auto effective_filename = not filename ? buffer.name() : parse_filename(*filename); + auto mode = context.options()["writemethod"].get(); context.hooks().run_hook(Hook::BufWritePre, effective_filename, context); - write_buffer_to_file(buffer, effective_filename, flags); + write_buffer_to_file(buffer, effective_filename, mode, flags); context.hooks().run_hook(Hook::BufWritePost, effective_filename, context); } @@ -395,8 +396,9 @@ void write_all_buffers(Context& context, bool sync = false) buffer->is_modified()) and !(buffer->flags() & Buffer::Flags::ReadOnly)) { + auto mode = context.options()["writemethod"].get(); buffer->run_hook_in_own_context(Hook::BufWritePre, buffer->name(), context.name()); - write_buffer_to_file(*buffer, buffer->name(), sync ? WriteFlags::Sync : WriteFlags::None); + write_buffer_to_file(*buffer, buffer->name(), mode, sync ? WriteFlags::Sync : WriteFlags::None); buffer->run_hook_in_own_context(Hook::BufWritePost, buffer->name(), context.name()); } } diff --git a/src/file.cc b/src/file.cc index d5e73bed4..eb26ddc4c 100644 --- a/src/file.cc +++ b/src/file.cc @@ -280,25 +280,51 @@ void write_buffer_to_fd(Buffer& buffer, int fd) } } -void write_buffer_to_file(Buffer& buffer, StringView filename, WriteFlags flags) +int open_temp_file(StringView filename, char (&buffer)[PATH_MAX]) { - struct stat st; - auto zfilename = filename.zstr(); + String path = real_path(filename); + auto [dir,file] = split_path(path); - if (flags & WriteFlags::Force and ::stat(zfilename, &st) == 0) - { - if (::chmod(zfilename, st.st_mode | S_IWUSR) < 0) - throw runtime_error("unable to change file permissions"); - } + if (dir.empty()) + format_to(buffer, ".{}.kak.XXXXXX", file); else - flags |= ~WriteFlags::Force; + format_to(buffer, "{}/.{}.kak.XXXXXX", dir, file); + + return mkstemp(buffer); +} + +int open_temp_file(StringView filename) +{ + char buffer[PATH_MAX]; + return open_temp_file(filename, buffer); +} + +void write_buffer_to_file(Buffer& buffer, StringView filename, + WriteMethod method, WriteFlags flags) +{ + auto zfilename = filename.zstr(); + struct stat st; + + bool replace = method == WriteMethod::Replace; + bool force = flags & WriteFlags::Force; + + if ((replace or force) and ::stat(zfilename, &st) != 0) + { + force = false; + replace = false; + } + + if (force and ::chmod(zfilename, st.st_mode | S_IWUSR) < 0) + throw runtime_error("unable to change file permissions"); auto restore_mode = on_scope_end([&]{ - if (flags & WriteFlags::Force and ::chmod(zfilename, st.st_mode) < 0) + if ((force or replace) and ::chmod(zfilename, st.st_mode) < 0) throw runtime_error("unable to restore file permissions"); }); - const int fd = open(zfilename, O_CREAT | O_WRONLY | O_TRUNC, 0644); + char temp_filename[PATH_MAX]; + const int fd = replace ? open_temp_file(filename, temp_filename) + : open(zfilename, O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd == -1) throw file_access_error(filename, strerror(errno)); @@ -309,6 +335,9 @@ void write_buffer_to_file(Buffer& buffer, StringView filename, WriteFlags flags) ::fsync(fd); } + if (replace) + rename(temp_filename, zfilename); + if ((buffer.flags() & Buffer::Flags::File) and real_path(filename) == real_path(buffer.name())) buffer.notify_saved(); @@ -316,16 +345,7 @@ void write_buffer_to_file(Buffer& buffer, StringView filename, WriteFlags flags) void write_buffer_to_backup_file(Buffer& buffer) { - String path = real_path(buffer.name()); - auto [dir,file] = split_path(path); - - char pattern[PATH_MAX]; - if (dir.empty()) - format_to(pattern, ".{}.kak.XXXXXX", file); - else - format_to(pattern, "{}/.{}.kak.XXXXXX", dir, file); - - int fd = mkstemp(pattern); + const int fd = open_temp_file(buffer.name()); if (fd >= 0) { write_buffer_to_fd(buffer, fd); diff --git a/src/file.hh b/src/file.hh index db4ac6ffc..6871a1a39 100644 --- a/src/file.hh +++ b/src/file.hh @@ -2,6 +2,7 @@ #define file_hh_INCLUDED #include "array_view.hh" +#include "enum.hh" #include "meta.hh" #include "string.hh" #include "units.hh" @@ -51,6 +52,19 @@ struct MappedFile struct stat st {}; }; +enum class WriteMethod +{ + Overwrite, + Replace +}; +constexpr auto enum_desc(Meta::Type) +{ + return make_array, 2>({ + { WriteMethod::Overwrite, "overwrite" }, + { WriteMethod::Replace, "replace" }, + }); +} + enum class WriteFlags { None = 0, @@ -60,7 +74,7 @@ enum class WriteFlags constexpr bool with_bit_ops(Meta::Type) { return true; } void write_buffer_to_file(Buffer& buffer, StringView filename, - WriteFlags flags); + WriteMethod method, WriteFlags flags); void write_buffer_to_fd(Buffer& buffer, int fd); void write_buffer_to_backup_file(Buffer& buffer); diff --git a/src/main.cc b/src/main.cc index 95d5ca411..3cd9bc501 100644 --- a/src/main.cc +++ b/src/main.cc @@ -399,6 +399,9 @@ void register_options() reg.declare_option("autoreload", "autoreload buffer when a filesystem modification is detected", Autoreload::Ask); + reg.declare_option("writemethod", + "how to write buffer to files", + WriteMethod::Overwrite); reg.declare_option( "idle_timeout", "timeout, in milliseconds, before idle hooks are triggered", 50); reg.declare_option( @@ -848,10 +851,10 @@ int run_filter(StringView keystr, ConstArrayView files, bool quiet, Buffer* buffer = open_file_buffer(file, Buffer::Flags::NoHooks); if (not suffix_backup.empty()) write_buffer_to_file(*buffer, buffer->name() + suffix_backup, - WriteFlags::None); + WriteMethod::Overwrite, WriteFlags::None); apply_to_buffer(*buffer); write_buffer_to_file(*buffer, buffer->name(), - WriteFlags::None); + WriteMethod::Overwrite, WriteFlags::None); buffer_manager.delete_buffer(*buffer); } if (not isatty(0))