write the output to a temporary file

This commit is contained in:
Frédéric Mangano-Tarumi 2018-12-02 16:20:10 -05:00
parent a74ea34352
commit 1d6ca8fc59
5 changed files with 99 additions and 106 deletions

View File

@ -15,7 +15,7 @@ opustags \- Opus comment editor
.SH DESCRIPTION
.PP
\fBopustags\fP can read and edit the comment header of an Opus file.
It basically has two modes: read-only, and read-write for tag edition.
It basically has two modes: read-only, and read-write for tag editing.
.PP
In read-only mode, only the beginning of \fIINPUT\fP is read, and the tags are
printed on standard output.
@ -23,10 +23,12 @@ printed on standard output.
You can use the options below to edit the tags before printing them.
This could be useful to preview some changes before writing them.
.PP
In edition mode, you need to specify an output file (or \fB-\fP for standard output). It must be
different from the input file. To overwrite the input file, use \fB--in-place\fP.
In editing mode, you need to specify an output file with \fB--output\fP, or use \fB--in-place\fP to
overwrite the input file. If the output is a regular file, the result is first written to a
temporary file and then moved to its final location on success. On error, the temporary output file
is deleted.
.PP
Tag edition can be performed with the \fB--add\fP, \fB--delete\fP and \fB--set\fP
Tag editing can be performed with the \fB--add\fP, \fB--delete\fP and \fB--set\fP
options. Options can be specified in any order and dont conflict with each other.
First the specified tags are deleted, then the new tags are added.
.PP
@ -52,16 +54,15 @@ The input file will be read, its tags edited, then written to the specified outp
\fIFILE\fP is \fB-\fP then the resulting Opus file will be written to standard output.
The output file cant be the same as the input file.
.TP
.B \-i, \-\-in-place\fR[=\fP\fISUFFIX\fP\fR]\fP
Use this when you want to modify the input file in place. opustags will create a temporary output
file with the specified suffix (.otmp by default), and move it to the location of the input file on
success. If a file with the same name as the temporary file already exists, it will be overwritten
without warning.
.B \-i, \-\-in-place
Overwrite the input file instead of creating a separate output file. It has the same effect as
setting \fB--output\fP to the same path as the input file and enabling \fB--overwrite\fP.
This option conflicts with \fB--output\fP.
.TP
.B \-y, \-\-overwrite
By default, \fBopustags\fP refuses to overwrite an already existent file. Use
this option to allow that.
By default, \fBopustags\fP refuses to overwrite an already-existent file.
Use \fB-y\fP to allow overwriting.
Note that this option is not needed when the output is a special file like \fI/dev/null\fP.
.TP
.B \-d, \-\-delete \fIFIELD\fP
Delete all the tags whose field name is \fIFIELD\fP. They may be several one of them, though usually

View File

