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
This commit is contained in:
Maxime Coste 2019-02-12 20:54:37 +11:00
parent e8f26cbae7
commit 4dae2c875b
5 changed files with 72 additions and 26 deletions

View File

@ -240,6 +240,13 @@ are exclusively available to built-in options.
_default_ ask + _default_ ask +
auto reload the buffers when an external modification is detected 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)`:: *debug* `flags(hooks|shell|profile|keys|commands)`::
dump various debug information in the '\*debug*' buffer dump various debug information in the '\*debug*' buffer

View File

@ -342,9 +342,10 @@ void do_write_buffer(Context& context, Optional<String> filename, WriteFlags fla
throw runtime_error("cannot overwrite the buffer when in readonly mode"); throw runtime_error("cannot overwrite the buffer when in readonly mode");
auto effective_filename = not filename ? buffer.name() : parse_filename(*filename); auto effective_filename = not filename ? buffer.name() : parse_filename(*filename);
auto mode = context.options()["writemethod"].get<WriteMethod>();
context.hooks().run_hook(Hook::BufWritePre, effective_filename, context); 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); 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()) buffer->is_modified())
and !(buffer->flags() & Buffer::Flags::ReadOnly)) and !(buffer->flags() & Buffer::Flags::ReadOnly))
{ {
auto mode = context.options()["writemethod"].get<WriteMethod>();
buffer->run_hook_in_own_context(Hook::BufWritePre, buffer->name(), context.name()); 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()); buffer->run_hook_in_own_context(Hook::BufWritePost, buffer->name(), context.name());
} }
} }

View File

@ -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; String path = real_path(filename);
auto zfilename = filename.zstr(); auto [dir,file] = split_path(path);
if (flags & WriteFlags::Force and ::stat(zfilename, &st) == 0) if (dir.empty())
{ format_to(buffer, ".{}.kak.XXXXXX", file);
if (::chmod(zfilename, st.st_mode | S_IWUSR) < 0)
throw runtime_error("unable to change file permissions");
}
else 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([&]{ 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"); 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) if (fd == -1)
throw file_access_error(filename, strerror(errno)); throw file_access_error(filename, strerror(errno));
@ -309,6 +335,9 @@ void write_buffer_to_file(Buffer& buffer, StringView filename, WriteFlags flags)
::fsync(fd); ::fsync(fd);
} }
if (replace)
rename(temp_filename, zfilename);
if ((buffer.flags() & Buffer::Flags::File) and if ((buffer.flags() & Buffer::Flags::File) and
real_path(filename) == real_path(buffer.name())) real_path(filename) == real_path(buffer.name()))
buffer.notify_saved(); 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) void write_buffer_to_backup_file(Buffer& buffer)
{ {
String path = real_path(buffer.name()); const int fd = open_temp_file(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);
if (fd >= 0) if (fd >= 0)
{ {
write_buffer_to_fd(buffer, fd); write_buffer_to_fd(buffer, fd);

View File

@ -2,6 +2,7 @@
#define file_hh_INCLUDED #define file_hh_INCLUDED
#include "array_view.hh" #include "array_view.hh"
#include "enum.hh"
#include "meta.hh" #include "meta.hh"
#include "string.hh" #include "string.hh"
#include "units.hh" #include "units.hh"
@ -51,6 +52,19 @@ struct MappedFile
struct stat st {}; struct stat st {};
}; };
enum class WriteMethod
{
Overwrite,
Replace
};
constexpr auto enum_desc(Meta::Type<WriteMethod>)
{
return make_array<EnumDesc<WriteMethod>, 2>({
{ WriteMethod::Overwrite, "overwrite" },
{ WriteMethod::Replace, "replace" },
});
}
enum class WriteFlags enum class WriteFlags
{ {
None = 0, None = 0,
@ -60,7 +74,7 @@ enum class WriteFlags
constexpr bool with_bit_ops(Meta::Type<WriteFlags>) { return true; } constexpr bool with_bit_ops(Meta::Type<WriteFlags>) { return true; }
void write_buffer_to_file(Buffer& buffer, StringView filename, 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_fd(Buffer& buffer, int fd);
void write_buffer_to_backup_file(Buffer& buffer); void write_buffer_to_backup_file(Buffer& buffer);

View File

@ -399,6 +399,9 @@ void register_options()
reg.declare_option("autoreload", reg.declare_option("autoreload",
"autoreload buffer when a filesystem modification is detected", "autoreload buffer when a filesystem modification is detected",
Autoreload::Ask); Autoreload::Ask);
reg.declare_option("writemethod",
"how to write buffer to files",
WriteMethod::Overwrite);
reg.declare_option<int, check_timeout>( reg.declare_option<int, check_timeout>(
"idle_timeout", "timeout, in milliseconds, before idle hooks are triggered", 50); "idle_timeout", "timeout, in milliseconds, before idle hooks are triggered", 50);
reg.declare_option<int, check_timeout>( reg.declare_option<int, check_timeout>(
@ -848,10 +851,10 @@ int run_filter(StringView keystr, ConstArrayView<StringView> files, bool quiet,
Buffer* buffer = open_file_buffer(file, Buffer::Flags::NoHooks); Buffer* buffer = open_file_buffer(file, Buffer::Flags::NoHooks);
if (not suffix_backup.empty()) if (not suffix_backup.empty())
write_buffer_to_file(*buffer, buffer->name() + suffix_backup, write_buffer_to_file(*buffer, buffer->name() + suffix_backup,
WriteFlags::None); WriteMethod::Overwrite, WriteFlags::None);
apply_to_buffer(*buffer); apply_to_buffer(*buffer);
write_buffer_to_file(*buffer, buffer->name(), write_buffer_to_file(*buffer, buffer->name(),
WriteFlags::None); WriteMethod::Overwrite, WriteFlags::None);
buffer_manager.delete_buffer(*buffer); buffer_manager.delete_buffer(*buffer);
} }
if (not isatty(0)) if (not isatty(0))