]> git.lyx.org Git - lyx.git/blob - src/EmbeddedFiles.cpp
Embedding: store inset pointer instead of ParConstIterator to enable more accurate...
[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 "InsetIterator.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 #include "support/lyxlib.h"
32 #include "support/lstrings.h"
33
34 #include "LyX.h"
35 #include "Session.h"
36
37 #include <sstream>
38 #include <fstream>
39 #include <utility>
40
41 using std::ofstream;
42 using std::endl;
43 using std::vector;
44 using std::string;
45 using std::pair;
46 using std::make_pair;
47 using std::istream;
48 using std::ostream;
49 using std::getline;
50 using std::istringstream;
51
52 namespace lyx {
53
54 namespace fs = boost::filesystem;
55 namespace Alert = frontend::Alert;
56
57 using support::FileName;
58 using support::DocFileName;
59 using support::makeAbsPath;
60 using support::addName;
61 using support::onlyPath;
62 using support::absolutePath;
63 using support::onlyFilename;
64 using support::makeRelPath;
65 using support::changeExtension;
66 using support::bformat;
67 using support::prefixIs;
68 using support::sum;
69 using support::makedir;
70
71
72 EmbeddedFile::EmbeddedFile(string const & file, string const & inzip_name,
73         bool embed, Inset const * inset)
74         : DocFileName(file, true), inzip_name_(inzip_name), embedded_(embed),
75                 valid_(true), inset_list_()
76 {
77         if (inset != NULL)
78                 inset_list_.push_back(inset);
79 }
80
81
82 string EmbeddedFile::embeddedFile(Buffer const * buf) const
83 {
84         return addName(buf->temppath(), inzip_name_);
85 }
86
87
88 void EmbeddedFile::addInset(Inset const * inset)
89 {
90         inset_list_.push_back(inset);
91 }
92
93
94 Inset const * EmbeddedFile::inset(int idx) const
95 {
96         BOOST_ASSERT(idx < refCount());
97         // some embedded file do not have a valid par iterator
98         return inset_list_[idx];
99 }
100
101
102 void EmbeddedFile::saveBookmark(Buffer const * buf, int idx) const
103 {
104         Inset const * ptr = inset(idx);
105         // This might not be the most efficient method ... 
106         for (InsetIterator it = inset_iterator_begin(buf->inset()); it; ++it)
107                 if (&(*it) == ptr) {
108                         // this is basically BufferView::saveBookmark(0)
109                         LyX::ref().session().bookmarks().save(
110                                 FileName(buf->fileName()),
111                                 it.bottom().pit(),
112                                 it.bottom().pos(),
113                                 it.paragraph().id(),
114                                 it.pos(),
115                                 0
116                         );
117                 }
118         // this inset can not be located. There is something wrong that needs
119         // to be fixed.
120         BOOST_ASSERT(true);
121 }
122
123
124 string EmbeddedFile::availableFile(Buffer const * buf) const
125 {
126         if (embedded())
127                 return embeddedFile(buf);
128         else
129                 return absFilename();
130 }
131
132
133 void EmbeddedFile::invalidate()
134 {
135         // Clear inset_list_ because they will be registered again.
136         inset_list_.clear();
137         valid_ = false;
138 }
139
140
141 bool EmbeddedFile::extract(Buffer const * buf) const
142 {
143
144         string ext_file = absFilename();
145         string emb_file = embeddedFile(buf);
146
147         if (!fs::exists(emb_file))
148                 return false;
149
150         // if external file already exists ...
151         if (fs::exists(ext_file)) {
152                 // no need to copy if the files are the same
153                 if (sum(*this) == sum(FileName(emb_file)))
154                         return true;
155                 // otherwise, ask if overwrite
156                 int ret = Alert::prompt(
157                         _("Overwrite external file?"),
158                         bformat(_("External file %1$s already exists, do you want to overwrite it"),
159                                 from_utf8(ext_file)), 1, 1, _("&Overwrite"), _("&Cancel"));
160                 if (ret != 0)
161                         // if the user does not want to overwrite, we still consider it
162                         // a successful operation.
163                         return true;
164         }
165         // copy file
166         try {
167                 // need to make directory?
168                 string path = onlyPath(ext_file);
169                 if (!fs::is_directory(path))
170                         makedir(const_cast<char*>(path.c_str()), 0755);
171                 fs::copy_file(emb_file, ext_file, false);
172                 return true;
173         } catch (fs::filesystem_error const & fe) {
174                 Alert::error(_("Copy file failure"),
175                          bformat(_("Cannot copy file %1$s to %2$s.\n"
176                                    "Please check whether the directory exists and is writeable."),
177                                         from_utf8(emb_file), from_utf8(ext_file)));
178                 LYXERR(Debug::DEBUG) << "Fs error: " << fe.what() << endl;
179         }
180         return false;
181 }
182
183
184 bool EmbeddedFile::updateFromExternalFile(Buffer const * buf) const
185 {
186         string ext_file = absFilename();
187         string emb_file = embeddedFile(buf);
188
189         if (!fs::exists(ext_file))
190                 return false;
191         
192         // if embedded file already exists ...
193         if (fs::exists(emb_file)) {
194                 // no need to copy if the files are the same
195                 if (sum(*this) == sum(FileName(emb_file)))
196                         return true;
197                 // other wise, ask if overwrite
198                 int const ret = Alert::prompt(
199                         _("Update embedded file?"),
200                         bformat(_("Embeddedl file %1$s already exists, do you want to overwrite it"),
201                                 from_utf8(ext_file)), 1, 1, _("&Overwrite"), _("&Cancel"));
202                 if (ret != 0)
203                         // if the user does not want to overwrite, we still consider it
204                         // a successful operation.
205                         return true;
206         }
207         // copy file
208         try {
209                 // need to make directory?
210                 string path = onlyPath(emb_file);
211                 if (!fs::is_directory(path))
212                         makedir(const_cast<char*>(path.c_str()), 0755);
213                 fs::copy_file(ext_file, emb_file, false);
214                 return true;
215         } catch (fs::filesystem_error const & fe) {
216                 Alert::error(_("Copy file failure"),
217                          bformat(_("Cannot copy file %1$s to %2$s.\n"
218                                    "Please check whether the directory exists and is writeable."),
219                                         from_utf8(ext_file), from_utf8(emb_file)));
220                 LYXERR(Debug::DEBUG) << "Fs error: " << fe.what() << endl;
221         }
222         return false;
223 }
224
225
226 bool EmbeddedFiles::enabled() const
227 {
228         return buffer_->params().embedded;
229 }
230
231
232 bool EmbeddedFiles::enable(bool flag)
233 {
234         if (enabled() != flag) {
235                 // if enable, copy all files to temppath()
236                 // if disable, extract all files
237                 if ((flag && !updateFromExternalFile()) || (!flag && !extract()))
238                         return false;
239                 // if operation is successful
240                 buffer_->markDirty();
241                 buffer_->params().embedded = flag;
242         }
243 }
244
245
246 void EmbeddedFiles::registerFile(string const & filename,
247         bool embed, Inset const * inset)
248 {
249         // filename can be relative or absolute, translate to absolute filename
250         string abs_filename = makeAbsPath(filename, buffer_->filePath()).absFilename();
251         // try to find this file from the list
252         EmbeddedFileList::iterator it = file_list_.begin();
253         EmbeddedFileList::iterator it_end = file_list_.end();
254         for (; it != it_end; ++it)
255                 if (it->absFilename() == abs_filename || it->embeddedFile(buffer_) == abs_filename)
256                         break;
257         // find this filename, keep the original embedding status
258         if (it != file_list_.end()) {
259                 it->addInset(inset);
260                 // if the file is embedded, the embedded file should have exist
261                 // check for this to ensure that our logic is correct
262                 if (it->embedded())
263                         BOOST_ASSERT(fs::exists(it->embeddedFile(buffer_)));
264                 it->validate();
265                 return;
266         }
267         // try to be more careful
268         file_list_.push_back(EmbeddedFile(abs_filename, 
269                 getInzipName(abs_filename), embed, inset));
270         // validate if things are OK
271         BOOST_ASSERT(fs::exists(file_list_.back().availableFile(buffer_)));
272 }
273
274
275 void EmbeddedFiles::update()
276 {
277         // invalidate all files, obsolete files will then not be validated by the
278         // following document scan. These files will still be kept though, because
279         // they may be added later and their embedding status will be meaningful
280         // again (thinking of cut/paste of an InsetInclude).
281         EmbeddedFileList::iterator it = file_list_.begin();
282         EmbeddedFileList::iterator it_end = file_list_.end();
283         for (; it != it_end; ++it)
284                 // we do not update items that are manually inserted
285                 if (it->refCount() > 0)
286                         it->invalidate();
287
288         for (InsetIterator it = inset_iterator_begin(buffer_->inset()); it; ++it)
289                 it->registerEmbeddedFiles(*buffer_, *this);
290
291         LYXERR(Debug::FILES) << "Manifest updated: " << endl
292                 << *this
293                 << "End Manifest" << endl;
294 }
295
296
297 bool EmbeddedFiles::write(DocFileName const & filename)
298 {
299         // file in the temporary path has the content
300         string const content = FileName(addName(buffer_->temppath(),
301                 onlyFilename(filename.toFilesystemEncoding()))).toFilesystemEncoding();
302
303         // get a file list and write a manifest file
304         vector<pair<string, string> > filenames;
305         string const manifest = FileName(
306                 addName(buffer_->temppath(), "manifest.txt")).toFilesystemEncoding();
307
308         // write a manifest file
309         ofstream os(manifest.c_str());
310         os << *this;
311         os.close();
312         // prepare list of embedded file
313         EmbeddedFileList::iterator it = file_list_.begin();
314         EmbeddedFileList::iterator it_end = file_list_.end();
315         for (; it != it_end; ++it) {
316                 if (it->valid() && it->embedded()) {
317                         string file = it->availableFile(buffer_);
318                         if (file.empty())
319                                 lyxerr << "File " << it->absFilename() << " does not exist. Skip embedding it. " << endl;
320                         else
321                                 filenames.push_back(make_pair(file, it->inzipName()));
322                 }
323         }
324         // add filename (.lyx) and manifest to filenames
325         filenames.push_back(make_pair(content, onlyFilename(filename.toFilesystemEncoding())));
326         filenames.push_back(make_pair(manifest, "manifest.txt"));
327         // write a zip file with all these files. Write to a temp file first, to
328         // avoid messing up the original file in case something goes terribly wrong.
329         DocFileName zipfile(addName(buffer_->temppath(),
330                 onlyFilename(changeExtension(
331                         filename.toFilesystemEncoding(), ".zip"))));
332
333         ::zipFiles(zipfile.toFilesystemEncoding(), filenames);
334         // copy file back
335         try {
336                 fs::copy_file(zipfile.toFilesystemEncoding(), filename.toFilesystemEncoding(), false);
337         } catch (fs::filesystem_error const & fe) {
338                 Alert::error(_("Save failure"),
339                                  bformat(_("Cannot create file %1$s.\n"
340                                            "Please check whether the directory exists and is writeable."),
341                                          from_utf8(filename.absFilename())));
342                 LYXERR(Debug::DEBUG) << "Fs error: " << fe.what() << endl;
343         }
344         return true;
345 }
346
347
348 EmbeddedFiles::EmbeddedFileList::const_iterator EmbeddedFiles::find(std::string filename) const
349 {
350         EmbeddedFileList::const_iterator it = file_list_.begin();
351         EmbeddedFileList::const_iterator it_end = file_list_.end();
352         for (; it != it_end; ++it)
353                 if (it->absFilename() == filename || it->embeddedFile(buffer_) == filename)     
354                         return it;
355         return file_list_.end();
356 }
357
358
359 bool EmbeddedFiles::extract() const
360 {
361         EmbeddedFileList::const_iterator it = file_list_.begin();
362         EmbeddedFileList::const_iterator it_end = file_list_.end();
363         for (; it != it_end; ++it)
364                 if (it->valid() && it->embedded())
365                         if(!it->extract(buffer_))
366                                 return false;
367         return true;
368 }
369
370
371 bool EmbeddedFiles::updateFromExternalFile() const
372 {
373         EmbeddedFileList::const_iterator it = file_list_.begin();
374         EmbeddedFileList::const_iterator it_end = file_list_.end();
375         for (; it != it_end; ++it)
376                 if (it->valid() && it->embedded())
377                         if (!it->updateFromExternalFile(buffer_))
378                                 return false;
379         return true;
380 }
381
382
383 string const EmbeddedFiles::getInzipName(string const & abs_filename)
384 {
385         // register a new one, using relative file path as inzip_name
386         string inzip_name = to_utf8(makeRelPath(from_utf8(abs_filename),
387                 from_utf8(buffer_->fileName())));
388         // if inzip_name is an absolute path, use filename only to avoid
389         // leaking of filesystem information in inzip_name
390         // The second case covers cases '../path/file' and '.'
391         if (absolutePath(inzip_name) || prefixIs(inzip_name, "."))
392                 inzip_name = onlyFilename(abs_filename);
393         // if this name has been used...
394         // use _1_name, _2_name etc
395         string tmp = inzip_name;
396         EmbeddedFileList::iterator it;
397         EmbeddedFileList::iterator it_end = file_list_.end();
398         bool unique_name = false;
399         size_t i = 0;
400         while (!unique_name) {
401                 unique_name = true;
402                 if (i > 0)
403                         inzip_name = convert<string>(i) + "_" + tmp;
404                 it = file_list_.begin();
405                 for (; it != it_end; ++it)
406                         if (it->inzipName() == inzip_name) {
407                                 unique_name = false;
408                                 ++i;
409                                 break;
410                         }
411         }
412         return inzip_name;
413 }
414
415
416 istream & operator>> (istream & is, EmbeddedFiles & files)
417 {
418         files.clear();
419         string tmp;
420         getline(is, tmp);
421         // get version
422         istringstream itmp(tmp);
423         int version;
424         itmp.ignore(string("# LyX manifest version ").size());
425         itmp >> version;
426
427         if (version != 1) {
428                 lyxerr << "This version of LyX can only read LyX manifest version 1" << endl;
429                 return is;
430         }
431
432         getline(is, tmp);
433         if (tmp != "<manifest>") {
434                 lyxerr << "Invalid manifest file, lacking <manifest>" << endl;
435                 return is;
436         }
437         // manifest file may be messed up, be carefully
438         while (is.good()) {
439                 getline(is, tmp);
440                 if (tmp != "<file>")
441                         break;
442
443                 string fname;
444                 getline(is, fname);
445                 string inzip_name;
446                 getline(is, inzip_name);
447                 getline(is, tmp);
448                 istringstream itmp(tmp);
449                 int embed;
450                 itmp >> embed;
451
452                 getline(is, tmp);
453                 if (tmp != "</file>") {
454                         lyxerr << "Invalid manifest file, lacking </file>" << endl;
455                         break;
456                 }
457
458                 files.registerFile(fname, embed);
459         };
460         // the last line must be </manifest>
461         if (tmp != "</manifest>") {
462                 lyxerr << "Invalid manifest file, lacking </manifest>" << endl;
463                 return is;
464         }
465         return is;
466 }
467
468
469 ostream & operator<< (ostream & os, EmbeddedFiles const & files)
470 {
471         // store a version so that operator >> can read later versions
472         // using version information.
473         os << "# lyx manifest version 1\n";
474         os << "<manifest>\n";
475         EmbeddedFiles::EmbeddedFileList::const_iterator it = files.begin();
476         EmbeddedFiles::EmbeddedFileList::const_iterator it_end = files.end();
477         for (; it != it_end; ++it) {
478                 if (!it->valid())
479                         continue;
480                 // use differnt lines to make reading easier.
481                 os << "<file>\n"
482                         // save the relative path
483                         << to_utf8(makeRelPath(from_utf8(it->absFilename()),
484                                 from_utf8(files.buffer_->filePath()))) << '\n'
485                         << it->inzipName() << '\n'
486                         << it->embedded() << '\n'
487                         << "</file>\n";
488         }
489         os << "</manifest>\n";
490         return os;
491 }
492
493 }