@ -59,9 +59,10 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt)
opt = {};
if (argc == 1)
return {st::bad_arguments, "No arguments specified. Use -h for help."};
bool in_place = false;
int c;
optind = 0;
while ((c = getopt_long(argc, argv, ":ho:i::yd:a:s:DS", getopt_options, NULL)) != -1) {
while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DS", getopt_options, NULL)) != -1) {
switch (c) {
case 'h':
opt.print_help = true;
@ -74,11 +75,7 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt)
return {st::bad_arguments, "Output file path cannot be empty."};
break;
case 'i':
if (opt.in_place != nullptr)
return {st::bad_arguments, "Cannot specify --in-place more than once."};
opt.in_place = optarg == nullptr ? ".otmp" : optarg;
if (strcmp(opt.in_place, "") == 0)
return {st::bad_arguments, "In-place suffix cannot be empty."};
in_place = true;
break;
case 'y':
opt.overwrite = true;
@ -117,13 +114,17 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt)
opt.path_in = argv[optind];
if (opt.path_in.empty())
return {st::bad_arguments, "Input file path cannot be empty."};
if (opt.in_place != nullptr && !opt.path_out.empty())
return {st::bad_arguments, "Cannot combine --in-place and --output."};
if (in_place) {
if (!opt.path_out.empty())
return {st::bad_arguments, "Cannot combine --in-place and --output."};
if (opt.path_in == "-")
return {st::bad_arguments, "Cannot modify standard input in place."};
opt.path_out = opt.path_in;
opt.overwrite = true;
}
if (opt.path_in == "-" && opt.set_all)
return {st::bad_arguments,
"Cannot use standard input as input file when --set-all is specified."};
if (opt.path_in == "-" && opt.in_place)
return {st::bad_arguments, "Cannot modify standard input in place."};
return st::ok;
}
@ -256,73 +257,76 @@ ot::status ot::run(const ot::options& opt)
return st::ok;
}
std::string path_out = opt.in_place ? opt.path_in + opt.in_place : opt.path_out;
if (!path_out.empty() && path_out != "-") {
struct stat input_info;
if (opt.path_in != "-" && stat(opt.path_in.c_str(), &input_info) == -1)
return {ot::st::fatal_error,
"Could not identify '" + opt.path_in + "': " + strerror(errno)};
struct stat output_info;
if (stat(opt.path_out.c_str(), &output_info) == 0) {
if (opt.path_in != "-" &&
input_info.st_dev == output_info.st_dev &&
input_info.st_ino == output_info.st_ino)
return {ot::st::fatal_error,
"Input and output cannot be the same file. "
"Use --in-place instead."};
if (!opt.overwrite)
return {ot::st::fatal_error,
"'" + path_out + "' already exists. Use -y to overwrite."};
} else if (errno != ENOENT) {
return {ot::st::fatal_error,
"Could not identify '" + opt.path_in + "': " + strerror(errno)};
}
}
ot::file input;
if (opt.path_in == "-") {
if (opt.path_in == "-")
input = stdin;
else if ((input = fopen(opt.path_in.c_str(), "r")) == nullptr)
return {ot::st::standard_error,
"Could not open '" + opt.path_in + "' for reading: " + strerror(errno)};
ot::ogg_reader reader(input.get());
/* Read-only mode. */
if (opt.path_out.empty())
return process(reader, nullptr, opt);
/* Read-write mode.
*
* The output pointer is set to one of:
* - stdout for "-",
* - final_output.get() for special files like /dev/null,
* - temporary_output.get() for regular files.
*
* We use a temporary output file for the following reasons:
* 1. The partial .opus output may be seen by softwares like media players, or through
* inotify for the most attentive process.
* 2. If the process crashes badly, or the power cuts off, we don't want to leave a partial
* file at the final location. The temporary file is still going to stay but will have an
* obvious name.
* 3. If we're overwriting a regular file, we'd rather avoid wiping its content before we
* even started reading the input file. That way, the original file is always preserved
* on error or crash.
* 4. It is necessary for in-place editing. We can't reliably open the same file as both
* input and output.
*/
FILE* output = nullptr;
ot::partial_file temporary_output;
ot::file final_output;
ot::status rc = ot::st::ok;
struct stat output_info;
if (opt.path_out == "-") {
output = stdout;
} else if (stat(opt.path_out.c_str(), &output_info) == 0) {
/* The output file exists. */
if (!S_ISREG(output_info.st_mode)) {
/* Special files are opened for writing directly. */
if ((final_output = fopen(opt.path_out.c_str(), "w")) == nullptr)
rc = {ot::st::standard_error,
"Could not open '" + opt.path_out + "' for writing: " +
strerror(errno)};
output = final_output.get();
} else if (opt.overwrite) {
rc = temporary_output.open(opt.path_out.c_str());
output = temporary_output.get();
} else {
rc = {ot::st::fatal_error,
"'" + opt.path_out + "' already exists. Use -y to overwrite."};
}
} else if (errno == ENOENT) {
rc = temporary_output.open(opt.path_out.c_str());
output = temporary_output.get();
} else {
input = fopen(opt.path_in.c_str(), "r");
if (input == nullptr)
return {ot::st::standard_error,
"Could not open '" + opt.path_in + "' for reading: " + strerror(errno)};
rc = {ot::st::fatal_error,
"Could not identify '" + opt.path_in + "': " + strerror(errno)};
}
ot::file output;
if (path_out == "-") {
output.reset(stdout);
} else if (!path_out.empty()) {
output = fopen(path_out.c_str(), "w");
if (output == nullptr)
return {ot::st::standard_error,
"Could not open '" + path_out + "' for writing: " + strerror(errno)};
}
ot::status rc;
{
ot::ogg_reader reader(input.get());
std::unique_ptr<ot::ogg_writer> writer;
if (output != nullptr)
writer = std::make_unique<ot::ogg_writer>(output.get());
rc = process(reader, writer.get(), opt);
/* delete reader and writer before closing the files */
}
input.reset();
output.reset();
if (rc != ot::st::ok) {
if (!path_out.empty() && path_out != "-")
remove(path_out.c_str());
if (rc != ot::st::ok)
return rc;
}
if (opt.in_place) {
if (rename(path_out.c_str(), opt.path_in.c_str()) == -1)
return {ot::st::fatal_error,
"Could not move the result to '" + opt.path_in + "': " + strerror(errno)};
}
ot::ogg_writer writer(output);
rc = process(reader, &writer, opt);
if (rc == ot::st::ok)
rc = temporary_output.commit();
return ot::st::ok;
return rc;
}

