// -*- C++ -*- /** * \file EmbeddedFileList.cpp * This file is part of LyX, the document processor. * Licence details can be found in the file COPYING. * * \author Bo Peng * * Full author contact details are available in file CREDITS. * */ #include #include "EmbeddedFiles.h" #include "Buffer.h" #include "BufferParams.h" #include "ErrorList.h" #include "Format.h" #include "InsetIterator.h" #include "Lexer.h" #include "LyX.h" #include "Paragraph.h" #include "Session.h" #include "frontends/alert.h" #include "support/debug.h" #include "support/filetools.h" #include "support/gettext.h" #include "support/convert.h" #include "support/lstrings.h" #include "support/ExceptionMessage.h" #include "support/FileZipListDir.h" #include #include #include using namespace std; using namespace lyx::support; namespace lyx { namespace Alert = frontend::Alert; EmbeddedFile::EmbeddedFile(string const & file, std::string const & buffer_path) : DocFileName("", false), inzip_name_(""), embedded_(false), inset_list_(), temp_path_("") { set(file, buffer_path); } void EmbeddedFile::set(std::string const & filename, std::string const & buffer_path) { DocFileName::set(filename, buffer_path); if (filename.empty()) return; if (!buffer_path.empty()) inzip_name_ = calcInzipName(buffer_path); } void EmbeddedFile::setInzipName(std::string const & name) { if (name.empty() || name == inzip_name_) return; // an enabled EmbeededFile should have this problem handled BOOST_ASSERT(!enabled()); // file will be synced when it is enabled inzip_name_ = name; } string EmbeddedFile::embeddedFile() const { BOOST_ASSERT(enabled()); return temp_path_ + inzip_name_; } FileName EmbeddedFile::availableFile() const { if (enabled() && embedded()) return FileName(embeddedFile()); else return *this; } string EmbeddedFile::latexFilename(std::string const & buffer_path) const { return (enabled() && embedded()) ? inzip_name_ : relFilename(buffer_path); } void EmbeddedFile::addInset(Inset const * inset) { if (inset != NULL) inset_list_.push_back(inset); } void EmbeddedFile::setEmbed(bool embed) { embedded_ = embed; } void EmbeddedFile::enable(bool flag, Buffer const * buf, bool updateFile) { // This function will be called when // 1. through EmbeddedFiles::enable() when a file is read. Files // should be in place so no updateFromExternalFile or extract() // should be called. (updateFile should be false in this case). // 2. through menu item enable/disable. updateFile should be true. // 3. A single embedded file is added or modified. updateFile // can be true or false. LYXERR(Debug::FILES, (flag ? "Enable" : "Disable") << " " << absFilename() << (updateFile ? " (update file)." : " (no update).")); if (flag) { temp_path_ = buf->temppath(); if (!suffixIs(temp_path_, '/')) temp_path_ += '/'; if (embedded()) { if (updateFile) updateFromExternalFile(); } } else { if (embedded() && updateFile) extract(); temp_path_ = ""; } } bool EmbeddedFile::extract() const { BOOST_ASSERT(enabled()); string ext_file = absFilename(); string emb_file = embeddedFile(); FileName emb(emb_file); FileName ext(ext_file); if (!emb.exists()) { if (ext.exists()) return true; else throw ExceptionMessage(ErrorException, _("Failed to extract file"), bformat(_("Cannot extract file '%1$s'.\n" "Source file %2$s does not exist"), from_utf8(outputFilename()), from_utf8(emb_file))); } // if external file already exists ... if (ext.exists()) { // no need to copy if the files are the same if (checksum() == FileName(emb_file).checksum()) return true; // otherwise, ask if overwrite int ret = Alert::prompt( _("Overwrite external file?"), bformat(_("External file %1$s already exists, do you want to overwrite it"), from_utf8(ext_file)), 1, 1, _("&Overwrite"), _("&Cancel")); if (ret != 0) // if the user does not want to overwrite, we still consider it // a successful operation. return true; } // copy file // need to make directory? FileName path = ext.onlyPath(); if (!path.createPath()) { throw ExceptionMessage(ErrorException, _("Copy file failure"), bformat(_("Cannot create file path '%1$s'.\n" "Please check whether the path is writeable."), from_utf8(path.absFilename()))); return false; } if (emb.copyTo(ext)) { LYXERR(Debug::FILES, "Extract file " << emb_file << " to " << ext_file << endl); return true; } throw ExceptionMessage(ErrorException, _("Copy file failure"), bformat(_("Cannot copy file %1$s to %2$s.\n" "Please check whether the directory exists and is writeable."), from_utf8(emb_file), from_utf8(ext_file))); return false; } bool EmbeddedFile::updateFromExternalFile() const { BOOST_ASSERT(enabled()); string ext_file = absFilename(); string emb_file = embeddedFile(); FileName emb(emb_file); FileName ext(ext_file); if (!ext.exists()) { // no need to update if (emb.exists()) return true; // no external and internal file throw ExceptionMessage(ErrorException, _("Failed to embed file"), bformat(_("Failed to embed file %1$s.\n" "Please check whether this file exists and is readable."), from_utf8(ext_file))); } // if embedded file already exists ... if (emb.exists()) { // no need to copy if the files are the same if (checksum() == FileName(emb_file).checksum()) return true; // other wise, ask if overwrite int const ret = Alert::prompt( _("Update embedded file?"), bformat(_("Embedded file %1$s already exists, do you want to overwrite it"), from_utf8(ext_file)), 1, 1, _("&Overwrite"), _("&Cancel")); if (ret != 0) // if the user does not want to overwrite, we still consider it // a successful operation. return true; } // copy file // need to make directory? FileName path = emb.onlyPath(); if (!path.isDirectory()) path.createPath(); if (ext.copyTo(emb)) return true; throw ExceptionMessage(ErrorException, _("Copy file failure"), bformat(_("Cannot copy file %1$s to %2$s.\n" "Please check whether the directory exists and is writeable."), from_utf8(ext_file), from_utf8(emb_file))); //LYXERR(Debug::DEBUG, "Fs error: " << fe.what()); return false; } void EmbeddedFile::updateInsets() const { vector::const_iterator it = inset_list_.begin(); vector::const_iterator it_end = inset_list_.end(); for (; it != it_end; ++it) const_cast(*it)->updateEmbeddedFile(*this); } bool EmbeddedFile::isReadableFile() const { return availableFile().isReadableFile(); } unsigned long EmbeddedFile::checksum() const { return availableFile().checksum(); } /** Under the lyx temp directory, content.lyx and its embedded files are usually saved as $temp/$embDirName/file.lyx $temp/$embDirName/figure1.png for ./figure1.png) $temp/$embDirName/sub/figure2.png for ./sub/figure2.png) This works fine for embedded files that are in the current or deeper directory of the document directory, but not for files such as ../figures/figure.png. A unique name $upDirName is chosen to represent .. in such filenames so that 'up' directories can be stored 'down' the directory tree: $temp/$embDirName/$upDirName/figures/figure.png for ../figures/figure.png $temp/$embDirName/$upDirName/$upDirName/figure.png for ../../figure.png This name has to be fixed because it is used in lyx bundled .zip file. Using a similar trick, we use $absDirName for absolute path so that an absolute filename can be saved as $temp/$embDirName/$absDirName/a/absolute/path for /a/absolute/path FIXME: embDirName is set to . so that embedded layout and class files can be used directly. However, putting all embedded files directly under the temp directory may lead to file conflicts. For example, if a user embeds a file blah.log in blah.lyx, it will be replaced when 'latex blah.tex' is called. */ const std::string embDirName = "."; const std::string upDirName = "LyX.Embed.Dir.Up"; const std::string absDirName = "LyX.Embed.Dir.Abs"; const std::string driveName = "LyX.Embed.Drive"; const std::string spaceName = "LyX.Embed.Space"; std::string EmbeddedFile::calcInzipName(std::string const & buffer_path) { string inzipName = to_utf8(makeRelPath(from_utf8(absFilename()), from_utf8(buffer_path))); if (FileName(inzipName).isAbsolute()) inzipName = absDirName + '/' + inzipName; // replace .. by upDirName if (prefixIs(inzipName, ".")) inzipName = subst(inzipName, "..", upDirName); // replace special characters by their value inzipName = subst(inzipName, ":", driveName); inzipName = subst(inzipName, " ", spaceName); // to avoid name conflict between $docu_path/file and $temp_path/file // embedded files are in a subdirectory of $temp_path. inzipName = embDirName + '/' + inzipName; return inzipName; } void EmbeddedFile::syncInzipFile(std::string const & buffer_path) { BOOST_ASSERT(enabled()); string old_emb_file = temp_path_ + '/' + inzip_name_; FileName old_emb(old_emb_file); if (!old_emb.exists()) throw ExceptionMessage(ErrorException, _("Failed to open file"), bformat(_("Embedded file %1$s does not exist. Did you tamper lyx temporary directory?"), old_emb.displayName())); string new_inzip_name = calcInzipName(buffer_path); if (new_inzip_name == inzip_name_) return; LYXERR(Debug::FILES, " OLD ZIP " << old_emb_file << " NEW ZIP " << calcInzipName(buffer_path)); string new_emb_file = temp_path_ + '/' + new_inzip_name; FileName new_emb(new_emb_file); // need to make directory? FileName path = new_emb.onlyPath(); if (!path.createPath()) { throw ExceptionMessage(ErrorException, _("Sync file failure"), bformat(_("Cannot create file path '%1$s'.\n" "Please check whether the path is writeable."), from_utf8(path.absFilename()))); return; } if (old_emb.copyTo(new_emb)) { LYXERR(Debug::FILES, "Sync inzip file from " << inzip_name_ << " to " << new_inzip_name); inzip_name_ = new_inzip_name; return; } throw ExceptionMessage(ErrorException, _("Sync file failure"), bformat(_("Cannot copy file %1$s to %2$s.\n" "Please check whether the directory exists and is writeable."), from_utf8(old_emb_file), from_utf8(new_emb_file))); } bool operator==(EmbeddedFile const & lhs, EmbeddedFile const & rhs) { return lhs.absFilename() == rhs.absFilename() && lhs.saveAbsPath() == rhs.saveAbsPath() && lhs.embedded() == rhs.embedded(); } bool operator!=(EmbeddedFile const & lhs, EmbeddedFile const & rhs) { return !(lhs == rhs); } void EmbeddedFileList::enable(bool flag, Buffer & buffer, bool updateFile) { // update embedded file list update(buffer); int count_embedded = 0; int count_external = 0; iterator it = begin(); iterator it_end = end(); // an exception may be thrown for (; it != it_end; ++it) { it->enable(flag, &buffer, updateFile); if (it->embedded()) ++count_embedded; else ++count_external; } // if operation is successful (no exception is thrown) buffer.markDirty(); buffer.params().embedded = flag; // if the operation is successful, update insets for (it = begin(); it != it_end; ++it) it->updateInsets(); if (!updateFile) return; // show result if (flag) { docstring const msg = bformat(_("%1$d external files are ignored.\n" "%2$d embeddable files are embedded.\n"), count_external, count_embedded); Alert::information(_("Packing all files"), msg); } else { docstring const msg = bformat(_("%1$d external files are ignored.\n" "%2$d embedded files are extracted.\n"), count_external, count_embedded); Alert::information(_("Unpacking all files"), msg); } } void EmbeddedFileList::registerFile(EmbeddedFile const & file, Inset const * inset, Buffer const & buffer) { BOOST_ASSERT(!buffer.embedded() || file.enabled()); // try to find this file from the list std::vector::iterator it = begin(); std::vector::iterator it_end = end(); for (; it != it_end; ++it) if (it->absFilename() == file.absFilename()) { if (it->embedded() != file.embedded()) { Alert::error(_("Wrong embedding status."), bformat(_("File %1$s is included in more than one insets, " "but with different embedding status. Assuming embedding status."), from_utf8(it->outputFilename()))); it->setEmbed(true); // update the inset with this embedding status. const_cast(inset)->updateEmbeddedFile(*it); } it->addInset(inset); return; } // file.clearInsets(); push_back(file); back().addInset(inset); } void EmbeddedFileList::validate(Buffer const & buffer) { clear(); for (InsetIterator it = inset_iterator_begin(buffer.inset()); it; ++it) it->registerEmbeddedFiles(*this); iterator it = begin(); iterator it_end = end(); for (; it != it_end; ++it) { if (buffer.embedded() && it->embedded()) // An exception will be raised if inzip file does not exist it->syncInzipFile(buffer.filePath()); else // inzipName may be OS dependent it->setInzipName(it->calcInzipName(buffer.filePath())); } for (it = begin(); it != it_end; ++it) it->updateInsets(); if (!buffer.embedded()) return; // check if extra embedded files exist vector extra = buffer.params().extraEmbeddedFiles(); vector::iterator e_it = extra.begin(); vector::iterator e_end = extra.end(); for (; e_it != e_end; ++e_it) { EmbeddedFile file = EmbeddedFile(*e_it, buffer.filePath()); // do not update from external file file.enable(true, &buffer, false); // but we do need to check file existence. if (!FileName(file.embeddedFile()).exists()) throw ExceptionMessage(ErrorException, _("Failed to open file"), bformat(_("Embedded file %1$s does not exist. Did you tamper lyx temporary directory?"), file.displayName())); } } void EmbeddedFileList::update(Buffer const & buffer) { clear(); for (InsetIterator it = inset_iterator_begin(buffer.inset()); it; ++it) it->registerEmbeddedFiles(*this); // add extra embedded files vector extra = buffer.params().extraEmbeddedFiles(); vector::iterator it = extra.begin(); vector::iterator it_end = extra.end(); for (; it != it_end; ++it) { EmbeddedFile file = EmbeddedFile(*it, buffer.filePath()); file.setEmbed(true); file.enable(buffer.embedded(), &buffer, false); insert(end(), file); } } bool EmbeddedFileList::writeFile(DocFileName const & filename, Buffer const & buffer) { // file in the temporary path has the content string const content = FileName(addName(buffer.temppath(), "content.lyx")).toFilesystemEncoding(); vector > filenames; // add content.lyx to filenames filenames.push_back(make_pair(content, "content.lyx")); // prepare list of embedded file update(buffer); // iterator it = begin(); iterator it_end = end(); for (; it != it_end; ++it) { if (it->embedded()) { string file = it->embeddedFile(); if (!FileName(file).exists()) throw ExceptionMessage(ErrorException, _("Failed to write file"), bformat(_("Embedded file %1$s does not exist. Did you tamper lyx temporary directory?"), it->displayName())); filenames.push_back(make_pair(file, it->inzipName())); LYXERR(Debug::FILES, "Writing file " << it->outputFilename() << " as " << it->inzipName() << endl); } } // write a zip file with all these files. Write to a temp file first, to // avoid messing up the original file in case something goes terribly wrong. DocFileName zipfile(addName(buffer.temppath(), onlyFilename(changeExtension( filename.toFilesystemEncoding(), ".zip")))); ::zipFiles(zipfile.toFilesystemEncoding(), filenames); // copy file back if (!zipfile.copyTo(filename)) { Alert::error(_("Save failure"), bformat(_("Cannot create file %1$s.\n" "Please check whether the directory exists and is writeable."), from_utf8(filename.absFilename()))); //LYXERR(Debug::DEBUG, "Fs error: " << fe.what()); } return true; } } // namespace lyx