One fix.
[ardour.git] / libs / ardour / file_source.cc
1 /*
2     Copyright (C) 2006-2009 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include <vector>
21
22 #include <sys/time.h>
23 #include <sys/stat.h>
24 #include <unistd.h>
25 #include <fcntl.h>
26 #include <errno.h>
27
28 #include <glib.h>
29 #include <glib/gstdio.h>
30
31 #include "pbd/convert.h"
32 #include "pbd/basename.h"
33 #include "pbd/stl_delete.h"
34 #include "pbd/strsplit.h"
35 #include "pbd/shortpath.h"
36 #include "pbd/enumwriter.h"
37 #include "pbd/file_utils.h"
38
39 #include <glibmm/miscutils.h>
40 #include <glibmm/fileutils.h>
41 #include <glibmm/threads.h>
42
43 #include "ardour/data_type.h"
44 #include "ardour/file_source.h"
45 #include "ardour/session.h"
46 #include "ardour/source.h"
47 #include "ardour/utils.h"
48
49 #include "pbd/i18n.h"
50
51 using namespace std;
52 using namespace ARDOUR;
53 using namespace PBD;
54 using namespace Glib;
55
56 PBD::Signal2<int,std::string,std::vector<std::string> > FileSource::AmbiguousFileName;
57
58 FileSource::FileSource (Session& session, DataType type, const string& path, const string& origin, Source::Flag flag)
59         : Source(session, type, path, flag)
60         , _path (path)
61         , _file_is_new (!origin.empty()) // if origin is left unspecified (empty string) then file must exist
62         , _channel (0)
63         , _origin (origin)
64 {
65         set_within_session_from_path (path);
66 }
67
68 FileSource::FileSource (Session& session, const XMLNode& node, bool /*must_exist*/)
69         : Source (session, node)
70         , _file_is_new (false)
71         , _channel (0)
72 {
73         /* this setting of _path is temporary - we expect derived classes
74            to call ::init() which will actually locate the file
75            and reset _path and _within_session correctly.
76         */
77
78         _path = _name;
79         _within_session = true;
80 }
81
82 FileSource::~FileSource()
83 {
84 }
85
86 void
87 FileSource::existence_check ()
88 {
89         if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
90                 prevent_deletion ();
91         }
92 }
93
94 void
95 FileSource::prevent_deletion ()
96 {
97         if (!(_flags & Destructive)) {
98                 mark_immutable ();
99         } else {
100                 _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
101         }
102 }
103
104 bool
105 FileSource::removable () const
106 {
107         bool r = ((_flags & Removable)
108                   && ((_flags & RemoveAtDestroy) ||
109                       ((_flags & RemovableIfEmpty) && empty())));
110
111         return r;
112 }
113
114 int
115 FileSource::init (const string& pathstr, bool must_exist)
116 {
117         _timeline_position = 0;
118
119         if (Stateful::loading_state_version < 3000) {
120                 if (!find_2X (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
121                         throw MissingSource (pathstr, _type);
122                 }
123         } else {
124                 if (!find (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
125                         throw MissingSource (pathstr, _type);
126                 }
127         }
128
129         set_within_session_from_path (_path);
130
131         _name = Glib::path_get_basename (_path);
132
133         if (must_exist) {
134                 if (!Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
135                         throw MissingSource (pathstr, _type);
136                 }
137         }
138
139         return 0;
140 }
141
142 int
143 FileSource::set_state (const XMLNode& node, int /*version*/)
144 {
145         XMLProperty const * prop;
146
147         if ((prop = node.property (X_("channel"))) != 0) {
148                 _channel = atoi (prop->value());
149         } else {
150                 _channel = 0;
151         }
152
153         if ((prop = node.property (X_("origin"))) != 0) {
154                 _origin = prop->value();
155         }
156
157         return 0;
158 }
159
160 void
161 FileSource::mark_take (const string& id)
162 {
163         if (writable ()) {
164                 _take_id = id;
165         }
166 }
167
168 int
169 FileSource::move_to_trash (const string& trash_dir_name)
170 {
171         if (!within_session() || !writable()) {
172                 return -1;
173         }
174
175         /* don't move the file across filesystems, just stick it in the
176            trash_dir_name directory on whichever filesystem it was already on
177         */
178
179         vector<string> v;
180         v.push_back (Glib::path_get_dirname (Glib::path_get_dirname (_path)));
181         v.push_back (trash_dir_name);
182         v.push_back (Glib::path_get_basename (_path));
183
184         string newpath = Glib::build_filename (v);
185
186         /* the new path already exists, try versioning */
187
188         if (Glib::file_test (newpath.c_str(), Glib::FILE_TEST_EXISTS)) {
189                 char buf[PATH_MAX+1];
190                 int version = 1;
191                 string newpath_v;
192
193                 snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), version);
194                 newpath_v = buf;
195
196                 while (Glib::file_test (newpath_v, Glib::FILE_TEST_EXISTS) && version < 999) {
197                         snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), ++version);
198                         newpath_v = buf;
199                 }
200
201                 if (version == 999) {
202                         PBD::error << string_compose (
203                                         _("there are already 1000 files with names like %1; versioning discontinued"),
204                                         newpath) << endmsg;
205                 } else {
206                         newpath = newpath_v;
207                 }
208         }
209
210         if (::g_rename (_path.c_str(), newpath.c_str()) != 0) {
211                 PBD::error << string_compose (
212                                 _("cannot rename file source from %1 to %2 (%3)"),
213                                 _path, newpath, g_strerror (errno)) << endmsg;
214                 return -1;
215         }
216
217         if (move_dependents_to_trash() != 0) {
218                 /* try to back out */
219                 ::g_rename (newpath.c_str(), _path.c_str());
220                 return -1;
221         }
222
223         _path = newpath;
224
225         /* file can not be removed twice, since the operation is not idempotent */
226         _flags = Flag (_flags & ~(RemoveAtDestroy|Removable|RemovableIfEmpty));
227
228         return 0;
229 }
230
231 /** Find the actual source file based on \a filename.
232  *
233  * If the source is within the session tree, \a path should be a simple filename (no slashes).
234  * If the source is external, \a path should be a full path.
235  * In either case, found_path is set to the complete absolute path of the source file.
236  * \return true if the file was found.
237  */
238 bool
239 FileSource::find (Session& s, DataType type, const string& path, bool must_exist,
240                   bool& isnew, uint16_t& /* chan */, string& found_path)
241 {
242         bool ret = false;
243         string keeppath;
244
245         isnew = false;
246
247         if (!Glib::path_is_absolute (path)) {
248                 vector<string> hits;
249                 string fullpath;
250                 std::vector<std::string> dirs = s.source_search_path (type);
251
252                 if (dirs.size() == 0) {
253                         error << _("FileSource: search path not set") << endmsg;
254                         goto out;
255                 }
256
257                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
258
259                         fullpath = Glib::build_filename (*i, path);
260
261                         if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
262                                 keeppath = fullpath;
263                                 hits.push_back (fullpath);
264                         }
265                 }
266
267                 /* Remove duplicate inodes from the list of ambiguous files, since if there are symlinks
268                    in the session path it is possible to arrive at the same file via more than one path.
269
270                    I suppose this is not necessary on Windows.
271                 */
272
273                 vector<string> de_duped_hits;
274
275                 for (vector<string>::iterator i = hits.begin(); i != hits.end(); ++i) {
276
277                         vector<string>::iterator j = i;
278                         ++j;
279
280                         while (j != hits.end()) {
281                                 if (PBD::equivalent_paths (*i, *j)) {
282                                         /* *i and *j are the same file; break out of the loop early */
283                                         break;
284                                 }
285
286                                 ++j;
287                         }
288
289                         if (j == hits.end ()) {
290                                 de_duped_hits.push_back (*i);
291                         }
292                 }
293
294                 if (de_duped_hits.size() > 1) {
295
296                         /* more than one match: ask the user */
297
298                         int which = FileSource::AmbiguousFileName (path, de_duped_hits).get_value_or (-1);
299
300                         if (which < 0) {
301                                 goto out;
302                         } else {
303                                 keeppath = de_duped_hits[which];
304                         }
305
306                 } else if (de_duped_hits.size() == 0) {
307
308                         /* no match: error */
309
310                         if (must_exist) {
311                                 /* do not generate an error here, leave that to
312                                    whoever deals with the false return value.
313                                 */
314                                 goto out;
315                         } else {
316                                 isnew = true;
317                         }
318                 } else {
319
320                         /* only one match: happy days */
321
322                         keeppath = de_duped_hits[0];
323                 }
324
325        } else {
326                 keeppath = path;
327         }
328
329         /* Current find() is unable to parse relative path names to yet non-existant
330            sources. QuickFix(tm)
331         */
332
333         if (keeppath.empty()) {
334                 if (must_exist) {
335                         error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
336                 } else {
337                         keeppath = path;
338                 }
339         }
340
341         found_path = keeppath;
342         ret = true;
343
344   out:
345         return ret;
346 }
347
348 /** Find the actual source file based on \a filename.
349  *
350  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
351  * If the source is external, \a filename should be a full path.
352  * In either case, found_path is set to the complete absolute path of the source file.
353  * \return true iff the file was found.
354  */
355 bool
356 FileSource::find_2X (Session& s, DataType type, const string& path, bool must_exist,
357                      bool& isnew, uint16_t& chan, string& found_path)
358 {
359         string pathstr = path;
360         string::size_type pos;
361         bool ret = false;
362
363         isnew = false;
364
365         if (!Glib::path_is_absolute (pathstr)) {
366
367                 /* non-absolute pathname: find pathstr in search path */
368
369                 vector<string> dirs = s.source_search_path (type);
370
371                 int cnt;
372                 string fullpath;
373                 string keeppath;
374
375                 if (dirs.size() == 0) {
376                         error << _("FileSource: search path not set") << endmsg;
377                         goto out;
378                 }
379
380                 cnt = 0;
381
382                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
383
384                         fullpath = Glib::build_filename (*i, pathstr);
385
386                         /* i (paul) made a nasty design error by using ':' as a special character in
387                            Ardour 0.99 .. this hack tries to make things sort of work.
388                         */
389
390                         if ((pos = pathstr.find_last_of (':')) != string::npos) {
391
392                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
393
394                                         /* its a real file, no problem */
395
396                                         keeppath = fullpath;
397                                         ++cnt;
398
399                                 } else {
400
401                                         if (must_exist) {
402
403                                                 /* might be an older session using file:channel syntax. see if the version
404                                                    without the :suffix exists
405                                                  */
406
407                                                 string shorter = pathstr.substr (0, pos);
408                                                 fullpath = Glib::build_filename (*i, shorter);
409
410                                                 if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
411                                                         chan = atoi (pathstr.substr (pos+1));
412                                                         pathstr = shorter;
413                                                         keeppath = fullpath;
414                                                         ++cnt;
415                                                 }
416
417                                         } else {
418
419                                                 /* new derived file (e.g. for timefx) being created in a newer session */
420
421                                         }
422                                 }
423
424                         } else {
425
426                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
427                                         keeppath = fullpath;
428                                         ++cnt;
429                                 }
430                         }
431                 }
432
433                 if (cnt > 1) {
434
435                         error << string_compose (
436                                         _("FileSource: \"%1\" is ambiguous when searching\n\t"), pathstr) << endmsg;
437                         goto out;
438
439                 } else if (cnt == 0) {
440
441                         if (must_exist) {
442                                 error << string_compose(
443                                                 _("Filesource: cannot find required file (%1)"), pathstr) << endmsg;
444                                 goto out;
445                         } else {
446                                 isnew = true;
447                         }
448                 }
449
450                 /* Current find() is unable to parse relative path names to yet non-existant
451                    sources. QuickFix(tm) */
452                 if (keeppath == "") {
453                         if (must_exist) {
454                                 error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
455                         } else {
456                                 keeppath = pathstr;
457                         }
458                 }
459
460                 found_path = keeppath;
461
462                 ret = true;
463
464         } else {
465
466                 /* external files and/or very very old style sessions include full paths */
467
468                 /* ugh, handle ':' situation */
469
470                 if ((pos = pathstr.find_last_of (':')) != string::npos) {
471
472                         string shorter = pathstr.substr (0, pos);
473
474                         if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
475                                 chan = atoi (pathstr.substr (pos+1));
476                                 pathstr = shorter;
477                         }
478                 }
479
480                 found_path = pathstr;
481
482                 if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
483
484                         /* file does not exist or we cannot read it */
485
486                         if (must_exist) {
487                                 error << string_compose(
488                                                 _("Filesource: cannot find required file (%1): %2"),
489                                                 path, g_strerror (errno)) << endmsg;
490                                 goto out;
491                         }
492
493 #ifndef PLATFORM_WINDOWS
494                         if (errno != ENOENT) {
495                                 error << string_compose(
496                                                 _("Filesource: cannot check for existing file (%1): %2"),
497                                                 path, g_strerror (errno)) << endmsg;
498                                 goto out;
499                         }
500 #endif
501                         /* a new file */
502                         isnew = true;
503                         ret = true;
504
505                 } else {
506
507                         /* already exists */
508                         ret = true;
509                 }
510         }
511
512 out:
513         return ret;
514 }
515
516 void
517 FileSource::mark_immutable ()
518 {
519         /* destructive sources stay writable, and their other flags don't change.  */
520         if (!(_flags & Destructive)) {
521                 _flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
522         }
523 }
524
525 void
526 FileSource::mark_immutable_except_write ()
527 {
528         /* destructive sources stay writable, and their other flags don't change.  */
529         if (!(_flags & Destructive)) {
530                 _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
531         }
532 }
533
534 void
535 FileSource::mark_nonremovable ()
536 {
537         _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
538 }
539
540 void
541 FileSource::set_within_session_from_path (const std::string& path)
542 {
543         _within_session = _session.path_is_within_session (path);
544 }
545
546 void
547 FileSource::set_path (const std::string& newpath)
548 {
549         close ();
550         _path = newpath;
551         set_within_session_from_path (newpath);
552         if (_within_session) {
553                 _origin = Glib::path_get_basename (newpath);
554         } else {
555                 _origin = newpath;
556         }
557 }
558
559
560 void
561 FileSource::replace_file (const std::string& newpath)
562 {
563         close ();
564         _path = newpath;
565         _name = Glib::path_get_basename (newpath);
566 }
567
568 void
569 FileSource::inc_use_count ()
570 {
571         Source::inc_use_count ();
572 }
573
574 bool
575 FileSource::is_stub () const
576 {
577         if (!empty()) {
578                 return false;
579         }
580
581         if (!removable()) {
582                 return false;
583         }
584
585         if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
586                 return false;
587         }
588
589         return true;
590 }
591
592 int
593 FileSource::rename (const string& newpath)
594 {
595         Glib::Threads::Mutex::Lock lm (_lock);
596         string oldpath = _path;
597
598         // Test whether newpath exists, if yes notify the user but continue.
599         if (Glib::file_test (newpath, Glib::FILE_TEST_EXISTS)) {
600                 error << string_compose (_("Programming error! %1 tried to rename a file over another file! It's safe to continue working, but please report this to the developers."), PROGRAM_NAME) << endmsg;
601                 return -1;
602         }
603
604         if (Glib::file_test (oldpath.c_str(), Glib::FILE_TEST_EXISTS)) {
605                 /* rename only needed if file exists on disk */
606                 if (::g_rename (oldpath.c_str(), newpath.c_str()) != 0) {
607                         error << string_compose (_("cannot rename file %1 to %2 (%3)"), oldpath, newpath, g_strerror(errno)) << endmsg;
608                         return -1;
609                 }
610         }
611
612         _name = Glib::path_get_basename (newpath);
613         _path = newpath;
614
615         return 0;
616 }
617
618