View File

@ -386,8 +386,7 @@ void delete_comments(opus_tags& tags, const char* field_name);
*/
/**
* Structured representation of the command-line arguments to opustags. It must faithfully represent
* what the user asked for, with as little interpretation or deduction as possible.
* Structured representation of the command-line arguments to opustags.
*/
struct options {
/**
@ -404,22 +403,16 @@ struct options {
std::string path_in;
/**
* Path to the optional file. The special "-" string means stdout. When empty, opustags runs
* in read-only mode.
* in read-only mode. For in-place editing, path_out is defined equal to path_in.
*
* Option: --output
* Options: --output, --in-place
*/
std::string path_out;
/**
* If null, in-place editing is disabled. Otherwise, it points to the suffix to add to the
* file name.
* By default, opustags won't overwrite the output file if it already exists. This can be
* forced with --overwrite. It is also enabled by --in-place.
*
* Option: --in-place
*/
const char* in_place = nullptr;
/**
* By default, opustags won't overwrite the output file if it already exists.
*
* Option: --overwrite
* Options: --overwrite, --in-place
*/
bool overwrite = false;
/**

View File

@ -34,15 +34,14 @@ void check_good_arguments()
throw failure("did not catch --help");
opt = parse({"opustags", "x", "--output", "y", "-D", "-s", "X=Y Z"});
if (opt.in_place != nullptr || opt.path_in != "x" || opt.path_out != "y" || !opt.delete_all ||
if (opt.path_in != "x" || opt.path_out != "y" || !opt.delete_all || opt.overwrite ||
opt.to_delete.size() != 1 || opt.to_delete[0] != "X=Y Z" ||
opt.to_add.size() != 1 || opt.to_add[0] != "X=Y Z")
throw failure("unexpected option parsing result for case #1");
opt = parse({"opustags", "-S", "-y", "x", "-S", "-a", "x=y z", "-i"});
if (opt.in_place == nullptr || opt.path_in != "x" || !opt.path_out.empty() ||
!opt.set_all || !opt.overwrite || opt.to_delete.size() != 0 ||
opt.to_add.size() != 1 || opt.to_add[0] != "x=y z")
opt = parse({"opustags", "-S", "x", "-S", "-a", "x=y z", "-i"});
if (opt.path_in != "x" || opt.path_out != "x" || !opt.set_all || !opt.overwrite ||
opt.to_delete.size() != 0 || opt.to_add.size() != 1 || opt.to_add[0] != "x=y z")
throw failure("unexpected option parsing result for case #2");
}
@ -58,7 +57,6 @@ void check_bad_arguments()
};
error_case({"opustags"}, "No arguments specified. Use -h for help.", "no arguments");
error_case({"opustags", "--output", ""}, "Output file path cannot be empty.", "empty output path");
error_case({"opustags", "--in-place="}, "In-place suffix cannot be empty.", "empty in-place suffix");
error_case({"opustags", "--delete", "X="}, "Invalid field name 'X='.", "bad field name for -d");
error_case({"opustags", "-a", "X"}, "Invalid comment 'X'.", "bad comment for -a");
error_case({"opustags", "--set", "X"}, "Invalid comment 'X'.", "bad comment for --set");
@ -76,7 +74,6 @@ void check_bad_arguments()
error_case({"opustags", "-i", "-"}, "Cannot modify standard input in place.", "write stdin in-place");
error_case({"opustags", "-o", "x", "--output", "y", "z"},
"Cannot specify --output more than once.", "double output");
error_case({"opustags", "-i", "-i", "z"}, "Cannot specify --in-place more than once.", "double in-place");
}
int main(int argc, char **argv)

View File

@ -100,9 +100,7 @@ error: 'out.opus' already exists. Use -y to overwrite.
EOF
is(md5('out.opus'), 'd41d8cd98f00b204e9800998ecf8427e', 'the output wasn\'t written');
is_deeply(opustags(qw(out.opus -o out.opus)), ['', <<'EOF', 256], 'output and input can\'t be the same');
error: Input and output cannot be the same file. Use --in-place instead.
EOF
is_deeply(opustags(qw(gobble.opus -o /dev/null)), ['', '', 0], 'write to /dev/null');
is_deeply(opustags(qw(gobble.opus -o out.opus --overwrite)), ['', '', 0], 'overwrite');
is(md5('out.opus'), '111a483596ac32352fbce4d14d16abd2', 'successfully overwritten');