-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathbackuphelper.cpp
301 lines (277 loc) · 13.9 KB
/
backuphelper.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
#include "./backuphelper.h"
#include "./diagnostics.h"
#include "./mediafileinfo.h"
#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/path.h>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
using namespace std;
using namespace CppUtilities;
namespace TagParser {
/*!
* \namespace TagParser::BackupHelper
* \brief Helps to create and restore backup files when rewriting
* files to apply changed tag information.
*
* Methods in this namespace are internally used eg. in implementations of AbstractContainer::internalMakeFile().
*/
namespace BackupHelper {
/*!
* \brief Restores the original file from the specified backup file.
* \param originalPath Specifies the path to the original file.
* \param backupPath Specifies the path to the backup file.
* \param originalStream Specifies a std::fstream instance for the original file.
* \param backupStream Specifies a std::fstream instance for the backup file.
*
* This helper function is used by MediaFileInfo and container implementations
* to restore the original file from the specified backup file in the case a Failure
* or an IO error occurs. The specified streams will be closed if
* currently open.
*
* If moving isn't possible (eg. \a originalPath and \a backupPath refer to different partitions) the backup
* file will be restored by copying.
*
* \throws Throws std::ios_base::failure on failure.
* \todo Implement callback for progress updates (copy).
*/
void restoreOriginalFileFromBackupFile(
const std::string &originalPath, const std::string &backupPath, NativeFileStream &originalStream, NativeFileStream &backupStream)
{
// ensure streams are closed but don't handle any errors anymore at this point
originalStream.exceptions(ios_base::goodbit);
backupStream.exceptions(ios_base::goodbit);
originalStream.close();
backupStream.close();
originalStream.clear();
backupStream.clear();
// restore usual exception handling of the streams
originalStream.exceptions(ios_base::badbit | ios_base::failbit);
backupStream.exceptions(ios_base::badbit | ios_base::failbit);
// check whether backup file actually exists and close the backup stream afterwards
const auto originalPathForOpen = std::filesystem::path(makeNativePath(BasicFileInfo::pathForOpen(originalPath)));
const auto backupPathForOpen = std::filesystem::path(makeNativePath(BasicFileInfo::pathForOpen(backupPath)));
auto ec = std::error_code();
if (!std::filesystem::exists(backupPathForOpen, ec) && !ec) {
throw std::ios_base::failure("Backup/temporary file has not been created.");
}
// remove original file and restore backup
std::filesystem::remove(originalPathForOpen, ec);
if (ec) {
throw std::ios_base::failure("Unable to remove original file: " + ec.message());
}
std::filesystem::rename(backupPathForOpen, originalPathForOpen, ec);
if (ec) {
// try making a copy instead, maybe backup dir is on another partition
std::filesystem::copy_file(backupPathForOpen, originalPathForOpen, ec);
}
if (ec) {
throw std::ios_base::failure("Unable to restore original file from backup file \"" % backupPath % "\" after failure: " + ec.message());
}
}
/*!
* \brief Creates a backup file for the specified file.
* \param backupDir Specifies the directory to store backup files. If empty, the directory of the file
* to be backuped is used.
* \param originalPath Specifies the path of the file to be backuped.
* \param backupPath Contains the path of the created backup file when this function returns.
* \param originalStream Specifies a std::fstream for the original file.
* \param backupStream Specifies a std::fstream for creating the backup file.
*
* This helper function is used by MediaFileInfo and container implementations to create a backup file
* when applying changes. The specified \a backupPath is set to the path of the created backup file.
* The specified \a backupStream will be closed if currently open. Then it is
* used to open the backup file using the flags ios_base::in and ios_base::binary.
*
* The specified \a originalStream is closed before performing the move operation.
*
* If moving isn't possible (eg. \a originalPath and \a backupPath refer to different partitions) the backup
* file will be created by copying.
*
* The original file can now be rewritten to apply changes. When this operation fails
* the created backup file can be restored using restoreOriginalFileFromBackupFile().
*
* \throws Throws std::ios_base::failure on failure.
* \todo Implement callback for progress updates (copy).
*/
void createBackupFile(const std::string &backupDir, const std::string &originalPath, std::string &backupPath, NativeFileStream &originalStream,
NativeFileStream &backupStream)
{
// determine dirs
const auto backupDirRelative = std::filesystem::path(makeNativePath(backupDir)).is_relative();
const auto originalDir = backupDirRelative ? BasicFileInfo::containingDirectory(originalPath) : string();
// determine the backup path
auto ec = std::error_code();
for (unsigned int i = 0;; ++i) {
if (backupDir.empty()) {
if (i) {
backupPath = originalPath % '.' % i + ".bak";
} else {
backupPath = originalPath + ".bak";
}
} else {
const auto fileName(BasicFileInfo::fileName(originalPath, i));
if (i) {
const auto ext(BasicFileInfo::extension(originalPath));
if (backupDirRelative) {
backupPath = originalDir % '/' % backupDir % '/' % fileName % '.' % i + ext;
} else {
backupPath = backupDir % '/' % fileName % '.' % i + ext;
}
} else {
if (backupDirRelative) {
backupPath = originalDir % '/' % backupDir % '/' + fileName;
} else {
backupPath = backupDir % '/' + fileName;
}
}
}
// test whether the backup path is still unused; otherwise continue loop
if (!std::filesystem::exists(makeNativePath(BasicFileInfo::pathForOpen(backupPath)), ec)) {
break;
}
}
// ensure original file is closed
if (originalStream.is_open()) {
originalStream.close();
}
// rename original file
const auto u8originalPath = std::filesystem::path(makeNativePath(originalPath));
const auto backupPathForOpen = std::filesystem::path(makeNativePath(BasicFileInfo::pathForOpen(backupPath)));
std::filesystem::rename(u8originalPath, backupPathForOpen, ec);
if (ec) {
// try making a copy instead, maybe backup dir is on another partition
std::filesystem::copy_file(u8originalPath, backupPathForOpen, ec);
}
if (ec) {
throw std::ios_base::failure(argsToString("Unable to create backup file \"", BasicFileInfo::pathForOpen(backupPath), "\" of \"", originalPath,
"\" before rewriting it: " + ec.message()));
}
// manage streams
try {
// ensure there is no file associated with the originalStream object
if (originalStream.is_open()) {
originalStream.close();
}
// ensure there is no file associated with the backupStream object
if (backupStream.is_open()) {
backupStream.close();
}
// open backup stream
backupStream.exceptions(ios_base::failbit | ios_base::badbit);
backupStream.open(BasicFileInfo::pathForOpen(backupPath).data(), ios_base::in | ios_base::binary);
} catch (const std::ios_base::failure &failure) {
// try to restore the previous state in the error case
try {
restoreOriginalFileFromBackupFile(originalPath, backupPath, originalStream, backupStream);
} catch (const std::ios_base::failure &) {
throw std::ios_base::failure("Unable to restore original file from backup file \"" % backupPath % "\" after failure: " + failure.what());
}
throw std::ios_base::failure(argsToString("Unable to open backup file: ", failure.what()));
}
}
/*!
* \brief Creates a backup file like createBackupFile() but canonicalizes \a originalPath before doing the backup.
* \remarks
* - This function sets \a originalPath to be a canonical path.
* - Using this function (instead of createBackupFile()) is recommended so the actual file is being altered.
*/
void createBackupFileCanonical(const std::string &backupDir, std::string &originalPath, std::string &backupPath,
CppUtilities::NativeFileStream &originalStream, CppUtilities::NativeFileStream &backupStream)
{
auto ec = std::error_code();
if (const auto canonicalPath = std::filesystem::canonical(makeNativePath(BasicFileInfo::pathForOpen(originalPath)), ec); !ec) {
originalPath = canonicalPath.string();
} else {
throw std::ios_base::failure("Unable to canonicalize path of original file before rewriting it: " + ec.message());
}
createBackupFile(backupDir, originalPath, backupPath, originalStream, backupStream);
}
/*!
* \brief Handles a failure/abort which occurred after the file has been modified.
*
* - Restores the backup file using restoreOriginalFileFromBackupFile() if one has been created.
* - Adds appropriate notifications to the specified \a fileInfo.
* - Re-throws the exception.
*
* \remarks Must only be called when an exception derived from Failure or ios_base::failure
* has been caught; this method uses the "exception dispatcher" idiom.
*
* \param fileInfo Specifies the MediaFileInfo instance which has been modified.
* \param backupPath Specifies the path of the backup file; might be empty if none has been created.
* \param outputStream Specifies the stream used to write the output file. This is usually just the stream
* of \a fileInfo, but is specified here explicitly for higher flexibility.
* \param backupStream Specifies the stream assembled using createBackupFile(); might be a default fstream if
* no backup file has been created.
* \param diag Specifies the container to add diagnostic messages to.
* \param context Specifies the context used to add notifications.
*/
void handleFailureAfterFileModified(MediaFileInfo &fileInfo, const std::string &backupPath, NativeFileStream &outputStream,
NativeFileStream &backupStream, Diagnostics &diag, const std::string &context)
{
handleFailureAfterFileModifiedCanonical(fileInfo, fileInfo.path(), backupPath, outputStream, backupStream, diag, context);
}
/*!
* \brief Handles a failure/abort which occurred after the file has been modified.
* \remarks Same as handleFailureAfterFileModified() but allows specifying the original path instead of just using the
* path from \a mediaFileInfo.
*/
void handleFailureAfterFileModifiedCanonical(MediaFileInfo &fileInfo, const std::string &originalPath, const std::string &backupPath,
CppUtilities::NativeFileStream &outputStream, CppUtilities::NativeFileStream &backupStream, Diagnostics &diag, const std::string &context)
{
// reset the associated container in any case
if (fileInfo.container()) {
fileInfo.container()->reset();
}
// re-throw the current exception
try {
throw;
} catch (const OperationAbortedException &) {
if (!backupPath.empty()) {
// a temp/backup file has been created -> restore original file
diag.emplace_back(DiagLevel::Information, "Rewriting the file to apply changed tag information has been aborted.", context);
try {
restoreOriginalFileFromBackupFile(originalPath, backupPath, outputStream, backupStream);
diag.emplace_back(DiagLevel::Warning, "The original file has been restored.", context);
} catch (const std::ios_base::failure &failure) {
diag.emplace_back(DiagLevel::Critical, argsToString("The original file could not be restored: ", failure.what()), context);
}
} else {
diag.emplace_back(DiagLevel::Information, "Applying new tag information has been aborted.", context);
}
throw;
} catch (const Failure &) {
if (!backupPath.empty()) {
// a temp/backup file has been created -> restore original file
diag.emplace_back(DiagLevel::Critical, "Rewriting the file to apply changed tag information failed.", context);
try {
restoreOriginalFileFromBackupFile(originalPath, backupPath, outputStream, backupStream);
diag.emplace_back(DiagLevel::Warning, "The original file has been restored.", context);
} catch (const std::ios_base::failure &failure) {
diag.emplace_back(DiagLevel::Critical, argsToString("The original file could not be restored: ", failure.what()), context);
}
} else {
diag.emplace_back(DiagLevel::Critical, "Applying new tag information failed.", context);
}
throw;
} catch (const std::ios_base::failure &) {
if (!backupPath.empty()) {
// a temp/backup file has been created -> restore original file
diag.emplace_back(DiagLevel::Critical, "An IO error occurred when rewriting the file to apply changed tag information.", context);
try {
restoreOriginalFileFromBackupFile(originalPath, backupPath, outputStream, backupStream);
diag.emplace_back(DiagLevel::Warning, "The original file has been restored.", context);
} catch (const std::ios_base::failure &failure) {
diag.emplace_back(DiagLevel::Critical, argsToString("The original file could not be restored: ", failure.what()), context);
}
} else {
diag.emplace_back(DiagLevel::Critical, "An IO error occurred when applying tag information.", context);
}
throw;
}
}
} // namespace BackupHelper
} // namespace TagParser