diff --git a/CMakeLists.txt b/CMakeLists.txt index cc41865..8e14874 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ add_library( src/cli.cc src/ogg.cc src/opus.cc + src/system.cc ) target_link_libraries(libopustags PUBLIC ${OGG_LIBRARIES}) diff --git a/src/opustags.h b/src/opustags.h index 4931ed6..f435dfe 100644 --- a/src/opustags.h +++ b/src/opustags.h @@ -83,6 +83,11 @@ struct status { std::string message; }; +/***********************************************************************************************//** + * \defgroup system System + * \{ + */ + /** * Smart auto-closing FILE* handle. * @@ -92,6 +97,37 @@ struct file : std::unique_ptr { file(FILE* f = nullptr) : std::unique_ptr(f, &fclose) {} }; +/** + * A partial file is a temporary file created to store the result of something. When it is complete, + * it is moved to a final destination. Open it with #open and then you can either #commit it to save + * it to its destination, or you can #abort to delete the temporary file. When the #partial_file + * object is destroyed, it deletes the currently opened temporary file, if any. + */ +class partial_file { +public: + ~partial_file() { abort(); } + /** + * Open a temporary file meant to be moved to the specified destination file path. The + * temporary file is created in the same directory as its destination in order to make the + * final move operation instant. + */ + ot::status open(const char* destination); + /** Close then move the partial file to its final location. */ + ot::status commit(); + /** Delete the temporary file. */ + void abort(); + /** Get the underlying FILE* handle. */ + FILE* get() { return file.get(); } + /** Get the name of the temporary file. */ + const char* name() const { return file == nullptr ? nullptr : temporary_name.c_str(); } +private: + std::string temporary_name; + std::string final_name; + ot::file file; +}; + +/** \} */ + /***********************************************************************************************//** * \defgroup ogg Ogg * \{ diff --git a/src/system.cc b/src/system.cc new file mode 100644 index 0000000..fb5396a --- /dev/null +++ b/src/system.cc @@ -0,0 +1,52 @@ +/** + * \file src/system.cc + * \ingroup system + * + * Provide a high-level interface to system-related features, like filesystem manipulations. + * + * Ideally, all OS-specific features should be grouped here. + * + * This modules shoumd not depend on any other opustags module. + */ + +#include + +#include + +ot::status ot::partial_file::open(const char* destination) +{ + abort(); + final_name = destination; + temporary_name = final_name + ".XXXXXX.part"; + int fd = mkstemps(const_cast(temporary_name.data()), 5); + if (fd == -1) + return {st::standard_error, + "Could not create a partial file for '" + final_name + "': " + + strerror(errno)}; + file = fdopen(fd, "w"); + if (file == nullptr) + return {st::standard_error, + "Could not get the partial file handle to '" + temporary_name + "': " + + strerror(errno)}; + return st::ok; +} + +ot::status ot::partial_file::commit() +{ + if (file == nullptr) + return st::ok; + file.reset(); + if (rename(temporary_name.c_str(), final_name.c_str()) == -1) + return {st::standard_error, + "Could not move the result file '" + temporary_name + "' to '" + + final_name + "': " + strerror(errno) + "."}; + return st::ok; +} + +void ot::partial_file::abort() +{ + if (file == nullptr) + return; + file.reset(); + remove(temporary_name.c_str()); +} diff --git a/t/CMakeLists.txt b/t/CMakeLists.txt index 365b228..c705314 100644 --- a/t/CMakeLists.txt +++ b/t/CMakeLists.txt @@ -1,3 +1,6 @@ +add_executable(system.t EXCLUDE_FROM_ALL system.cc) +target_link_libraries(system.t libopustags) + add_executable(opus.t EXCLUDE_FROM_ALL opus.cc) target_link_libraries(opus.t libopustags) @@ -12,5 +15,5 @@ configure_file(gobble.opus . COPYONLY) add_custom_target( check COMMAND prove "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}" - DEPENDS opustags gobble.opus opus.t ogg.t cli.t + DEPENDS opustags gobble.opus system.t opus.t ogg.t cli.t ) diff --git a/t/system.cc b/t/system.cc new file mode 100644 index 0000000..c1e604b --- /dev/null +++ b/t/system.cc @@ -0,0 +1,44 @@ +#include +#include "tap.h" + +#include +#include + +void check_partial_files() +{ + static const char* result = "partial_file.test"; + std::string name; + { + ot::partial_file bad_tmp; + if (bad_tmp.open("/dev/null") != ot::st::standard_error) + throw failure("cannot open a device as a partial file"); + if (bad_tmp.open(result) != ot::st::ok) + throw failure("could not open a simple result file"); + name = bad_tmp.name(); + if (name.size() != strlen(result) + 12 || + name.compare(0, strlen(result), result) != 0) + throw failure("the temporary name is surprising: " + name); + } + if (access(name.c_str(), F_OK) != -1) + throw failure("the bad temporary file was not deleted"); + + ot::partial_file good_tmp; + if (good_tmp.open(result) != ot::st::ok) + throw failure("could not open the result file"); + name = good_tmp.name(); + if (good_tmp.commit() != ot::st::ok) + throw failure("could not commit the result file"); + if (access(name.c_str(), F_OK) != -1) + throw failure("the good temporary file was not deleted"); + if (access(result, F_OK) != 0) + throw failure("the final result file is not there"); + if (remove(result) != 0) + throw failure("could not remove the result file"); +} + +int main(int argc, char **argv) +{ + std::cout << "1..1\n"; + run(check_partial_files, "test partial files"); + return 0; +}