877a0a49390465b570a39c3a3d8f187dd90ac79b
[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
37 #include <glibmm/miscutils.h>
38 #include <glibmm/fileutils.h>
39 #include <glibmm/thread.h>
40
41 #include "ardour/file_source.h"
42 #include "ardour/directory_names.h"
43 #include "ardour/session.h"
44 #include "ardour/session_directory.h"
45 #include "ardour/source_factory.h"
46 #include "ardour/filename_extensions.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         if (!within_session()) {
128                 _session.ensure_search_path_includes (Glib::path_get_dirname (_path), _type);
129         }
130
131         _name = Glib::path_get_basename (_path);
132
133         if (_file_is_new && must_exist) {
134                 return -1;
135         }
136
137         return 0;
138 }
139
140 int
141 FileSource::set_state (const XMLNode& node, int /*version*/)
142 {
143         const XMLProperty* prop;
144
145         if ((prop = node.property (X_("channel"))) != 0) {
146                 _channel = atoi (prop->value());
147         } else {
148                 _channel = 0;
149         }
150
151         if ((prop = node.property (X_("origin"))) != 0) {
152                 _origin = prop->value();
153         }
154
155         return 0;
156 }
157
158 void
159 FileSource::mark_take (const string& id)
160 {
161         if (writable ()) {
162                 _take_id = id;
163         }
164 }
165
166 int
167 FileSource::move_to_trash (const string& trash_dir_name)
168 {
169         if (!within_session() || !writable()) {
170                 return -1;
171         }
172
173         /* don't move the file across filesystems, just stick it in the
174            trash_dir_name directory on whichever filesystem it was already on
175         */
176
177         vector<string> v;
178         v.push_back (Glib::path_get_dirname (Glib::path_get_dirname (_path)));
179         v.push_back (trash_dir_name);
180         v.push_back (Glib::path_get_basename (_path));
181
182         string newpath = Glib::build_filename (v);
183
184         /* the new path already exists, try versioning */
185
186         if (Glib::file_test (newpath.c_str(), Glib::FILE_TEST_EXISTS)) {
187                 char buf[PATH_MAX+1];
188                 int version = 1;
189                 string newpath_v;
190
191                 snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), version);
192                 newpath_v = buf;
193
194                 while (Glib::file_test (newpath_v, Glib::FILE_TEST_EXISTS) && version < 999) {
195                         snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), ++version);
196                         newpath_v = buf;
197                 }
198
199                 if (version == 999) {
200                         PBD::error << string_compose (
201                                         _("there are already 1000 files with names like %1; versioning discontinued"),
202                                         newpath) << endmsg;
203                 } else {
204                         newpath = newpath_v;
205                 }
206         }
207
208         if (::rename (_path.c_str(), newpath.c_str()) != 0) {
209                 PBD::error << string_compose (
210                                 _("cannot rename file source from %1 to %2 (%3)"),
211                                 _path, newpath, strerror (errno)) << endmsg;
212                 return -1;
213         }
214
215         if (move_dependents_to_trash() != 0) {
216                 /* try to back out */
217                 rename (newpath.c_str(), _path.c_str());
218                 return -1;
219         }
220
221         _path = newpath;
222
223         /* file can not be removed twice, since the operation is not idempotent */
224         _flags = Flag (_flags & ~(RemoveAtDestroy|Removable|RemovableIfEmpty));
225
226         return 0;
227 }
228
229 /** Find the actual source file based on \a filename.
230  *
231  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
232  * If the source is external, \a filename should be a full path.
233  * In either case, found_path is set to the complete absolute path of the source file.
234  * \return true iff the file was found.
235  */
236 bool
237 FileSource::find (Session& s, DataType type, const string& path, bool must_exist,
238                   bool& isnew, uint16_t& chan, string& found_path)
239 {
240         bool ret = false;
241         string keeppath;
242
243         isnew = false;
244
245         if (!Glib::path_is_absolute (path)) {
246                 vector<string> dirs;
247                 vector<string> hits;
248                 string fullpath;
249
250                 string search_path = s.source_search_path (type);
251
252                 if (search_path.length() == 0) {
253                         error << _("FileSource: search path not set") << endmsg;
254                         goto out;
255                 }
256
257                 split (search_path, dirs, ':');
258
259                 hits.clear ();
260
261                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
262
263                         fullpath = Glib::build_filename (*i, path);
264
265                         if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
266                                 keeppath = fullpath;
267                                 hits.push_back (fullpath);
268                         }
269                 }
270
271                 /* Remove duplicate inodes from the list of ambiguous files, since if there are symlinks
272                    in the session path it is possible to arrive at the same file via more than one path.
273                 */
274
275                 vector<string> de_duped_hits;
276
277                 for (vector<string>::iterator i = hits.begin(); i != hits.end(); ++i) {
278
279                         vector<string>::iterator j = i;
280                         ++j;
281                         
282                         while (j != hits.end()) {
283
284                                 struct stat bufA;
285                                 int const rA = stat (i->c_str(), &bufA);
286                                 struct stat bufB;
287                                 int const rB = stat (j->c_str(), &bufB);
288
289                                 if (rA == 0 && rB == 0 && bufA.st_ino == bufB.st_ino) {
290                                         /* *i and *j are the same file; break out of the loop early */
291                                         break;
292                                 }
293
294                                 ++j;
295                         }
296
297                         if (j == hits.end ()) {
298                                 de_duped_hits.push_back (*i);
299                         }
300                 }
301
302                 if (de_duped_hits.size() > 1) {
303
304                         /* more than one match: ask the user */
305
306                         int which = FileSource::AmbiguousFileName (path, search_path, de_duped_hits).get_value_or (-1);
307
308                         if (which < 0) {
309                                 goto out;
310                         } else {
311                                 keeppath = de_duped_hits[which];
312                         }
313
314                 } else if (de_duped_hits.size() == 0) {
315
316                         /* no match: error */
317
318                         if (must_exist) {
319                                 error << string_compose(
320                                         _("Filesource: cannot find required file (%1): while searching %2"),
321                                         path, search_path) << endmsg;
322                                 goto out;
323                         } else {
324                                 isnew = true;
325                         }
326                 } else {
327
328                         /* only one match: happy days */
329                         
330                         keeppath = de_duped_hits[0];
331                 }
332                                                   
333         } else {
334                 keeppath = path;
335         }
336
337         /* Current find() is unable to parse relative path names to yet non-existant
338            sources. QuickFix(tm)
339         */
340         if (keeppath == "") {
341                 if (must_exist) {
342                         error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
343                 } else {
344                         keeppath = path;
345                 }
346         }
347
348         found_path = keeppath;
349
350         ret = true;
351
352   out:
353         return ret;
354 }
355
356 /** Find the actual source file based on \a filename.
357  *
358  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
359  * If the source is external, \a filename should be a full path.
360  * In either case, found_path is set to the complete absolute path of the source file.
361  * \return true iff the file was found.
362  */
363 bool
364 FileSource::find_2X (Session& s, DataType type, const string& path, bool must_exist,
365                      bool& isnew, uint16_t& chan, string& found_path)
366 {
367         string search_path = s.source_search_path (type);
368
369         string pathstr = path;
370         string::size_type pos;
371         bool ret = false;
372
373         isnew = false;
374
375         if (!Glib::path_is_absolute (pathstr)) {
376
377                 /* non-absolute pathname: find pathstr in search path */
378
379                 vector<string> dirs;
380                 int cnt;
381                 string fullpath;
382                 string keeppath;
383
384                 if (search_path.length() == 0) {
385                         error << _("FileSource: search path not set") << endmsg;
386                         goto out;
387                 }
388
389                 split (search_path, dirs, ':');
390
391                 cnt = 0;
392
393                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
394
395                         fullpath = Glib::build_filename (*i, pathstr);
396
397                         /* i (paul) made a nasty design error by using ':' as a special character in
398                            Ardour 0.99 .. this hack tries to make things sort of work.
399                         */
400
401                         if ((pos = pathstr.find_last_of (':')) != string::npos) {
402
403                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
404
405                                         /* its a real file, no problem */
406
407                                         keeppath = fullpath;
408                                         ++cnt;
409
410                                 } else {
411
412                                         if (must_exist) {
413
414                                                 /* might be an older session using file:channel syntax. see if the version
415                                                    without the :suffix exists
416                                                  */
417
418                                                 string shorter = pathstr.substr (0, pos);
419                                                 fullpath = Glib::build_filename (*i, shorter);
420
421                                                 if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
422                                                         chan = atoi (pathstr.substr (pos+1));
423                                                         pathstr = shorter;
424                                                         keeppath = fullpath;
425                                                         ++cnt;
426                                                 }
427
428                                         } else {
429
430                                                 /* new derived file (e.g. for timefx) being created in a newer session */
431
432                                         }
433                                 }
434
435                         } else {
436
437                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
438                                         keeppath = fullpath;
439                                         ++cnt;
440                                 }
441                         }
442                 }
443
444                 if (cnt > 1) {
445
446                         error << string_compose (
447                                         _("FileSource: \"%1\" is ambigous when searching %2\n\t"),
448                                         pathstr, search_path) << endmsg;
449                         goto out;
450
451                 } else if (cnt == 0) {
452
453                         if (must_exist) {
454                                 error << string_compose(
455                                                 _("Filesource: cannot find required file (%1): while searching %2"),
456                                                 pathstr, search_path) << endmsg;
457                                 goto out;
458                         } else {
459                                 isnew = true;
460                         }
461                 }
462
463                 /* Current find() is unable to parse relative path names to yet non-existant
464                    sources. QuickFix(tm) */
465                 if (keeppath == "") {
466                         if (must_exist) {
467                                 error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
468                         } else {
469                                 keeppath = pathstr;
470                         }
471                 }
472
473                 found_path = keeppath;
474
475                 ret = true;
476
477         } else {
478
479                 /* external files and/or very very old style sessions include full paths */
480
481                 /* ugh, handle ':' situation */
482
483                 if ((pos = pathstr.find_last_of (':')) != string::npos) {
484
485                         string shorter = pathstr.substr (0, pos);
486
487                         if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
488                                 chan = atoi (pathstr.substr (pos+1));
489                                 pathstr = shorter;
490                         }
491                 }
492
493                 found_path = pathstr;
494
495                 if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
496
497                         /* file does not exist or we cannot read it */
498
499                         if (must_exist) {
500                                 error << string_compose(
501                                                 _("Filesource: cannot find required file (%1): %2"),
502                                                 path, strerror (errno)) << endmsg;
503                                 goto out;
504                         }
505
506                         if (errno != ENOENT) {
507                                 error << string_compose(
508                                                 _("Filesource: cannot check for existing file (%1): %2"),
509                                                 path, strerror (errno)) << endmsg;
510                                 goto out;
511                         }
512
513                         /* a new file */
514                         isnew = true;
515                         ret = true;
516
517                 } else {
518
519                         /* already exists */
520                         ret = true;
521                 }
522         }
523
524 out:
525         return ret;
526 }
527
528 int
529 FileSource::set_source_name (const string& newname, bool destructive)
530 {
531         Glib::Mutex::Lock lm (_lock);
532         string oldpath = _path;
533         string newpath = _session.change_source_path_by_name (oldpath, _name, newname, destructive);
534
535         if (newpath.empty()) {
536                 error << string_compose (_("programming error: %1"), "cannot generate a changed file path") << endmsg;
537                 return -1;
538         }
539
540         // Test whether newpath exists, if yes notify the user but continue.
541         if (Glib::file_test (newpath, Glib::FILE_TEST_EXISTS)) {
542                 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;
543                 return -1;
544         }
545
546         if (::rename (oldpath.c_str(), newpath.c_str()) != 0) {
547                 error << string_compose (_("cannot rename file %1 to %2 (%3)"), oldpath, newpath, strerror(errno)) << endmsg;
548                 return -1;
549         }
550
551         _name = Glib::path_get_basename (newpath);
552         _path = newpath;
553
554         return 0;
555 }
556
557 void
558 FileSource::mark_immutable ()
559 {
560         /* destructive sources stay writable, and their other flags don't change.  */
561         if (!(_flags & Destructive)) {
562                 _flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
563         }
564 }
565
566 void
567 FileSource::mark_nonremovable ()
568 {
569         _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
570 }
571
572 void
573 FileSource::set_within_session_from_path (const std::string& path)
574 {
575         _within_session = _session.path_is_within_session (path);
576 }
577
578 void
579 FileSource::set_path (const std::string& newpath)
580 {
581         _path = newpath;
582 }
583
584 void
585 FileSource::inc_use_count ()
586 {
587         Source::inc_use_count ();
588 }
589