From c1717e7055a39e1aa0eb973fff1fc137d19960ba Mon Sep 17 00:00:00 2001 From: twanvl Date: Sat, 7 Oct 2006 13:10:39 +0000 Subject: [PATCH] Implemented Packages, + some minor tweaks to headers git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@6 0fc631ac-6414-0410-93d0-97cfa31319b6 --- src/util/error.hpp | 8 + src/util/io/package.cpp | 395 ++++++++++++++++++++++++++++++++++++++++ src/util/io/package.hpp | 183 +++++++++++++++++++ src/util/prec.hpp | 1 + src/util/smart_ptr.hpp | 4 +- src/util/window_id.hpp | 23 +-- 6 files changed, 600 insertions(+), 14 deletions(-) create mode 100644 src/util/io/package.cpp create mode 100644 src/util/io/package.hpp diff --git a/src/util/error.hpp b/src/util/error.hpp index 3c468889..d4359709 100644 --- a/src/util/error.hpp +++ b/src/util/error.hpp @@ -48,6 +48,14 @@ class PackageError : public Error { inline PackageError(const String& str) : Error(str) {} }; +/// A file is not found +class FileNotFoundError : public PackageError { + public: + inline FileNotFoundError(const String& file, const String& package) + : PackageError(_("File not found: ") + file + _(" in package ") + package) + {} +}; + // ----------------------------------------------------------------------------- : Parse errors /// Parse errors diff --git a/src/util/io/package.cpp b/src/util/io/package.cpp new file mode 100644 index 00000000..02f931d0 --- /dev/null +++ b/src/util/io/package.cpp @@ -0,0 +1,395 @@ +//+----------------------------------------------------------------------------+ +//| Description: Magic Set Editor - Program to make Magic (tm) cards | +//| Copyright: (C) 2001 - 2006 Twan van Laarhoven | +//| License: GNU General Public License 2 or later (see file COPYING) | +//+----------------------------------------------------------------------------+ + +// ----------------------------------------------------------------------------- : Includes + +#include +#include +#include +#include +#include +#include + +DECLARE_TYPEOF(Package::FileInfos); + +// ----------------------------------------------------------------------------- : Package : outside + +Package::Package() + : zipStream (nullptr) + , fileStream(nullptr) +{} + +Package::~Package() { + delete zipStream; + delete fileStream; + // remove any remaining temporary files + FOR_EACH(f, files) { + if (f.second.wasWritten()) { + wxRemoveFile(f.second.tempName); + } + } +} + +bool Package::isOpened() const { + return !filename.empty(); +} +bool Package::needSaveAs() const { + return !filename.empty(); +} +String Package::name() const { + // wxFileName is too slow (profiled) + // wxFileName fn(filename); + // return fn.GetName(); + size_t ext = filename.find_last_of(_('.')); + size_t slash = filename.find_last_of(_("/\\:"), ext); + if (slash == String::npos && ext == String::npos) return filename; + else if (slash == String::npos) return filename.substr(0,ext); + else if ( ext == String::npos) return filename.substr(slash+1); + else return filename.substr(slash+1, ext-slash-1); +} +const String& Package::absoluteFilename() const { + return filename; +} + + +void Package::open(const String& n) { + assert(!isOpened()); // not already opened + // get absolute path + wxFileName fn(n); + fn.Normalize(); + filename = fn.GetFullPath(); + // get modified time + if (!fn.FileExists() || !fn.GetTimes(0, &modified, 0)) { + modified = wxDateTime(0.0); // long time ago + } + // type of package + if (wxDirExists(filename)) { + openDirectory(); + } else if (wxFileExists(filename)) { + openZipfile(); + } else { + throw PackageError(_("Package not found: '") + filename + _("'")); + } +} + +void Package::save(bool removeUnused) { + assert(!needSaveAs()); + saveAs(filename, removeUnused); +} + +void Package::saveAs(const String& name, bool removeUnused) { + // type of package + if (wxDirExists(name)) { + saveToDirectory(name, removeUnused); + } else { + saveToZipfile (name, removeUnused); + } + filename = name; + // cleanup : remove temp files, remove deleted files from the list + FileInfos::iterator it = files.begin(); + while (it != files.end()) { + if (it->second.wasWritten()) { + // remove corresponding temp file + wxRemoveFile(it->second.tempName); + } + if (!it->second.keep && removeUnused) { + // also remove the record of deleted files + FileInfos::iterator toRemove = it; + ++it; + files.erase(toRemove); + } else { + // free zip entry, we will reopen the file + it->second.keep = false; + it->second.tempName.clear(); + delete it->second.zipEntry; + it->second.zipEntry = 0; + ++it; + } + } + // reopen only needed for zipfile + if (!wxDirExists(name)) { + openZipfile(); + } else { + // make sure we have no zip open + delete zipStream; zipStream = nullptr; + delete fileStream; fileStream = nullptr; + } +} + +// ----------------------------------------------------------------------------- : Package : inside + +/// Class that is a wxZipInputStream over a wxFileInput stream +/** Note that wxFileInputStream is also a base class, because it must be constructed first + * This class requires a patch in wxWidgets (2.5.4) + * change zipstrm.cpp line 1745:; + * if ((!m_ffile || AtHeader())); + * to: + * if ((AtHeader())); + * It seems that in 2.6.3 this is no longer necessary (TODO: test) + */ +class ZipFileInputStream : private wxFileInputStream, public wxZipInputStream { + public: + ZipFileInputStream(const String& filename, wxZipEntry* entry) + : wxFileInputStream(filename) + , wxZipInputStream(static_cast(*this)) + { + OpenEntry(*entry); + } +}; + +InputStreamP Package::openIn(const String& file) { +// if (!n.empty() && n.getChar(0) == _('/')) { +// // absolute path, open file from another package +// return packMan.openFileFromPackage(n); +// } + FileInfos::iterator it = files.find(toStandardName(file)); + if (it == files.end()) { + throw FileNotFoundError(file, filename); + } + InputStreamP stream; + if (it->second.wasWritten()) { + // written to this file, open the temp file + stream = new_shared1(it->second.tempName); + } else if (wxFileExists(filename+_("/")+file)) { + // a file in directory package + stream = new_shared1(filename+_("/")+file); + } else if (wxFileExists(filename) && it->second.zipEntry) { + // a file in a zip archive + stream = static_pointer_cast( + new_shared2(filename, it->second.zipEntry)); + } else { + // shouldn't happen, packaged changed by someone else since opening it + throw FileNotFoundError(file, filename); + } + if (!stream || !stream->IsOk()) { + throw FileNotFoundError(file, filename); + } else { + return stream; + } +} + +OutputStreamP Package::openOut(const String& file) { + return new_shared1(nameOut(file)); +} + +String Package::nameOut(const String& file) { + assert(wxThread::IsMain()); // Writing should only be done from the main thread + FileInfos::iterator it = files.find(file); + if (it == files.end()) { + // new file + it = addFile(file); + } + // return stream + if (it->second.wasWritten()) { + return it->second.tempName; + } else { + // create temp file + String name = wxFileName::CreateTempFileName(_("mse")); + it->second.tempName = name; + return name; + } +} + +String Package::newFileName(const String& prefix, const String& suffix) { + assert(wxThread::IsMain()); // Writing should only be done from the main thread + String name; + UInt infix = 0; + while (true) { + // create filename + name = prefix; + name << ++infix; + name += suffix; + // check if a file with that name exists + FileInfos::iterator it = files.find(name); + if (it == files.end()) { + // name doesn't exist yet + addFile(name); + return name; + } + } +} + +String Package::absoluteName(const String& file) { + assert(wxThread::IsMain()); + FileInfos::iterator it = files.find(toStandardName(file)); + if (it == files.end()) { + throw FileNotFoundError(file, filename); + } + if (it->second.wasWritten()) { + // written to this file, return the temp file + return it->second.tempName; + } else if (wxFileExists(filename+_("/")+file)) { + // dir package + return filename+_("/")+file; + } else { + // assume zip package + return filename+_("\1")+file; + } +} + +// ----------------------------------------------------------------------------- : Package : private + +Package::FileInfo::FileInfo() + : keep(false), zipEntry(nullptr) +{} + +Package::FileInfo::~FileInfo() { + delete zipEntry; +} + + +void Package::openDirectory() { + openSubdir(wxEmptyString); +} + +void Package::openSubdir(const String& name) { + wxDir d(filename + _("/") + name); + if (!d.IsOpened()) return; // ignore errors here + // find files + String f; // filename + for(bool ok = d.GetFirst(&f, wxEmptyString, wxDIR_FILES | wxDIR_HIDDEN) ; ok ; ok = d.GetNext(&f)) { + // add file + addFile(name + f); + // get modified time + wxFileName fn(filename + _("/") + name + f); + wxDateTime mod; + if (fn.GetTimes(0, &mod, 0) && mod > modified) modified = mod; + } + // find subdirs + for(bool ok = d.GetFirst(&f, wxEmptyString, wxDIR_DIRS | wxDIR_HIDDEN) ; ok ; ok = d.GetNext(&f)) { + openSubdir(name+f+_("/")); + } +} + +void Package::openZipfile() { + // close old streams + delete fileStream; fileStream = nullptr; + delete zipStream; zipStream = nullptr; + // open streams + fileStream = new wxFileInputStream(filename); + if (!fileStream->IsOk()) throw PackageError(_("Package not found: '")+filename+_("'")); + zipStream = new wxZipInputStream(*fileStream); + if (!zipStream->IsOk()) throw PackageError(_("Package not found: '")+filename+_("'")); + // read zip entries + while (true) { + wxZipEntry* entry = zipStream->GetNextEntry(); + if (!entry) break; + String name = toStandardName(entry->GetName()); + files[name].zipEntry = entry; + } + zipStream->CloseEntry(); +} + + +void Package::saveToDirectory(const String& saveAs, bool removeUnused) { + // write to a directory + FOR_EACH(f, files) { + if (!f.second.keep && removeUnused) { + // remove files that are not to be kept + // ignore failure (new file that is not kept) + wxRemoveFile(saveAs+_("/")+f.first); + } else if (f.second.wasWritten()) { + // move files that were updated + wxRemoveFile(saveAs+_("/")+f.first); + if (!wxRenameFile(f.second.tempName, saveAs+_("/")+f.first)) { + throw PackageError(_("Error while saving, unable to store file")); + } + } else if (filename != saveAs) { + // save as, copy old filess + if (!wxCopyFile(filename+_("/")+f.first, saveAs+_("/")+f.first)) { + throw PackageError(_("Error while saving, unable to store file")); + } + } else { + // old file, just keep it + } + } +} + +void Package::saveToZipfile(const String& saveAs, bool removeUnused) { + // create a temporary zip file name + String tempFile = saveAs + _(".tmp"); + wxRemoveFile(tempFile); + // open zip file + try { + scoped_ptr newFile(new wxFileOutputStream(tempFile)); + if (!newFile->IsOk()) throw PackageError(_("Error while saving, unable to open output file")); + scoped_ptr newZip(new wxZipOutputStream(*newFile)); + if (!newZip->IsOk()) throw PackageError(_("Error while saving, unable to open output file")); + // copy everything to a new zip file, unless it's updated or removed + if (zipStream) newZip->CopyArchiveMetaData(*zipStream); + FOR_EACH(f, files) { + if (!f.second.keep && removeUnused) { + // to remove a file simply don't copy it + } else if (f.second.zipEntry && !f.second.wasWritten()) { + // old file, was also in zip, not changed + zipStream->CloseEntry(); + newZip->CopyEntry(f.second.zipEntry, *zipStream); + f.second.zipEntry = 0; + } else { + // changed file, or the old package was not a zipfile + newZip->PutNextEntry(f.first); + InputStreamP temp = openIn(f.first); + newZip->Write(*temp); + } + } + // close the old file + delete zipStream; zipStream = nullptr; + delete fileStream; fileStream = nullptr; + } catch (Error e) { + // when things go wrong delete the temp file + wxRemoveFile(tempFile); + throw e; + } + // replace the old file with the new file, in effect commiting the changes + if (wxFileExists(saveAs)) { + // rename old file to .bak + wxRemoveFile(saveAs + _(".bak")); + wxRenameFile(saveAs, saveAs + _(".bak")); + } + wxRenameFile(tempFile, saveAs); +} + + +Package::FileInfos::iterator Package::addFile(const String& name) { + return files.insert(make_pair(toStandardName(name), FileInfo())).first; +} + +String Package::toStandardName(const String& name) { + String ret; + FOR_EACH_CONST(c, name) { + if (c==_('\\')) ret += _('/'); + else ret += toLower(c); + } + return ret; +} + +// ----------------------------------------------------------------------------- : Packaged + +// note: reflection must be declared before it is used +IMPLEMENT_REFLECTION(Packaged) { + // default does nothing +} + +Packaged::Packaged() { +} + +void Packaged::open(const String& package) { + Package::open(package); + Reader reader(openIn(typeName()), absoluteFilename() + _("/") + typeName()); +// try { + reader.handle(*this); +// } catch (const ParseError& err) { +// throw FileParseError(err.what(), filename+_("/")+typeName()); // more detailed message +// } +} +void Packaged::save() { + //writeFile(thisT().fileName, thisT()); + Package::save(); +} +void Packaged::saveAs(const String& package) { + //writeFile(thisT().fileName, thisT()); + Package::saveAs(package); +} diff --git a/src/util/io/package.hpp b/src/util/io/package.hpp new file mode 100644 index 00000000..89dd4c08 --- /dev/null +++ b/src/util/io/package.hpp @@ -0,0 +1,183 @@ +//+----------------------------------------------------------------------------+ +//| Description: Magic Set Editor - Program to make Magic (tm) cards | +//| Copyright: (C) 2001 - 2006 Twan van Laarhoven | +//| License: GNU General Public License 2 or later (see file COPYING) | +//+----------------------------------------------------------------------------+ + +#ifndef HEADER_UTIL_IO_PACKAGE +#define HEADER_UTIL_IO_PACKAGE + +// ----------------------------------------------------------------------------- : Includes + +#include + +class wxFileInputStream; +class wxZipInputStream; +class wxZipEntry; + +// ----------------------------------------------------------------------------- : Package + +/// A package is a container for files. On disk it is either a directory or a zip file. +/** Specific types of packages should inherit from Package or from Packaged. + * + * When modifications are made to a package they are not directly written, + * only when save() or saveAs() is called + * + * To accomplish this modified files are first written to temporary files, when save() is called + * the temporary files are moved/copied. + * + * Zip files are accessed using wxZip(Input|Output)Stream. + * The zip input stream appears to only allow one file at a time, since the stream itself maintains + * state about what file we are reading. + * There are multiple solutions: + * 1. (currently used) Open a new ZipInputStream for each file + * 2. (may be faster) First read the file into a memory buffer, + * return a stream based on that buffer (StringInputStream). + * + * TODO: maybe support sub packages (a package inside another package)? + */ +class Package { + public: + // --------------------------------------------------- : Managing the outside of the package + + /// Creates a new package + Package(); + virtual ~Package(); + + /// Is a file opened? + bool isOpened() const; + /// Must the package be saved with saveAs()? + /// This is the case if the package has not been saved/opened before + bool needSaveAs() const; + /// Determines the short name of this package: the filename without path or extension + String name() const; + /// Return the absolute filename of this file + const String& absoluteFilename() const; + + /// Open a package, should only be called when the package is constructed using the default constructor! + /// @pre open not called before [TODO] + void open(const String& package); + + /// Saves the package, by default saves as a zip file, unless + /// it was already a directory + /** If removeUnused=true all files that were in the file and + * are not touched with referenceFile will be deleted from the new archive! + * This is a form of garbage collection, to get rid of old picture files for example. + */ + void save(bool removeUnused = true); + + /// Saves the package under a different filename + void saveAs(const String& package, bool removeUnused = true); + + + // --------------------------------------------------- : Managing the inside of the package + + /// Open an input stream for a file in the package. + InputStreamP openIn(const String& file); + + /// Open an output stream for a file in the package. + /// (changes are only committed with save()) + OutputStreamP openOut(const String& file); + + /// Get a filename that can be written to to modfify a file in the package + /// (changes are only committed with save()) + String nameOut(const String& file); + + /// Creates a new, unique, filename with the specified prefix and suffix + /// for example newFileName("image/",".jpg") -> "image/1.jpg" + /// Returns the name of a temporary file that can be written to. + String newFileName(const String& prefix, const String& suffix); + + /// Signal that a file is still used by this package. + /// Must be called for files not opened using openOut/nameOut + /// If they are to be kept in the package. + void referenceFile(const String& file); + + /// Get an 'absolute filename' for a file in the package. + /// This file can later be opened from anywhere (other process) using openAbsoluteFile() + String absoluteName(const String& file); + + /// Open a file given an absolute filename + static InputStreamP openAbsoluteFile(const String& name); + /* + // --------------------------------------------------- : Managing the inside of the package : IO files + + template + void readFile (String n, T& obj) { + In i(openFileIn(n), filename + _("/") + n); + try { + i(obj); + } catch (ParseError e) { + throw FileParseError(e.what(), filename+_("/")+n); // more detailed message + } + } + + template + void writeFile(const String& file, T obj) { + } + */ + // --------------------------------------------------- : Private stuff + private: + + /// Information about a file in the package + struct FileInfo { + FileInfo(); + ~FileInfo(); + bool keep; ///< Should this file be kept in the package? (as opposed to deleting it) + String tempName; ///< Name of the temporary file where new contents of this file are placed + wxZipEntry* zipEntry; ///< Entry in the zip file for this file + inline bool wasWritten() ///< Is this file changed, and therefore written to a temporary file? + { return !tempName.empty(); } + }; + + /// Filename of the package + String filename; + /// Last modified time + DateTime modified; + public: + /// Information on files in the package + /** Note: must be public for DECLARE_TYPEOF to work */ + typedef map FileInfos; + private: + /// All files in the package + FileInfos files; + /// Filestream for reading zip files + wxFileInputStream* fileStream; + /// Filestream for reading zip files + wxZipInputStream* zipStream; + + void openDirectory(); + void openSubdir(const String&); + void openZipfile(); + void saveToDirectory(const String&, bool); + void saveToZipfile(const String&, bool); + FileInfos::iterator addFile(const String& file); + static String toStandardName(const String& file); +}; + +// ----------------------------------------------------------------------------- : Packaged + +/// Utility class for data types that are always stored in a package. +/** When the package is opened/saved a file describing the data object is read/written + */ +class Packaged : public Package { + public: + Packaged(); + virtual ~Packaged() {} + + /// Open a package, and read the data + void open(const String& package); + void save(); + void saveAs(const String& package); + + protected: + /// filename of the data file, and extension of the package file + virtual String typeName() = 0; + /// Can be overloaded to do validation after loading + virtual void validate() {} + + DECLARE_REFLECTION_VIRTUAL(); +}; + +// ----------------------------------------------------------------------------- : EOF +#endif diff --git a/src/util/prec.hpp b/src/util/prec.hpp index 58db78e7..c1ed11c6 100644 --- a/src/util/prec.hpp +++ b/src/util/prec.hpp @@ -16,6 +16,7 @@ #ifdef _MSC_VER # pragma warning (disable: 4100) // unreferenced formal parameter +# pragma warning (disable: 4355) // 'this' : used in base member initializer list # pragma warning (disable: 4800) // 'int' : forcing value to bool 'true' or 'false' (performance warning) #endif diff --git a/src/util/smart_ptr.hpp b/src/util/smart_ptr.hpp index f6c7ea78..a238fb53 100644 --- a/src/util/smart_ptr.hpp +++ b/src/util/smart_ptr.hpp @@ -14,7 +14,6 @@ // ----------------------------------------------------------------------------- : Includes -#include #include using namespace boost; @@ -23,8 +22,7 @@ using namespace boost; /// Declares the type TypeP as a shared_ptr #define DECLARE_POINTER_TYPE(Type) \ class Type; \ - typedef shared_ptr Type##P; \ - DECLARE_TYPEOF_COLLECTION(Type##P) + typedef shared_ptr Type##P; // ----------------------------------------------------------------------------- : Creating diff --git a/src/util/window_id.hpp b/src/util/window_id.hpp index 83957b44..f943e6ee 100644 --- a/src/util/window_id.hpp +++ b/src/util/window_id.hpp @@ -103,23 +103,23 @@ enum ChildMenuID { // SymbolSelectEditor toolbar/menu , ID_PART = 2001 -, ID_PART_MERGE = ID_PART + PART_MERGE -, ID_PART_SUBTRACT = ID_PART + PART_SUBTRACT -, ID_PART_INTERSECTION = ID_PART + PART_INTERSECTION -, ID_PART_DIFFERENCE = ID_PART + PART_DIFFERENCE -, ID_PART_OVERLAP = ID_PART + PART_OVERLAP -, ID_PART_BORDER = ID_PART + PART_BORDER +, ID_PART_MERGE = ID_PART + 0//PART_MERGE +, ID_PART_SUBTRACT = ID_PART + 1//PART_SUBTRACT +, ID_PART_INTERSECTION = ID_PART + 2//PART_INTERSECTION +, ID_PART_DIFFERENCE = ID_PART + 3//PART_DIFFERENCE +, ID_PART_OVERLAP = ID_PART + 4//PART_OVERLAP +, ID_PART_BORDER = ID_PART + 5//PART_BORDER , ID_PART_MAX // SymbolPointEditor toolbar/menu , ID_SEGMENT = 2101 -, ID_SEGMENT_LINE = ID_SEGMENT + SEGMENT_LINE -, ID_SEGMENT_CURVE = ID_SEGMENT + SEGMENT_CURVE +, ID_SEGMENT_LINE = ID_SEGMENT + 0//SEGMENT_LINE +, ID_SEGMENT_CURVE = ID_SEGMENT + 1//SEGMENT_CURVE , ID_SEGMENT_MAX , ID_LOCK = 2151 -, ID_LOCK_FREE = ID_LOCK + LOCK_FREE -, ID_LOCK_DIR = ID_LOCK + LOCK_DIR -, ID_LOCK_SIZE = ID_LOCK + LOCK_SIZE +, ID_LOCK_FREE = ID_LOCK + 0//LOCK_FREE +, ID_LOCK_DIR = ID_LOCK + 1//LOCK_DIR +, ID_LOCK_SIZE = ID_LOCK + 2//LOCK_SIZE , ID_LOCK_MAX // SymbolBasicShapeEditor toolbar/menu @@ -145,6 +145,7 @@ enum ControlID { , ID_VIEWER = 6001 , ID_EDITOR , ID_CONTROL +, ID_TAB_BAR , ID_CARD_LIST , ID_PART_LIST , ID_NOTES