tagparser/backuphelper.cpp

310 lines
13 KiB
C++
Raw Normal View History

2015-09-06 19:57:33 +02:00
#include "./backuphelper.h"
#include "./mediafileinfo.h"
#include <c++utilities/conversion/stringconversion.h>
2017-01-27 18:59:22 +01:00
#include <c++utilities/conversion/stringbuilder.h>
2016-06-14 22:53:43 +02:00
#include <c++utilities/io/catchiofailure.h>
#ifdef PLATFORM_WINDOWS
# include <windows.h>
#else
# include <sys/stat.h>
#endif
2015-04-22 19:22:01 +02:00
#include <string>
#include <fstream>
#include <cstdio>
#include <stdexcept>
using namespace std;
using namespace ConversionUtilities;
2016-06-14 22:53:43 +02:00
using namespace IoUtilities;
2015-04-22 19:22:01 +02:00
namespace Media {
/*!
* \namespace Media::BackupHelper
* \brief Helps to create and restore backup files when rewriting
* files to apply changed tag information.
*
2018-02-04 23:55:52 +01:00
* Methods in this namespace are internally used eg. in implementations of AbstractContainer::internalMakeFile().
2015-04-22 19:22:01 +02:00
*/
namespace BackupHelper {
/*!
* \brief Returns the directory used to store backup files.
*
* Setting this value allows creation of backup files in a custom location
* instead of the directory of the file being modified.
2018-02-04 23:55:52 +01:00
*
* \todo Add this as member variable to MediaFileInfo to avoid global.
2015-04-22 19:22:01 +02:00
*/
string &backupDirectory()
{
static string backupDir;
return backupDir;
}
/*!
* \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.
2015-04-22 19:22:01 +02:00
*
* 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
2015-04-22 19:22:01 +02:00
* 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.
*
2015-04-22 19:22:01 +02:00
* \throws Throws std::ios_base::failure on failure.
2017-08-17 19:04:58 +02:00
* \todo Implement callback for progress updates (copy).
2015-04-22 19:22:01 +02:00
*/
void restoreOriginalFileFromBackupFile(const std::string &originalPath, const std::string &backupPath, NativeFileStream &originalStream, NativeFileStream &backupStream)
2015-04-22 19:22:01 +02:00
{
// ensure the orignal stream is closed
2015-04-22 19:22:01 +02:00
if(originalStream.is_open()) {
originalStream.close();
}
// check wether backup file actually exists and close the backup stream afterwards
backupStream.exceptions(ios_base::goodbit);
backupStream.close();
backupStream.clear();
backupStream.open(backupPath, ios_base::in | ios_base::binary);
2015-04-22 19:22:01 +02:00
if(backupStream.is_open()) {
backupStream.close();
} else {
2016-06-14 22:53:43 +02:00
throwIoFailure("Backup/temporary file has not been created.");
2015-04-22 19:22:01 +02:00
}
// remove original file and restore backup
2015-04-22 19:22:01 +02:00
std::remove(originalPath.c_str());
2018-02-04 23:55:52 +01:00
if(std::rename(backupPath.c_str(), originalPath.c_str())) {
// can't rename/move the file (maybe backup dir on another partition) -> make a copy instead
try {
// need to open all streams again
backupStream.exceptions(ios_base::failbit | ios_base::badbit);
originalStream.exceptions(ios_base::failbit | ios_base::badbit);
backupStream.open(backupPath, ios_base::in | ios_base::binary);
originalStream.open(originalPath, ios_base::out | ios_base::binary);
originalStream << backupStream.rdbuf();
// TODO: callback for progress updates
2016-06-14 22:53:43 +02:00
} catch(...) {
catchIoFailure();
2017-01-27 18:59:22 +01:00
throwIoFailure(("Unable to restore original file from backup file \"" % backupPath + "\" after failure.").data());
}
2015-04-22 19:22:01 +02:00
}
}
2018-02-04 23:55:52 +01:00
/*!
* \brief Returns whether the specified \a path is relative.
*/
static bool isRelative(const std::string &path)
{
return path.empty() || (path.front() != '/' && (path.size() < 2 || path[1] != ':'));
}
2015-04-22 19:22:01 +02:00
/*!
* \brief Creates a backup file for the specified file.
* \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.
2015-04-22 19:22:01 +02:00
* \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
2017-08-17 19:04:58 +02:00
* when applying changes. The specified \a backupPath is set to the path of the created backup file.
2017-03-01 18:21:00 +01:00
* The specified \a backupStream will be closed if currently open. Then it is
2015-04-22 19:22:01 +02:00
* 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.
*
2015-04-22 19:22:01 +02:00
* 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.
2017-08-17 19:04:58 +02:00
* \todo Implement callback for progress updates (copy).
2015-04-22 19:22:01 +02:00
*/
void createBackupFile(const std::string &originalPath, std::string &backupPath, NativeFileStream &originalStream, NativeFileStream &backupStream)
2015-04-22 19:22:01 +02:00
{
2018-02-04 23:55:52 +01:00
// determine dirs
const auto &backupDir(backupDirectory());
const auto backupDirRelative(isRelative(backupDir));
const auto originalDir(backupDirRelative ? BasicFileInfo::containingDirectory(originalPath) : string());
// determine the backup path
for(unsigned int i = 0; ; ++i) {
if(backupDir.empty()) {
if(i) {
2017-01-30 00:42:35 +01:00
backupPath = originalPath % '.' % i + ".bak";
} else {
backupPath = originalPath + ".bak";
}
2015-04-22 19:22:01 +02:00
} else {
2018-02-04 23:55:52 +01:00
const auto fileName(BasicFileInfo::fileName(originalPath, i));
if(i) {
2018-02-04 23:55:52 +01:00
const auto ext(BasicFileInfo::extension(originalPath));
if(backupDirRelative) {
backupPath = originalDir % '/' % backupDir % '/' % fileName % '.' % i + ext;
} else {
2018-02-04 23:55:52 +01:00
backupPath = backupDir % '/' % fileName % '.' % i + ext;
}
} else {
2018-02-04 23:55:52 +01:00
if(backupDirRelative) {
backupPath = originalDir % '/' % backupDir % '/' + fileName;
} else {
2018-02-04 23:55:52 +01:00
backupPath = backupDir % '/' + fileName;
}
}
2015-04-22 19:22:01 +02:00
}
2018-02-04 23:55:52 +01:00
// test whether the backup path is still unused; otherwise continue loop
#ifdef PLATFORM_WINDOWS
if(GetFileAttributes(backupPath.c_str()) == INVALID_FILE_ATTRIBUTES) {
#else
2018-02-04 23:55:52 +01:00
struct stat backupStat;
if(stat(backupPath.c_str(), &backupStat)) {
#endif
break;
2018-02-04 23:55:52 +01:00
}
2015-04-22 19:22:01 +02:00
}
// ensure original file is closed
if(originalStream.is_open()) {
originalStream.close();
}
2018-02-04 23:55:52 +01:00
2015-04-22 19:22:01 +02:00
// rename original file
2018-02-04 23:55:52 +01:00
if(std::rename(originalPath.c_str(), backupPath.c_str())) {
// can't rename/move the file (maybe backup dir on another partition) -> make a copy instead
try {
backupStream.exceptions(ios_base::failbit | ios_base::badbit);
originalStream.exceptions(ios_base::failbit | ios_base::badbit);
// ensure backupStream is opened as write-only
if(backupStream.is_open()) {
backupStream.close();
}
backupStream.open(backupPath, ios_base::out | ios_base::binary);
// ensure originalStream is opened with read permissions
originalStream.open(originalPath, ios_base::in | ios_base::binary);
// do the actual copying
backupStream << originalStream.rdbuf();
// streams are closed in the next try-block
// TODO: callback for progress updates
2016-06-14 22:53:43 +02:00
} catch(...) {
catchIoFailure();
throwIoFailure("Unable to rename original file before rewriting it.");
}
2015-04-22 19:22:01 +02:00
}
2018-02-04 23:55:52 +01:00
// manage streams
2015-04-22 19:22:01 +02:00
try {
2018-02-04 23:55:52 +01:00
// 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
2015-04-22 19:22:01 +02:00
if(backupStream.is_open()) {
backupStream.close();
}
// open backup stream
backupStream.exceptions(ios_base::failbit | ios_base::badbit);
backupStream.open(backupPath, ios_base::in | ios_base::binary);
2016-06-14 22:53:43 +02:00
} catch(...) {
catchIoFailure();
// can't open the new file
// -> try to re-rename backup file in the error case to restore previous state
2018-02-04 23:55:52 +01:00
if(std::rename(backupPath.c_str(), originalPath.c_str())) {
2017-01-27 18:59:22 +01:00
throwIoFailure(("Unable to restore original file from backup file \"" % backupPath + "\" after failure.").data());
2015-04-22 19:22:01 +02:00
} else {
2016-06-14 22:53:43 +02:00
throwIoFailure("Unable to open backup file.");
2015-04-22 19:22:01 +02:00
}
}
}
/*!
* \brief Handles a failure/abort which occured 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 catched; this method uses the "exception dispatcher" idiom.
*
* \param fileInfo Specifies the MediaFileInfo instace 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 context Specifies the context used to add notifications.
*/
void handleFailureAfterFileModified(MediaFileInfo &fileInfo, const std::string &backupPath, NativeFileStream &outputStream, NativeFileStream &backupStream, 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
fileInfo.addNotification(NotificationType::Information, "Rewriting the file to apply changed tag information has been aborted.", context);
try {
restoreOriginalFileFromBackupFile(fileInfo.path(), backupPath, outputStream, backupStream);
fileInfo.addNotification(NotificationType::Information, "The original file has been restored.", context);
2016-06-14 22:53:43 +02:00
} catch(...) {
fileInfo.addNotification(NotificationType::Critical, catchIoFailure(), context);
}
} else {
fileInfo.addNotification(NotificationType::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
fileInfo.addNotification(NotificationType::Critical, "Rewriting the file to apply changed tag information failed.", context);
try {
restoreOriginalFileFromBackupFile(fileInfo.path(), backupPath, outputStream, backupStream);
fileInfo.addNotification(NotificationType::Information, "The original file has been restored.", context);
2016-06-14 22:53:43 +02:00
} catch(...) {
fileInfo.addNotification(NotificationType::Critical, catchIoFailure(), context);
}
} else {
fileInfo.addNotification(NotificationType::Critical, "Applying new tag information failed.", context);
}
throw;
2016-06-14 22:53:43 +02:00
} catch(...) {
const char *what = catchIoFailure();
if(!backupPath.empty()) {
// a temp/backup file has been created -> restore original file
fileInfo.addNotification(NotificationType::Critical, "An IO error occured when rewriting the file to apply changed tag information.", context);
try {
restoreOriginalFileFromBackupFile(fileInfo.path(), backupPath, outputStream, backupStream);
fileInfo.addNotification(NotificationType::Information, "The original file has been restored.", context);
2016-06-14 22:53:43 +02:00
} catch(...) {
fileInfo.addNotification(NotificationType::Critical, catchIoFailure(), context);
}
} else {
fileInfo.addNotification(NotificationType::Critical, "An IO error occured when applying tag information.", context);
}
2016-06-14 22:53:43 +02:00
throwIoFailure(what);
}
}
2015-04-22 19:22:01 +02:00
}
}