try not thinning when loading old-school automation lists
[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 <stdio.h> // for rename(), sigh
25 #include <unistd.h>
26 #include <fcntl.h>
27 #include <errno.h>
28
29 #include "pbd/convert.h"
30 #include "pbd/basename.h"
31 #include "pbd/mountpoint.h"
32 #include "pbd/stl_delete.h"
33 #include "pbd/strsplit.h"
34 #include "pbd/shortpath.h"
35 #include "pbd/enumwriter.h"
36 #include "pbd/filesystem.h"
37
38 #include <glibmm/miscutils.h>
39 #include <glibmm/fileutils.h>
40 #include <glibmm/thread.h>
41
42 #include "ardour/data_type.h"
43 #include "ardour/file_source.h"
44 #include "ardour/session.h"
45 #include "ardour/source.h"
46 #include "ardour/utils.h"
47
48 #include "i18n.h"
49
50 using namespace std;
51 using namespace ARDOUR;
52 using namespace PBD;
53 using namespace Glib;
54
55 PBD::Signal3<int,std::string,std::string,std::vector<std::string> > FileSource::AmbiguousFileName;
56
57 FileSource::FileSource (Session& session, DataType type, const string& path, const string& origin, Source::Flag flag)
58         : Source(session, type, path, flag)
59         , _path(path)
60         , _file_is_new(true)
61         , _channel (0)
62         , _origin (origin)
63         , _open (false)
64 {
65         set_within_session_from_path (path);
66
67         prevent_deletion ();
68 }
69
70 FileSource::FileSource (Session& session, const XMLNode& node, bool /*must_exist*/)
71         : Source (session, node)
72         , _file_is_new (false)
73 {
74         /* this setting of _path is temporary - we expect derived classes
75            to call ::init() which will actually locate the file
76            and reset _path and _within_session correctly.
77         */
78
79         _path = _name;
80         _within_session = true;
81
82         prevent_deletion ();
83 }
84
85 void
86 FileSource::prevent_deletion ()
87 {
88         /* if this file already exists, it cannot be removed, ever
89          */
90
91         if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
92                 if (!(_flags & Destructive)) {
93                         mark_immutable ();
94                 } else {
95                         _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
96                 }
97         }
98 }
99
100 bool
101 FileSource::removable () const
102 {
103         bool r = ((_flags & Removable)
104                   && ((_flags & RemoveAtDestroy) ||
105                       ((_flags & RemovableIfEmpty) && empty() == 0)));
106
107         return r;
108 }
109
110 int
111 FileSource::init (const string& pathstr, bool must_exist)
112 {
113         _timeline_position = 0;
114
115         if (Stateful::loading_state_version < 3000) {
116                 if (!find_2X (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
117                         throw MissingSource (pathstr, _type);
118                 }
119         } else {
120                 if (!find (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
121                         throw MissingSource (pathstr, _type);
122                 }
123         }
124
125         set_within_session_from_path (_path);
126         
127         _name = Glib::path_get_basename (_path);
128
129         if (_file_is_new && must_exist) {
130                 return -1;
131         }
132
133         return 0;
134 }
135
136 int
137 FileSource::set_state (const XMLNode& node, int /*version*/)
138 {
139         const XMLProperty* prop;
140
141         if ((prop = node.property (X_("channel"))) != 0) {
142                 _channel = atoi (prop->value());
143         } else {
144                 _channel = 0;
145         }
146
147         if ((prop = node.property (X_("origin"))) != 0) {
148                 _origin = prop->value();
149         }
150
151         return 0;
152 }
153
154 void
155 FileSource::mark_take (const string& id)
156 {
157         if (writable ()) {
158                 _take_id = id;
159         }
160 }
161
162 int
163 FileSource::move_to_trash (const string& trash_dir_name)
164 {
165         if (!within_session() || !writable()) {
166                 return -1;
167         }
168
169         /* don't move the file across filesystems, just stick it in the
170            trash_dir_name directory on whichever filesystem it was already on
171         */
172
173         vector<string> v;
174         v.push_back (Glib::path_get_dirname (Glib::path_get_dirname (_path)));
175         v.push_back (trash_dir_name);
176         v.push_back (Glib::path_get_basename (_path));
177
178         string newpath = Glib::build_filename (v);
179
180         /* the new path already exists, try versioning */
181
182         if (Glib::file_test (newpath.c_str(), Glib::FILE_TEST_EXISTS)) {
183                 char buf[PATH_MAX+1];
184                 int version = 1;
185                 string newpath_v;
186
187                 snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), version);
188                 newpath_v = buf;
189
190                 while (Glib::file_test (newpath_v, Glib::FILE_TEST_EXISTS) && version < 999) {
191                         snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), ++version);
192                         newpath_v = buf;
193                 }
194
195                 if (version == 999) {
196                         PBD::error << string_compose (
197                                         _("there are already 1000 files with names like %1; versioning discontinued"),
198                                         newpath) << endmsg;
199                 } else {
200                         newpath = newpath_v;
201                 }
202         }
203
204         if (::rename (_path.c_str(), newpath.c_str()) != 0) {
205                 PBD::error << string_compose (
206                                 _("cannot rename file source from %1 to %2 (%3)"),
207                                 _path, newpath, strerror (errno)) << endmsg;
208                 return -1;
209         }
210
211         if (move_dependents_to_trash() != 0) {
212                 /* try to back out */
213                 rename (newpath.c_str(), _path.c_str());
214                 return -1;
215         }
216
217         _path = newpath;
218
219         /* file can not be removed twice, since the operation is not idempotent */
220         _flags = Flag (_flags & ~(RemoveAtDestroy|Removable|RemovableIfEmpty));
221
222         return 0;
223 }
224
225 /** Find the actual source file based on \a filename.
226  *
227  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
228  * If the source is external, \a filename should be a full path.
229  * In either case, found_path is set to the complete absolute path of the source file.
230  * \return true iff the file was found.
231  */
232 bool
233 FileSource::find (Session& s, DataType type, const string& path, bool must_exist,
234                   bool& isnew, uint16_t& /* chan */, string& found_path)
235 {
236         bool ret = false;
237         string keeppath;
238
239         isnew = false;
240
241         if (!Glib::path_is_absolute (path)) {
242                 vector<string> dirs;
243                 vector<string> hits;
244                 string fullpath;
245
246                 string search_path = s.source_search_path (type);
247
248                 if (search_path.length() == 0) {
249                         error << _("FileSource: search path not set") << endmsg;
250                         goto out;
251                 }
252
253                 split (search_path, dirs, ':');
254
255                 hits.clear ();
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::sys::inodes_same (*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, search_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                                 error << string_compose(
312                                         _("Filesource: cannot find required file (%1): while searching %2"),
313                                         path, search_path) << endmsg;
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         if (keeppath == "") {
333                 if (must_exist) {
334                         error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
335                 } else {
336                         keeppath = path;
337                 }
338         }
339
340         found_path = keeppath;
341
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 search_path = s.source_search_path (type);
360
361         string pathstr = path;
362         string::size_type pos;
363         bool ret = false;
364
365         isnew = false;
366
367         if (!Glib::path_is_absolute (pathstr)) {
368
369                 /* non-absolute pathname: find pathstr in search path */
370
371                 vector<string> dirs;
372                 int cnt;
373                 string fullpath;
374                 string keeppath;
375
376                 if (search_path.length() == 0) {
377                         error << _("FileSource: search path not set") << endmsg;
378                         goto out;
379                 }
380
381                 split (search_path, dirs, ':');
382
383                 cnt = 0;
384
385                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
386
387                         fullpath = Glib::build_filename (*i, pathstr);
388
389                         /* i (paul) made a nasty design error by using ':' as a special character in
390                            Ardour 0.99 .. this hack tries to make things sort of work.
391                         */
392
393                         if ((pos = pathstr.find_last_of (':')) != string::npos) {
394
395                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
396
397                                         /* its a real file, no problem */
398
399                                         keeppath = fullpath;
400                                         ++cnt;
401
402                                 } else {
403
404                                         if (must_exist) {
405
406                                                 /* might be an older session using file:channel syntax. see if the version
407                                                    without the :suffix exists
408                                                  */
409
410                                                 string shorter = pathstr.substr (0, pos);
411                                                 fullpath = Glib::build_filename (*i, shorter);
412
413                                                 if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
414                                                         chan = atoi (pathstr.substr (pos+1));
415                                                         pathstr = shorter;
416                                                         keeppath = fullpath;
417                                                         ++cnt;
418                                                 }
419
420                                         } else {
421
422                                                 /* new derived file (e.g. for timefx) being created in a newer session */
423
424                                         }
425                                 }
426
427                         } else {
428
429                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
430                                         keeppath = fullpath;
431                                         ++cnt;
432                                 }
433                         }
434                 }
435
436                 if (cnt > 1) {
437
438                         error << string_compose (
439                                         _("FileSource: \"%1\" is ambigous when searching %2\n\t"),
440                                         pathstr, search_path) << endmsg;
441                         goto out;
442
443                 } else if (cnt == 0) {
444
445                         if (must_exist) {
446                                 error << string_compose(
447                                                 _("Filesource: cannot find required file (%1): while searching %2"),
448                                                 pathstr, search_path) << endmsg;
449                                 goto out;
450                         } else {
451                                 isnew = true;
452                         }
453                 }
454
455                 /* Current find() is unable to parse relative path names to yet non-existant
456                    sources. QuickFix(tm) */
457                 if (keeppath == "") {
458                         if (must_exist) {
459                                 error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
460                         } else {
461                                 keeppath = pathstr;
462                         }
463                 }
464
465                 found_path = keeppath;
466
467                 ret = true;
468
469         } else {
470
471                 /* external files and/or very very old style sessions include full paths */
472
473                 /* ugh, handle ':' situation */
474
475                 if ((pos = pathstr.find_last_of (':')) != string::npos) {
476
477                         string shorter = pathstr.substr (0, pos);
478
479                         if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
480                                 chan = atoi (pathstr.substr (pos+1));
481                                 pathstr = shorter;
482                         }
483                 }
484
485                 found_path = pathstr;
486
487                 if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
488
489                         /* file does not exist or we cannot read it */
490
491                         if (must_exist) {
492                                 error << string_compose(
493                                                 _("Filesource: cannot find required file (%1): %2"),
494                                                 path, strerror (errno)) << endmsg;
495                                 goto out;
496                         }
497
498                         if (errno != ENOENT) {
499                                 error << string_compose(
500                                                 _("Filesource: cannot check for existing file (%1): %2"),
501                                                 path, strerror (errno)) << endmsg;
502                                 goto out;
503                         }
504
505                         /* a new file */
506                         isnew = true;
507                         ret = true;
508
509                 } else {
510
511                         /* already exists */
512                         ret = true;
513                 }
514         }
515
516 out:
517         return ret;
518 }
519
520 int
521 FileSource::set_source_name (const string& newname, bool destructive)
522 {
523         Glib::Mutex::Lock lm (_lock);
524         string oldpath = _path;
525         string newpath = _session.change_source_path_by_name (oldpath, _name, newname, destructive);
526
527         if (newpath.empty()) {
528                 error << string_compose (_("programming error: %1"), "cannot generate a changed file path") << endmsg;
529                 return -1;
530         }
531
532         // Test whether newpath exists, if yes notify the user but continue.
533         if (Glib::file_test (newpath, Glib::FILE_TEST_EXISTS)) {
534                 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;
535                 return -1;
536         }
537
538         if (::rename (oldpath.c_str(), newpath.c_str()) != 0) {
539                 error << string_compose (_("cannot rename file %1 to %2 (%3)"), oldpath, newpath, strerror(errno)) << endmsg;
540                 return -1;
541         }
542
543         _name = Glib::path_get_basename (newpath);
544         _path = newpath;
545
546         return 0;
547 }
548
549 void
550 FileSource::mark_immutable ()
551 {
552         /* destructive sources stay writable, and their other flags don't change.  */
553         if (!(_flags & Destructive)) {
554                 _flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
555         }
556 }
557
558 void
559 FileSource::mark_nonremovable ()
560 {
561         _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
562 }
563
564 void
565 FileSource::set_within_session_from_path (const std::string& path)
566 {
567         _within_session = _session.path_is_within_session (path);
568 }
569
570 void
571 FileSource::set_path (const std::string& newpath)
572 {
573         _path = newpath;
574 }
575
576 void
577 FileSource::inc_use_count ()
578 {
579         Source::inc_use_count ();
580 }
581