improved file handling

- don't override backup files
- allow saving files at a different location
- reduce code duplication for restoring backups
This commit is contained in:
Martchus 2016-05-01 20:02:44 +02:00
parent 7010aee0e1
commit 6c6ab0e301
10 changed files with 406 additions and 236 deletions

View File

@ -150,7 +150,7 @@ set(META_APP_NAME "Tag Parser")
set(META_APP_AUTHOR "Martchus")
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
set(META_APP_DESCRIPTION "C++ library for reading and writing MP4 (iTunes), ID3, Vorbis and Matroska tags.")
set(META_VERSION_MAJOR 5)
set(META_VERSION_MAJOR 6)
set(META_VERSION_MINOR 0)
set(META_VERSION_PATCH 0)

View File

@ -1,5 +1,13 @@
#include "./backuphelper.h"
#include "./basicfileinfo.h"
#include "./mediafileinfo.h"
#include <c++utilities/conversion/stringconversion.h>
#ifdef PLATFORM_WINDOWS
# include <windows.h>
#else
# include <sys/stat.h>
#endif
#include <string>
#include <fstream>
@ -7,6 +15,7 @@
#include <stdexcept>
using namespace std;
using namespace ConversionUtilities;
namespace Media {
@ -15,13 +24,16 @@ namespace Media {
* \brief Helps to create and restore backup files when rewriting
* files to apply changed tag information.
*
* This class is only internally used (eg. in implementations of AbstractContainer::internalMake()).
* Methods in this namespace are internally used eg. in implementations of AbstractContainer::internalMake().
*/
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.
*/
string &backupDirectory()
{
@ -36,11 +48,14 @@ string &backupDirectory()
* \param originalStream A std::fstream instance for the original file.
* \param backupStream A std::fstream instance for the backup file.
*
* This helper function is used by MediaFileInfo to restore the original
* file from the specified backup file in the case a Failure
* 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.
*/
void restoreOriginalFileFromBackupFile(const string &originalPath, const string &backupPath, fstream &originalStream, fstream &backupStream)
@ -53,16 +68,27 @@ void restoreOriginalFileFromBackupFile(const string &originalPath, const string
backupStream.exceptions(ios_base::goodbit);
backupStream.close();
backupStream.clear();
backupStream.open(backupPath, ios_base::in | ios_base::out | ios_base::binary);
backupStream.open(backupPath, ios_base::in | ios_base::binary);
if(backupStream.is_open()) {
backupStream.close();
} else {
throw ios_base::failure("Backup/temporary file could not be created.");
throw ios_base::failure("Backup/temporary file has not been created.");
}
// remove original file and restore backup
std::remove(originalPath.c_str());
if(std::rename(backupPath.c_str(), originalPath.c_str()) != 0) { // restore backup
throw ios_base::failure("Unable to restore original file from backup file \"" + backupPath + "\" after failure.");
// unable to move the file
try { // to copy
// 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
} catch(const ios_base::failure &) {
throw ios_base::failure("Unable to restore original file from backup file \"" + backupPath + "\" after failure.");
}
}
}
@ -70,56 +96,130 @@ void restoreOriginalFileFromBackupFile(const string &originalPath, const string
* \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.
* \param backupStream Specifies a std::fstream for creating the backup file.
*
* This helper function is used by MediaFileInfo to create a backup file
* This helper function is used by MediaFileInfo and container implementations to create a backup file
* when applying changes. The path of the created backup file is set to \a backup path.
* The specified \a backupStream will be closed if currently open. Then is 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.
*/
void createBackupFile(const string &originalPath, string &backupPath, fstream &backupStream)
void createBackupFile(const string &originalPath, string &backupPath, fstream &originalStream, fstream &backupStream)
{
// set the backup path
// determine the backup path
const string &backupDir = backupDirectory();
if(backupDir.empty()) {
backupPath = originalPath + ".bak";
} else {
string fileName = BasicFileInfo::fileName(originalPath);
if(backupDir.at(0) != '/' && (backupDir.size() < 2 || backupDir.at(1) != ':')) {
// backupDir is a relative path
backupPath = BasicFileInfo::containingDirectory(originalPath);
backupPath += '/';
backupPath += backupDir;
backupPath += '/';
backupPath += fileName;
#ifndef PLATFORM_WINDOWS
struct stat backupStat;
#endif
for(unsigned int i = 0; ; ++i) {
if(backupDir.empty()) {
if(i) {
backupPath = originalPath + '.' + numberToString(i) + ".bak";
} else {
backupPath = originalPath + ".bak";
}
} else {
// backupDir is an absolute path
backupPath = backupDir;
backupPath += '/';
backupPath += fileName;
const string fileName = BasicFileInfo::fileName(originalPath, i);
if(i) {
const string ext = BasicFileInfo::extension(originalPath);
if(backupDir.at(0) != '/' && (backupDir.size() < 2 || backupDir.at(1) != ':')) {
// backupDir is a relative path
backupPath = BasicFileInfo::containingDirectory(originalPath);
backupPath += '/';
backupPath += backupDir;
backupPath += '/';
backupPath += fileName;
backupPath += '.';
backupPath += numberToString(i);
backupPath += ext;
} else {
// backupDir is an absolute path
backupPath = backupDir;
backupPath += '/';
backupPath += fileName;
backupPath += '.';
backupPath += numberToString(i);
backupPath += ext;
}
} else {
if(backupDir.at(0) != '/' && (backupDir.size() < 2 || backupDir.at(1) != ':')) {
// backupDir is a relative path
backupPath = BasicFileInfo::containingDirectory(originalPath);
backupPath += '/';
backupPath += backupDir;
backupPath += '/';
backupPath += fileName;
} else {
// backupDir is an absolute path
backupPath = backupDir;
backupPath += '/';
backupPath += fileName;
}
}
}
// test whether the backup file already exists
#ifdef PLATFORM_WINDOWS
if(GetFileAttributes(backupPath.c_str()) == INVALID_FILE_ATTRIBUTES) {
#else
if(stat(backupPath.c_str(), &backupStat)) {
#endif
break;
} // else: the backup file already exists -> find another file name
}
// remove backup file if already exists
std::remove(backupPath.c_str());
// ensure original file is closed
if(originalStream.is_open()) {
originalStream.close();
}
// rename original file
if(std::rename(originalPath.c_str(), backupPath.c_str()) != 0) {
throw ios_base::failure("Unable to rename original file before rewriting it.");
// can't rename/move the file
try { // to copy
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
} catch(const ios_base::failure &) {
throw ios_base::failure("Unable to rename original file before rewriting it.");
}
}
try {
// ensure there is no file associated with the backuStream object
// ensure there is not 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(backupPath.c_str(), ios_base::in | ios_base::binary);
backupStream.open(backupPath, ios_base::in | ios_base::binary);
} catch(const ios_base::failure &) {
// try to re-rename backup file in the error case
// can't open the new file
// -> try to re-rename backup file in the error case to restore previous state
if(std::rename(backupPath.c_str(), originalPath.c_str()) != 0) {
throw ios_base::failure("Unable to restore original file from backup file \"" + backupPath + "\" after failure.");
} else {
@ -128,6 +228,82 @@ void createBackupFile(const string &originalPath, string &backupPath, fstream &b
}
}
/*!
* \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 string &backupPath, fstream &outputStream, fstream &backupStream, const 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);
} catch(const ios_base::failure &ex) {
fileInfo.addNotification(NotificationType::Critical, ex.what(), 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);
} catch(const ios_base::failure &ex) {
fileInfo.addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
fileInfo.addNotification(NotificationType::Critical, "Applying new tag information failed.", context);
}
throw;
} catch(const ios_base::failure &) {
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);
} catch(const ios_base::failure &ex) {
fileInfo.addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
fileInfo.addNotification(NotificationType::Critical, "An IO error occured when applying tag information.", context);
}
throw;
}
}
}
}

View File

@ -8,11 +8,14 @@
namespace Media {
class MediaFileInfo;
namespace BackupHelper {
LIB_EXPORT std::string &backupDirectory();
LIB_EXPORT void restoreOriginalFileFromBackupFile(const std::string &originalPath, const std::string &backupPath, std::fstream &originalStream, std::fstream &backupStream);
LIB_EXPORT void createBackupFile(const std::string &originalPath, std::string &backupPath, std::fstream &backupStream);
LIB_EXPORT void createBackupFile(const std::string &originalPath, std::string &backupPath, std::fstream &originalStream, std::fstream &backupStream);
LIB_EXPORT void handleFailureAfterFileModified(MediaFileInfo &mediaFileInfo, const std::string &backupPath, std::fstream &outputStream, std::fstream &backupStream, const std::string &context = "making file");
}

View File

@ -221,12 +221,4 @@ void BasicFileInfo::invalidated()
close();
}
/*!
* \brief Call this function when subclassing to report a changed file size.
*/
void BasicFileInfo::reportSizeChanged(uint64 newSize)
{
m_size = newSize;
}
}

View File

@ -12,18 +12,23 @@ namespace Media {
class LIB_EXPORT BasicFileInfo
{
public:
// constructor, destructor
BasicFileInfo(const std::string &path = std::string());
BasicFileInfo(const BasicFileInfo &) = delete;
BasicFileInfo &operator=(const BasicFileInfo &) = delete;
virtual ~BasicFileInfo();
// methods to control associated file stream
void open(bool readOnly = false);
void reopen(bool readonly = false);
bool isOpen() const;
bool isReadOnly() const;
void close();
void invalidate();
std::fstream &stream();
const std::fstream &stream() const;
// methods to get, set path (components)
const std::string &path() const;
void setPath(const std::string &path);
static std::string fileName(const std::string &path, bool cutExtension = false);
@ -34,10 +39,11 @@ public:
std::string pathWithoutExtension() const;
static std::string containingDirectory(const std::string &path);
std::string containingDirectory() const;
std::fstream &stream();
const std::fstream &stream() const;
// methods to get, set the file size
uint64 size() const;
void reportSizeChanged(uint64 newSize);
void reportPathChanged(const std::string &newPath);
protected:
virtual void invalidated();
@ -105,6 +111,24 @@ inline uint64 BasicFileInfo::size() const
return m_size;
}
/*!
* \brief Call this function to report that the size changed.
* \remarks Should be called after writing/truncating the stream().
*/
inline void BasicFileInfo::reportSizeChanged(uint64 newSize)
{
m_size = newSize;
}
/*!
* \brief Call this function to report that the path changed.
* \remarks Should be called after associating another file to the stream() manually.
*/
inline void BasicFileInfo::reportPathChanged(const std::string &newPath)
{
m_path = newPath;
}
}
#endif // BASICFILEINFO_H

View File

@ -814,7 +814,7 @@ void MatroskaContainer::internalMakeFile()
// -> holds new padding
uint64 newPadding;
// -> whether rewrite is required (always required when forced to rewrite)
bool rewriteRequired = fileInfo().isForcingRewrite();
bool rewriteRequired = fileInfo().isForcingRewrite() || !fileInfo().saveFilePath().empty();
// calculate EBML header size
// -> sub element ID sizes
@ -1341,20 +1341,34 @@ nonRewriteCalculations:
char buff[8]; // buffer used to make size denotations
if(rewriteRequired) {
// move current file to temp dir and reopen it as backupStream, recreate original file
try {
// ensure the file is close before moving
fileInfo().close();
BackupHelper::createBackupFile(fileInfo().path(), backupPath, backupStream);
// set backup stream as associated input stream since we need the original elements to write the new file
setStream(backupStream);
// recreate original file, define buffer variables
outputStream.open(fileInfo().path(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Creation of temporary file (to rewrite the original file) failed.", context);
throw;
if(fileInfo().saveFilePath().empty()) {
// move current file to temp dir and reopen it as backupStream, recreate original file
try {
BackupHelper::createBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
// recreate original file, define buffer variables
outputStream.open(fileInfo().path(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Creation of temporary file (to rewrite the original file) failed.", context);
throw;
}
} else {
// open the current file as backupStream and create a new outputStream at the specified "save file path"
try {
backupStream.exceptions(ios_base::badbit | ios_base::failbit);
backupStream.open(fileInfo().path(), ios_base::in | ios_base::binary);
fileInfo().close();
outputStream.open(fileInfo().saveFilePath(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Opening streams to write output file failed.", context);
throw;
}
}
// set backup stream as associated input stream since we need the original elements to write the new file
setStream(backupStream);
// TODO: reduce code duplication
} else { // !rewriteRequired
// buffer currently assigned attachments
for(auto &maker : attachmentMaker) {
@ -1643,6 +1657,13 @@ nonRewriteCalculations:
if(rewriteRequired) {
// report new size
fileInfo().reportSizeChanged(outputStream.tellp());
// "save as path" is now the regular path
if(!fileInfo().saveFilePath().empty()) {
fileInfo().reportPathChanged(fileInfo().saveFilePath());
fileInfo().setSaveFilePath(string());
}
// the outputStream needs to be reopened to be able to read again
outputStream.close();
outputStream.open(fileInfo().path(), ios_base::in | ios_base::out | ios_base::binary);
@ -1690,56 +1711,9 @@ nonRewriteCalculations:
outputStream.flush();
// handle errors (which might have been occured after renaming/creating backup file)
} catch(const OperationAbortedException &) {
reset();
if(&stream() != &outputStream) {
// a temp/backup file has been created -> restore original file
setStream(outputStream);
addNotification(NotificationType::Information, "Rewriting the file to apply changed tag information has been aborted.", context);
try {
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
addNotification(NotificationType::Information, "The original file has been restored.", context);
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
addNotification(NotificationType::Information, "Applying new tag information has been aborted.", context);
}
throw;
} catch(const Failure &) {
reset();
if(&stream() != &outputStream) {
// a temp/backup file has been created -> restore original file
setStream(outputStream);
addNotification(NotificationType::Critical, "Rewriting the file to apply changed tag information failed.", context);
try {
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
addNotification(NotificationType::Information, "The original file has been restored.", context);
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
addNotification(NotificationType::Critical, "Applying new tag information failed.", context);
}
throw;
} catch(const ios_base::failure &) {
reset();
if(&stream() != &outputStream) {
// a temp/backup file has been created -> restore original file
setStream(outputStream);
addNotification(NotificationType::Critical, "An IO error occured when rewriting the file to apply changed tag information.", context);
try {
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
addNotification(NotificationType::Information, "The original file has been restored.", context);
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
addNotification(NotificationType::Critical, "An IO error occured when applying tag information.", context);
}
throw;
} catch(...) {
BackupHelper::handleFailureAfterFileModified(fileInfo(), backupPath, outputStream, backupStream, context);
}
// TODO: reduce code duplication
}
}

View File

@ -1332,8 +1332,8 @@ void MediaFileInfo::invalidated()
void MediaFileInfo::makeMp3File()
{
const string context("making MP3 file");
// there's no need to rewrite the complete file if there is just are not ID3v2 tags present or to be written
if(!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty()) {
// there's no need to rewrite the complete file if there are no ID3v2 tags present or to be written
if(!isForcingRewrite() && m_id3v2Tags.empty() && m_actualId3v2TagOffsets.empty() && m_saveFilePath.empty()) {
if(m_actualExistingId3v1Tag) {
// there is currently an ID3v1 tag at the end of the file
if(m_id3v1Tag) {
@ -1395,7 +1395,7 @@ void MediaFileInfo::makeMp3File()
}
// check whether rewrite is required
bool rewriteRequired = isForcingRewrite() || (tagsSize > static_cast<uint32>(m_containerOffset));
bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > static_cast<uint32>(m_containerOffset));
uint32 padding = 0;
if(!rewriteRequired) {
// rewriting is not forced and new tag is not too big for available space
@ -1428,18 +1428,29 @@ void MediaFileInfo::makeMp3File()
fstream backupStream; // create a stream to open the backup/original file for the case rewriting the file is required
if(rewriteRequired) {
// move current file to temp dir and reopen it as backupStream, recreate original file
try {
// ensure the file is close before moving
close();
BackupHelper::createBackupFile(path(), backupPath, backupStream);
// recreate original file, define buffer variables
outputStream.open(path(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Creation of temporary file (to rewrite the original file) failed.", context);
throw;
if(m_saveFilePath.empty()) {
// move current file to temp dir and reopen it as backupStream, recreate original file
try {
BackupHelper::createBackupFile(path(), backupPath, outputStream, backupStream);
// recreate original file, define buffer variables
outputStream.open(path(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Creation of temporary file (to rewrite the original file) failed.", context);
throw;
}
} else {
// open the current file as backupStream and create a new outputStream at the specified "save file path"
try {
backupStream.exceptions(ios_base::badbit | ios_base::failbit);
backupStream.open(path(), ios_base::in | ios_base::binary);
outputStream.open(m_saveFilePath, ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Opening streams to write output file failed.", context);
throw;
}
}
} else { // !rewriteRequired
// reopen original file to ensure it is opened for writing
try {
@ -1501,6 +1512,11 @@ void MediaFileInfo::makeMp3File()
if(rewriteRequired) {
// report new size
reportSizeChanged(outputStream.tellp());
// "save as path" is now the regular path
if(!saveFilePath().empty()) {
reportPathChanged(saveFilePath());
m_saveFilePath.clear();
}
// stream is useless for further usage because it is write-only
outputStream.close();
} else {
@ -1521,50 +1537,9 @@ void MediaFileInfo::makeMp3File()
}
}
} catch(const OperationAbortedException &) {
if(&stream() != &outputStream) {
// a temp/backup file has been created -> restore original file
addNotification(NotificationType::Information, "Rewriting the file to apply changed tag information has been aborted.", context);
try {
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, outputStream, backupStream);
addNotification(NotificationType::Information, "The original file has been restored.", context);
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
addNotification(NotificationType::Information, "Applying new tag information has been aborted.", context);
}
throw;
} catch(const Failure &) {
if(&stream() != &outputStream) {
// a temp/backup file has been created -> restore original file
addNotification(NotificationType::Critical, "Rewriting the file to apply changed tag information failed.", context);
try {
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, outputStream, backupStream);
addNotification(NotificationType::Information, "The original file has been restored.", context);
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
addNotification(NotificationType::Critical, "Applying new tag information failed.", context);
}
throw;
} catch(const ios_base::failure &) {
if(&stream() != &outputStream) {
// a temp/backup file has been created -> restore original file
addNotification(NotificationType::Critical, "An IO error occured when rewriting the file to apply changed tag information.", context);
try {
BackupHelper::restoreOriginalFileFromBackupFile(path(), backupPath, outputStream, backupStream);
addNotification(NotificationType::Information, "The original file has been restored.", context);
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
addNotification(NotificationType::Critical, "An IO error occured when applying tag information.", context);
}
throw;
} catch(...) {
BackupHelper::handleFailureAfterFileModified(*this, backupPath, outputStream, backupStream, context);
}
// TODO: reduce code duplication
}
}

View File

@ -68,7 +68,7 @@ public:
MediaFileInfo(const std::string &path);
MediaFileInfo(const MediaFileInfo &) = delete;
MediaFileInfo &operator=(const MediaFileInfo &) = delete;
virtual ~MediaFileInfo();
~MediaFileInfo();
// methods to parse file
void parseContainerFormat();
@ -142,6 +142,8 @@ public:
void clearParsingResults();
// methods to get, set object behaviour
const std::string &saveFilePath() const;
void setSaveFilePath(const std::string &saveFilePath);
bool isForcingFullParse() const;
void setForceFullParse(bool forceFullParse);
bool isForcingRewrite() const;
@ -169,6 +171,7 @@ private:
// currently only the makeMp3File() methods is present; corresponding methods for
// other formats are outsourced to container classes
void makeMp3File();
// fields related to the container
ParsingStatus m_containerParsingStatus;
ContainerFormat m_containerFormat;
@ -177,17 +180,22 @@ private:
bool m_actualExistingId3v1Tag;
std::list<std::streamoff> m_actualId3v2TagOffsets;
std::unique_ptr<AbstractContainer> m_container;
// fields related to the tracks
ParsingStatus m_tracksParsingStatus;
std::unique_ptr<AbstractTrack> m_singleTrack;
// fields related to the tag
ParsingStatus m_tagsParsingStatus;
std::unique_ptr<Id3v1Tag> m_id3v1Tag;
std::vector<std::unique_ptr<Id3v2Tag> > m_id3v2Tags;
// fields related to the chapters and the attachments
ParsingStatus m_chaptersParsingStatus;
ParsingStatus m_attachmentsParsingStatus;
// fields specifying object behaviour
std::string m_saveFilePath;
bool m_forceFullParse;
bool m_forceRewrite;
size_t m_minPadding;
@ -357,6 +365,34 @@ inline const std::vector<std::unique_ptr<Id3v2Tag> > &MediaFileInfo::id3v2Tags()
return m_id3v2Tags;
}
/*!
* \brief Returns the "save file path" which has been set using setSaveFilePath().
* \sa setSaveFilePath()
*/
inline const std::string &MediaFileInfo::saveFilePath() const
{
return m_saveFilePath;
}
/*!
* \brief Sets the "save file path".
*
* If \a saveFilePath is not empty, this path will be used to save the output file
* when applying changes using applyChanges(). Thus the current file is not modified
* by applyChanges() in this case and the variable isForcingRewrite() does not
* affect the behaviour of applyChanges(). If the changes have been applied
* without fatal errors the "save file path" is cleared and used as the
* new regular path().
*
* By default, this path is empty.
*
* \remarks \a saveFilePath mustn't be the current path().
*/
inline void MediaFileInfo::setSaveFilePath(const std::string &saveFilePath)
{
m_saveFilePath = saveFilePath;
}
/*!
* \brief Returns the container for the current file.
*

View File

@ -491,20 +491,34 @@ calculatePadding:
BinaryWriter outputWriter(&outputStream);
if(rewriteRequired) {
// move current file to temp dir and reopen it as backupStream, recreate original file
try {
// ensure the file is close before moving
fileInfo().close();
BackupHelper::createBackupFile(fileInfo().path(), backupPath, backupStream);
// set backup stream as associated input stream since we need the original elements to write the new file
setStream(backupStream);
// recreate original file, define buffer variables
outputStream.open(fileInfo().path(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Creation of temporary file (to rewrite the original file) failed.", context);
throw;
if(fileInfo().saveFilePath().empty()) {
// move current file to temp dir and reopen it as backupStream, recreate original file
try {
BackupHelper::createBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
// recreate original file, define buffer variables
outputStream.open(fileInfo().path(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Creation of temporary file (to rewrite the original file) failed.", context);
throw;
}
} else {
// open the current file as backupStream and create a new outputStream at the specified "save file path"
try {
backupStream.exceptions(ios_base::badbit | ios_base::failbit);
backupStream.open(fileInfo().path(), ios_base::in | ios_base::binary);
fileInfo().close();
outputStream.open(fileInfo().saveFilePath(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Opening streams to write output file failed.", context);
throw;
}
}
// set backup stream as associated input stream since we need the original elements to write the new file
setStream(backupStream);
// TODO: reduce code duplication
} else { // !rewriteRequired
// reopen original file to ensure it is opened for writing
try {
@ -741,6 +755,11 @@ calculatePadding:
if(rewriteRequired) {
// report new size
fileInfo().reportSizeChanged(outputStream.tellp());
// "save as path" is now the regular path
if(!fileInfo().saveFilePath().empty()) {
fileInfo().reportPathChanged(fileInfo().saveFilePath());
fileInfo().setSaveFilePath(string());
}
// the outputStream needs to be reopened to be able to read again
outputStream.close();
outputStream.open(fileInfo().path(), ios_base::in | ios_base::out | ios_base::binary);
@ -807,56 +826,9 @@ calculatePadding:
outputStream.flush();
// handle errors (which might have been occured after renaming/creating backup file)
} catch(const OperationAbortedException &) {
reset();
if(&stream() != &outputStream) {
// a temp/backup file has been created -> restore original file
setStream(outputStream);
addNotification(NotificationType::Information, "Rewriting the file to apply changed tag information has been aborted.", context);
try {
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
addNotification(NotificationType::Information, "The original file has been restored.", context);
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
addNotification(NotificationType::Information, "Applying new tag information has been aborted.", context);
}
throw;
} catch(const Failure &) {
reset();
if(&stream() != &outputStream) {
// a temp/backup file has been created -> restore original file
setStream(outputStream);
addNotification(NotificationType::Critical, "Rewriting the file to apply changed tag information failed.", context);
try {
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
addNotification(NotificationType::Information, "The original file has been restored.", context);
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
addNotification(NotificationType::Critical, "Applying new tag information failed.", context);
}
throw;
} catch(const ios_base::failure &) {
reset();
if(&stream() != &outputStream) {
// a temp/backup file has been created -> restore original file
setStream(outputStream);
addNotification(NotificationType::Critical, "An IO error occured when rewriting the file to apply changed tag information.", context);
try {
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, outputStream, backupStream);
addNotification(NotificationType::Information, "The original file has been restored.", context);
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, ex.what(), context);
}
} else {
addNotification(NotificationType::Critical, "An IO error occured when applying tag information.", context);
}
throw;
} catch(...) {
BackupHelper::handleFailureAfterFileModified(fileInfo(), backupPath, outputStream, backupStream, context);
}
// TODO: reduce code duplication
}
/*!

View File

@ -263,14 +263,33 @@ void OggContainer::internalMakeFile()
const string context("making OGG file");
updateStatus("Prepare for rewriting OGG file ...");
parseTags(); // tags need to be parsed before the file can be rewritten
fileInfo().close();
string backupPath;
fstream backupStream;
try {
BackupHelper::createBackupFile(fileInfo().path(), backupPath, backupStream);
// recreate original file
fileInfo().stream().open(fileInfo().path(), ios_base::out | ios_base::binary | ios_base::trunc);
if(fileInfo().saveFilePath().empty()) {
// move current file to temp dir and reopen it as backupStream, recreate original file
try {
BackupHelper::createBackupFile(fileInfo().path(), backupPath, fileInfo().stream(), backupStream);
// recreate original file, define buffer variables
fileInfo().stream().open(fileInfo().path(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Creation of temporary file (to rewrite the original file) failed.", context);
throw;
}
} else {
// open the current file as backupStream and create a new outputStream at the specified "save file path"
try {
backupStream.exceptions(ios_base::badbit | ios_base::failbit);
backupStream.open(fileInfo().path(), ios_base::in | ios_base::binary);
fileInfo().close();
fileInfo().stream().open(fileInfo().saveFilePath(), ios_base::out | ios_base::binary | ios_base::trunc);
} catch(const ios_base::failure &) {
addNotification(NotificationType::Critical, "Opening streams to write output file failed.", context);
throw;
}
}
try {
// prepare iterating comments
VorbisComment *currentComment;
OggParameter *currentParams;
@ -432,8 +451,15 @@ void OggContainer::internalMakeFile()
}
}
// report new size
fileInfo().reportSizeChanged(stream().tellp());
// "save as path" is now the regular path
if(!fileInfo().saveFilePath().empty()) {
fileInfo().reportPathChanged(fileInfo().saveFilePath());
fileInfo().setSaveFilePath(string());
}
// close backups stream; reopen new file as readable stream
backupStream.close();
fileInfo().close();
@ -447,17 +473,9 @@ void OggContainer::internalMakeFile()
// clear iterator
m_iterator.clear(fileInfo().stream(), startOffset(), fileInfo().size());
} catch(const OperationAbortedException &) {
addNotification(NotificationType::Information, "Rewriting file to apply new tag information has been aborted.", context);
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, fileInfo().stream(), backupStream);
} catch(...) {
m_iterator.setStream(fileInfo().stream());
throw;
} catch(const ios_base::failure &ex) {
addNotification(NotificationType::Critical, "IO error occured when rewriting file to apply new tag information.\n" + string(ex.what()), context);
BackupHelper::restoreOriginalFileFromBackupFile(fileInfo().path(), backupPath, fileInfo().stream(), backupStream);
m_iterator.setStream(fileInfo().stream());
throw;
BackupHelper::handleFailureAfterFileModified(fileInfo(), backupPath, fileInfo().stream(), backupStream, context);
}
}