From 6c6ab0e30149dfc67bf4b5720e96e41f7cd5b748 Mon Sep 17 00:00:00 2001 From: Martchus Date: Sun, 1 May 2016 20:02:44 +0200 Subject: [PATCH] improved file handling - don't override backup files - allow saving files at a different location - reduce code duplication for restoring backups --- CMakeLists.txt | 2 +- backuphelper.cpp | 234 +++++++++++++++++++++++++++++---- backuphelper.h | 5 +- basicfileinfo.cpp | 8 -- basicfileinfo.h | 28 +++- matroska/matroskacontainer.cpp | 98 +++++--------- mediafileinfo.cpp | 87 +++++------- mediafileinfo.h | 38 +++++- mp4/mp4container.cpp | 94 +++++-------- ogg/oggcontainer.cpp | 48 ++++--- 10 files changed, 406 insertions(+), 236 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 01db9c4..697c139 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/backuphelper.cpp b/backuphelper.cpp index 4ccee5f..968ab3d 100644 --- a/backuphelper.cpp +++ b/backuphelper.cpp @@ -1,5 +1,13 @@ #include "./backuphelper.h" -#include "./basicfileinfo.h" +#include "./mediafileinfo.h" + +#include + +#ifdef PLATFORM_WINDOWS +# include +#else +# include +#endif #include #include @@ -7,6 +15,7 @@ #include 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; + + } +} + } } diff --git a/backuphelper.h b/backuphelper.h index 10641be..d1395c1 100644 --- a/backuphelper.h +++ b/backuphelper.h @@ -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"); } diff --git a/basicfileinfo.cpp b/basicfileinfo.cpp index d8a704a..4ef9590 100644 --- a/basicfileinfo.cpp +++ b/basicfileinfo.cpp @@ -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; -} - } diff --git a/basicfileinfo.h b/basicfileinfo.h index b62aa90..fffb5a1 100644 --- a/basicfileinfo.h +++ b/basicfileinfo.h @@ -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 diff --git a/matroska/matroskacontainer.cpp b/matroska/matroskacontainer.cpp index b3277a9..c9f5eee 100644 --- a/matroska/matroskacontainer.cpp +++ b/matroska/matroskacontainer.cpp @@ -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 } } diff --git a/mediafileinfo.cpp b/mediafileinfo.cpp index ceaf402..4bcc438 100644 --- a/mediafileinfo.cpp +++ b/mediafileinfo.cpp @@ -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(m_containerOffset)); + bool rewriteRequired = isForcingRewrite() || !m_saveFilePath.empty() || (tagsSize > static_cast(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 } } diff --git a/mediafileinfo.h b/mediafileinfo.h index 8118929..a5c0c59 100644 --- a/mediafileinfo.h +++ b/mediafileinfo.h @@ -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 m_actualId3v2TagOffsets; std::unique_ptr m_container; + // fields related to the tracks ParsingStatus m_tracksParsingStatus; std::unique_ptr m_singleTrack; + // fields related to the tag ParsingStatus m_tagsParsingStatus; std::unique_ptr m_id3v1Tag; std::vector > 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 > &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. * diff --git a/mp4/mp4container.cpp b/mp4/mp4container.cpp index 2da0322..2a46d4b 100644 --- a/mp4/mp4container.cpp +++ b/mp4/mp4container.cpp @@ -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 } /*! diff --git a/ogg/oggcontainer.cpp b/ogg/oggcontainer.cpp index ae5b6e7..449a39b 100644 --- a/ogg/oggcontainer.cpp +++ b/ogg/oggcontainer.cpp @@ -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); } }