]> git.lyx.org Git - lyx.git/blob - src/EmbeddedFiles.cpp
Embedding: add 'addFile' to embed arbitrary file, fix a few bugs along the way
[lyx.git] / src / EmbeddedFiles.cpp
1 // -*- C++ -*-
2 /**
3  * \file EmbeddedFiles.cpp
4  * This file is part of LyX, the document processor.
5  * Licence details can be found in the file COPYING.
6  *
7  * \author Bo Peng
8  *
9  * Full author contact details are available in file CREDITS.
10  *
11  */
12
13 #include <config.h>
14
15 #include "EmbeddedFiles.h"
16 #include "Buffer.h"
17 #include "BufferParams.h"
18 #include "Paragraph.h"
19 #include "ParIterator.h"
20 #include "debug.h"
21 #include "gettext.h"
22 #include "Format.h"
23
24 #include "frontends/alert.h"
25
26 #include <boost/filesystem/operations.hpp>
27
28 #include "support/filetools.h"
29 #include "support/fs_extras.h"
30 #include "support/convert.h"
31
32 #include <sstream>
33 #include <fstream>
34 #include <utility>
35
36 using std::ofstream;
37 using std::endl;
38 using std::vector;
39 using std::string;
40 using std::pair;
41 using std::make_pair;
42 using std::istream;
43 using std::ostream;
44 using std::getline;
45 using std::istringstream;
46
47 namespace lyx {
48
49 namespace fs = boost::filesystem;
50 namespace Alert = frontend::Alert;
51
52 using support::FileName;
53 using support::DocFileName;
54 using support::makeAbsPath;
55 using support::addName;
56 using support::onlyPath;
57 using support::absolutePath;
58 using support::onlyFilename;
59 using support::makeRelPath;
60 using support::changeExtension;
61 using support::bformat;
62 using support::zipFiles;
63 using support::prefixIs;
64
65
66 EmbeddedFile::EmbeddedFile(string const & file, string const & inzip_name,
67         STATUS status, ParConstIterator const & pit)
68         : DocFileName(file, true), inzip_name_(inzip_name), status_(status),
69                 valid_(true), par_it_(pit)
70 {}
71
72
73 string EmbeddedFile::embeddedFile(Buffer const * buf) const
74 {
75         return addName(buf->temppath(), inzip_name_);
76 }
77
78
79 int const EmbeddedFile::parID() const
80 {
81         // some embedded file do not have a valid par iterator
82         return par_it_ == ParConstIterator() ? 0 : par_it_->id();
83 }
84
85
86 void EmbeddedFile::setParIter(ParConstIterator const & pit)
87 {
88         par_it_ = pit;
89 }
90
91
92 bool EmbeddedFiles::enabled() const
93 {
94         return buffer_->params().embedded;
95 }
96
97
98 void EmbeddedFiles::enable(bool flag)
99 {
100         // FIXME: there are much more to do here. 
101         // If we enable embedding, it is maybe a good idea to copy embedded files
102         // to temppath()
103         // if we disable embedding, embedded files need to be copied to their
104         // original positions.
105         if (enabled() != flag) {
106                 // file will be changed
107                 buffer_->markDirty();
108                 buffer_->params().embedded = flag;
109         }
110 }
111
112
113 void EmbeddedFiles::registerFile(string const & filename,
114         EmbeddedFile::STATUS status, ParConstIterator const & pit)
115 {
116         string abs_filename = makeAbsPath(filename, buffer_->filePath()).absFilename();
117         // try to find this file from the list
118         EmbeddedFileList::iterator it = file_list_.begin();
119         EmbeddedFileList::iterator it_end = file_list_.end();
120         for (; it != it_end; ++it)
121                 if (it->absFilename() == abs_filename)
122                         break;
123         // find this filename
124         if (it != file_list_.end()) {
125                 it->setParIter(pit);
126                 it->setStatus(status);
127                 it->validate();
128                 return;
129         }
130         file_list_.push_back(EmbeddedFile(abs_filename, 
131                 getInzipName(abs_filename), status, pit));
132 }
133
134
135 void EmbeddedFiles::update()
136 {
137         // invalidate all files, obsolete files will then not be validated by the
138         // following document scan. These files will still be kept though, because
139         // they may be added later and their embedding status will be meaningful
140         // again (thinking of cut/paste of an InsetInclude).
141         EmbeddedFileList::iterator it = file_list_.begin();
142         EmbeddedFileList::iterator it_end = file_list_.end();
143         for (; it != it_end; ++it)
144                 // if the status of a file is EMBEDDED, it will be there
145                 // even if it is not referred by a document.
146                 if (it->status() != EmbeddedFile::EMBEDDED)
147                         it->invalidate();
148
149         ParIterator pit = buffer_->par_iterator_begin();
150         ParIterator pit_end = buffer_->par_iterator_end();
151         for (; pit != pit_end; ++pit) {
152                 // For each paragraph, traverse its insets and register embedded files
153                 InsetList::const_iterator iit = pit->insetlist.begin();
154                 InsetList::const_iterator iit_end = pit->insetlist.end();
155                 for (; iit != iit_end; ++iit) {
156                         Inset & inset = *iit->inset;
157                         inset.registerEmbeddedFiles(*buffer_, *this, pit);
158                 }
159         }
160         LYXERR(Debug::FILES) << "Manifest updated: " << endl
161                 << *this
162                 << "End Manifest" << endl;
163 }
164
165
166 bool EmbeddedFiles::write(DocFileName const & filename)
167 {
168         // file in the temporary path has the content
169         string const content = FileName(addName(buffer_->temppath(),
170                 onlyFilename(filename.toFilesystemEncoding()))).toFilesystemEncoding();
171
172         // get a file list and write a manifest file
173         vector<pair<string, string> > filenames;
174         string const manifest = FileName(
175                 addName(buffer_->temppath(), "manifest.txt")).toFilesystemEncoding();
176
177         // write a manifest file
178         ofstream os(manifest.c_str());
179         os << *this;
180         os.close();
181         // prepare list of embedded file
182         EmbeddedFileList::iterator it = file_list_.begin();
183         EmbeddedFileList::iterator it_end = file_list_.end();
184         for (; it != it_end; ++it) {
185                 if (it->valid() && it->embedded()) {
186                         string ext_file = it->absFilename();
187                         string emb_file = it->embeddedFile(buffer_);
188                         if (it->status() == EmbeddedFile::AUTO) {
189                                 // use external file first
190                                 if (fs::exists(ext_file))
191                                         filenames.push_back(make_pair(ext_file, it->inzipName()));
192                                 else if (fs::exists(emb_file))
193                                         filenames.push_back(make_pair(emb_file, it->inzipName()));
194                                 else
195                                         lyxerr << "File " << ext_file << " does not exist. Skip embedding it. " << endl;
196                         } else if (it->status() == EmbeddedFile::EMBEDDED) {
197                                 // use embedded file first
198                                 if (fs::exists(emb_file))
199                                         filenames.push_back(make_pair(emb_file, it->inzipName()));
200                                 else if (fs::exists(ext_file))
201                                         filenames.push_back(make_pair(ext_file, it->inzipName()));
202                                 else
203                                         lyxerr << "File " << ext_file << " does not exist. Skip embedding it. " << endl;
204                         }
205                 }
206         }
207         // add filename (.lyx) and manifest to filenames
208         filenames.push_back(make_pair(content, onlyFilename(filename.toFilesystemEncoding())));
209         filenames.push_back(make_pair(manifest, "manifest.txt"));
210         // write a zip file with all these files. Write to a temp file first, to
211         // avoid messing up the original file in case something goes terribly wrong.
212         DocFileName zipfile(addName(buffer_->temppath(),
213                 onlyFilename(changeExtension(
214                         filename.toFilesystemEncoding(), ".zip"))));
215
216         zipFiles(zipfile, filenames);
217         // copy file back
218         try {
219                 fs::copy_file(zipfile.toFilesystemEncoding(), filename.toFilesystemEncoding(), false);
220         } catch (fs::filesystem_error const & fe) {
221                 Alert::error(_("Save failure"),
222                                  bformat(_("Cannot create file %1$s.\n"
223                                            "Please check whether the directory exists and is writeable."),
224                                          from_utf8(filename.absFilename())));
225                 LYXERR(Debug::DEBUG) << "Fs error: " << fe.what() << endl;
226         }
227         return true;
228 }
229
230
231 string const EmbeddedFiles::getInzipName(string const & abs_filename)
232 {
233         // register a new one, using relative file path as inzip_name
234         string inzip_name = to_utf8(makeRelPath(from_utf8(abs_filename),
235                 from_utf8(buffer_->fileName())));
236         // if inzip_name is an absolute path, use filename only to avoid
237         // leaking of filesystem information in inzip_name
238         if (absolutePath(inzip_name) || prefixIs(inzip_name, ".."))
239                 inzip_name = onlyFilename(inzip_name);
240         // if this name has been used...
241         // use _1_name, _2_name etc
242         string tmp = inzip_name;
243         EmbeddedFileList::iterator it;
244         EmbeddedFileList::iterator it_end = file_list_.end();
245         bool unique_name = false;
246         while (!unique_name) {
247                 unique_name = true;
248                 size_t i = 0;
249                 if (i > 0)
250                         inzip_name = convert<string>(i) + "_" + tmp;
251                 it = file_list_.begin();
252                 for (; it != it_end; ++it)
253                         if (it->inzipName() == inzip_name) {
254                                 unique_name = false;
255                                 ++i;
256                                 break;
257                         }
258         }
259         return inzip_name;
260 }
261
262
263 istream & operator>> (istream & is, EmbeddedFiles & files)
264 {
265         files.clear();
266         string tmp;
267         getline(is, tmp);
268         // get version
269         istringstream itmp(tmp);
270         int version;
271         itmp.ignore(string("# LyX manifest version ").size());
272         itmp >> version;
273
274         if (version != 1) {
275                 lyxerr << "This version of LyX can only read LyX manifest version 1" << endl;
276                 return is;
277         }
278
279         getline(is, tmp);
280         if (tmp != "<manifest>") {
281                 lyxerr << "Invalid manifest file, lacking <manifest>" << endl;
282                 return is;
283         }
284         // manifest file may be messed up, be carefully
285         while (is.good()) {
286                 getline(is, tmp);
287                 if (tmp != "<file>")
288                         break;
289
290                 string fname;
291                 getline(is, fname);
292                 string inzip_name;
293                 getline(is, inzip_name);
294                 getline(is, tmp);
295                 istringstream itmp(tmp);
296                 int status;
297                 itmp >> status;
298
299                 getline(is, tmp);
300                 if (tmp != "</file>") {
301                         lyxerr << "Invalid manifest file, lacking </file>" << endl;
302                         break;
303                 }
304
305                 files.registerFile(fname, static_cast<EmbeddedFile::STATUS>(status));
306         };
307         // the last line must be </manifest>
308         if (tmp != "</manifest>") {
309                 lyxerr << "Invalid manifest file, lacking </manifest>" << endl;
310                 return is;
311         }
312         return is;
313 }
314
315
316 ostream & operator<< (ostream & os, EmbeddedFiles const & files)
317 {
318         // store a version so that operator >> can read later versions
319         // using version information.
320         os << "# lyx manifest version 1\n";
321         os << "<manifest>\n";
322         EmbeddedFiles::EmbeddedFileList::const_iterator it = files.begin();
323         EmbeddedFiles::EmbeddedFileList::const_iterator it_end = files.end();
324         for (; it != it_end; ++it) {
325                 if (!it->valid())
326                         continue;
327                 // use differnt lines to make reading easier.
328                 os << "<file>\n"
329                         // save the relative path
330                         << to_utf8(makeRelPath(from_utf8(it->absFilename()),
331                                 from_utf8(files.buffer_->filePath()))) << '\n'
332                         << it->inzipName() << '\n'
333                         << it->status() << '\n'
334                         << "</file>\n";
335         }
336         os << "</manifest>\n";
337         return os;
338 }
339
340 }