initial pass at a missing file dialog and "relocatable" source files. lots more to...
[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, Source::Flag flag)
58         : Source(session, type, path, flag)
59         , _path(path)
60         , _file_is_new(true)
61         , _channel (0)
62 {
63         set_within_session_from_path (path);
64 }
65
66 FileSource::FileSource (Session& session, const XMLNode& node, bool /*must_exist*/)
67         : Source(session, node)
68         , _file_is_new (false)
69 {
70         /* this setting of _path is temporary - we expect derived classes
71            to call ::init() which will actually locate the file
72            and reset _path and _within_session correctly.
73         */
74
75         _path = _name;
76         _within_session = true;
77 }
78
79 bool
80 FileSource::removable () const
81 {
82         bool r = (_path.find (stub_dir_name) != string::npos) ||
83                 ((_flags & Removable)
84                  && ((_flags & RemoveAtDestroy) || 
85                      ((_flags & RemovableIfEmpty) && empty() == 0)));
86
87         return r;
88 }
89
90 int
91 FileSource::init (const string& pathstr, bool must_exist)
92 {
93         _timeline_position = 0;
94
95         if (Stateful::loading_state_version < 3000) {
96                 if (!find_2X (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
97                         throw MissingSource (pathstr, _type);
98                 }
99         } else {
100                 if (!find (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
101                         throw MissingSource (pathstr, _type);
102                 }
103         }
104
105         set_within_session_from_path (pathstr);
106
107         if (_within_session) {
108                 _name = Glib::path_get_basename (_name);
109         }
110
111         if (_file_is_new && must_exist) {
112                 return -1;
113         }
114
115         return 0;
116 }
117
118 int
119 FileSource::set_state (const XMLNode& node, int /*version*/)
120 {
121         const XMLProperty* prop;
122
123         if ((prop = node.property (X_("channel"))) != 0) {
124                 _channel = atoi (prop->value());
125         } else {
126                 _channel = 0;
127         }
128
129         return 0;
130 }
131
132 void
133 FileSource::mark_take (const string& id)
134 {
135         if (writable ()) {
136                 _take_id = id;
137         }
138 }
139
140 int
141 FileSource::move_to_trash (const string& trash_dir_name)
142 {
143         if (!within_session() || !writable()) {
144                 return -1;
145         }
146
147         /* don't move the file across filesystems, just stick it in the
148            trash_dir_name directory on whichever filesystem it was already on
149         */
150
151         vector<string> v;
152         v.push_back (Glib::path_get_dirname (Glib::path_get_dirname (_path)));
153         v.push_back (trash_dir_name);
154         v.push_back (Glib::path_get_basename (_path));
155
156         string newpath = Glib::build_filename (v);
157
158         /* the new path already exists, try versioning */
159
160         if (Glib::file_test (newpath.c_str(), Glib::FILE_TEST_EXISTS)) {
161                 char buf[PATH_MAX+1];
162                 int version = 1;
163                 string newpath_v;
164
165                 snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), version);
166                 newpath_v = buf;
167
168                 while (access (newpath_v.c_str(), F_OK) == 0 && version < 999) {
169                         snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), ++version);
170                         newpath_v = buf;
171                 }
172
173                 if (version == 999) {
174                         PBD::error << string_compose (
175                                         _("there are already 1000 files with names like %1; versioning discontinued"),
176                                         newpath) << endmsg;
177                 } else {
178                         newpath = newpath_v;
179                 }
180         }
181
182         if (::rename (_path.c_str(), newpath.c_str()) != 0) {
183                 PBD::error << string_compose (
184                                 _("cannot rename file source from %1 to %2 (%3)"),
185                                 _path, newpath, strerror (errno)) << endmsg;
186                 return -1;
187         }
188
189         if (move_dependents_to_trash() != 0) {
190                 /* try to back out */
191                 rename (newpath.c_str(), _path.c_str());
192                 return -1;
193         }
194
195         _path = newpath;
196
197         /* file can not be removed twice, since the operation is not idempotent */
198         _flags = Flag (_flags & ~(RemoveAtDestroy|Removable|RemovableIfEmpty));
199
200         return 0;
201 }
202
203 /** Find the actual source file based on \a filename.
204  *
205  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
206  * If the source is external, \a filename should be a full path.
207  * In either case, found_path is set to the complete absolute path of the source file.
208  * \return true iff the file was found.
209  */
210 bool
211 FileSource::find (Session& s, DataType type, const string& path, bool must_exist,
212                   bool& isnew, uint16_t& chan, string& found_path)
213 {
214         string search_path = s.source_search_path (type);
215
216         string pathstr = path;
217         bool ret = false;
218
219         cerr << "Searching along " << search_path << endl;
220
221         isnew = false;
222
223         vector<string> dirs;
224         vector<string> hits;
225         int cnt;
226         string fullpath;
227         string keeppath;
228         
229         if (search_path.length() == 0) {
230                 error << _("FileSource: search path not set") << endmsg;
231                 goto out;
232         }
233         
234         split (search_path, dirs, ':');
235         
236         cnt = 0;
237         hits.clear ();
238         
239         for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
240                 
241                 cerr << "Searching in " << *i << " for " << pathstr << endl;
242                 
243                 fullpath = Glib::build_filename (*i, pathstr);
244                 
245                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
246                         keeppath = fullpath;
247                         hits.push_back (fullpath);
248                         ++cnt;
249                 }
250         }
251         
252         if (cnt > 1) {
253                 
254                 int which = FileSource::AmbiguousFileName (pathstr, search_path, hits).get_value_or (-1);
255
256                 if (which < 0) {
257                         goto out;
258                 } else {
259                         keeppath = hits[which];
260                 }
261                 
262         } else if (cnt == 0) {
263                 
264                 if (must_exist) {
265                         error << string_compose(
266                                 _("Filesource: cannot find required file (%1): while searching %2"),
267                                 pathstr, search_path) << endmsg;
268                         goto out;
269                 } else {
270                         isnew = true;
271                 }
272         }
273         
274         /* Current find() is unable to parse relative path names to yet non-existant
275            sources. QuickFix(tm) 
276         */
277         if (keeppath == "") {
278                 if (must_exist) {
279                         error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
280                 } else {
281                         keeppath = pathstr;
282                 }
283         }
284         
285         found_path = keeppath;
286         
287         ret = true;
288         
289   out:
290         return ret;
291 }
292
293 /** Find the actual source file based on \a filename.
294  *
295  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
296  * If the source is external, \a filename should be a full path.
297  * In either case, found_path is set to the complete absolute path of the source file.
298  * \return true iff the file was found.
299  */
300 bool
301 FileSource::find_2X (Session& s, DataType type, const string& path, bool must_exist,
302                      bool& isnew, uint16_t& chan, string& found_path)
303 {
304         string search_path = s.source_search_path (type);
305
306         string pathstr = path;
307         string::size_type pos;
308         bool ret = false;
309
310         isnew = false;
311
312         if (!Glib::path_is_absolute (pathstr)) {
313
314                 /* non-absolute pathname: find pathstr in search path */
315
316                 vector<string> dirs;
317                 int cnt;
318                 string fullpath;
319                 string keeppath;
320
321                 if (search_path.length() == 0) {
322                         error << _("FileSource: search path not set") << endmsg;
323                         goto out;
324                 }
325
326                 split (search_path, dirs, ':');
327
328                 cnt = 0;
329
330                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
331                         
332                         fullpath = Glib::build_filename (*i, pathstr);
333
334                         /* i (paul) made a nasty design error by using ':' as a special character in
335                            Ardour 0.99 .. this hack tries to make things sort of work.
336                         */
337
338                         if ((pos = pathstr.find_last_of (':')) != string::npos) {
339
340                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
341
342                                         /* its a real file, no problem */
343
344                                         keeppath = fullpath;
345                                         ++cnt;
346
347                                 } else {
348
349                                         if (must_exist) {
350
351                                                 /* might be an older session using file:channel syntax. see if the version
352                                                    without the :suffix exists
353                                                  */
354
355                                                 string shorter = pathstr.substr (0, pos);
356                                                 fullpath = Glib::build_filename (*i, shorter);
357
358                                                 if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
359                                                         chan = atoi (pathstr.substr (pos+1));
360                                                         pathstr = shorter;
361                                                         keeppath = fullpath;
362                                                         ++cnt;
363                                                 }
364
365                                         } else {
366
367                                                 /* new derived file (e.g. for timefx) being created in a newer session */
368
369                                         }
370                                 }
371
372                         } else {
373
374                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
375                                         keeppath = fullpath;
376                                         ++cnt;
377                                 }
378                         }
379                 }
380
381                 if (cnt > 1) {
382
383                         error << string_compose (
384                                         _("FileSource: \"%1\" is ambigous when searching %2\n\t"),
385                                         pathstr, search_path) << endmsg;
386                         goto out;
387
388                 } else if (cnt == 0) {
389
390                         if (must_exist) {
391                                 error << string_compose(
392                                                 _("Filesource: cannot find required file (%1): while searching %2"),
393                                                 pathstr, search_path) << endmsg;
394                                 goto out;
395                         } else {
396                                 isnew = true;
397                         }
398                 }
399
400                 /* Current find() is unable to parse relative path names to yet non-existant
401                    sources. QuickFix(tm) */
402                 if (keeppath == "") {
403                         if (must_exist) {
404                                 error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
405                         } else {
406                                 keeppath = pathstr;
407                         }
408                 }
409
410                 found_path = keeppath;
411
412                 ret = true;
413
414         } else {
415
416                 /* external files and/or very very old style sessions include full paths */
417
418                 /* ugh, handle ':' situation */
419
420                 if ((pos = pathstr.find_last_of (':')) != string::npos) {
421
422                         string shorter = pathstr.substr (0, pos);
423
424                         if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
425                                 chan = atoi (pathstr.substr (pos+1));
426                                 pathstr = shorter;
427                         }
428                 }
429
430                 found_path = pathstr;
431
432                 if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
433
434                         /* file does not exist or we cannot read it */
435
436                         if (must_exist) {
437                                 error << string_compose(
438                                                 _("Filesource: cannot find required file (%1): %2"),
439                                                 path, strerror (errno)) << endmsg;
440                                 goto out;
441                         }
442
443                         if (errno != ENOENT) {
444                                 error << string_compose(
445                                                 _("Filesource: cannot check for existing file (%1): %2"),
446                                                 path, strerror (errno)) << endmsg;
447                                 goto out;
448                         }
449
450                         /* a new file */
451                         isnew = true;
452                         ret = true;
453
454                 } else {
455
456                         /* already exists */
457                         ret = true;
458                 }
459         }
460
461 out:
462         return ret;
463 }
464
465 int
466 FileSource::set_source_name (const string& newname, bool destructive)
467 {
468         Glib::Mutex::Lock lm (_lock);
469         string oldpath = _path;
470         string newpath = _session.change_source_path_by_name (oldpath, _name, newname, destructive);
471
472         if (newpath.empty()) {
473                 error << string_compose (_("programming error: %1"), "cannot generate a changed file path") << endmsg;
474                 return -1;
475         }
476
477         // Test whether newpath exists, if yes notify the user but continue.
478         if (Glib::file_test (newpath, Glib::FILE_TEST_EXISTS)) {
479                 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;
480                 return -1;
481         }
482         
483         if (::rename (oldpath.c_str(), newpath.c_str()) != 0) {
484                 error << string_compose (_("cannot rename file %1 to %2 (%3)"), oldpath, newpath, strerror(errno)) << endmsg;
485                 return -1;
486         }
487
488         _name = Glib::path_get_basename (newpath);
489         _path = newpath;
490
491         return 0;
492 }
493
494 void
495 FileSource::mark_immutable ()
496 {
497         /* destructive sources stay writable, and their other flags don't change.  */
498         if (!(_flags & Destructive)) {
499                 _flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
500         }
501 }
502
503 void
504 FileSource::mark_nonremovable ()
505 {
506         _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
507 }
508
509 void
510 FileSource::set_within_session_from_path (const std::string& path)
511 {
512         _within_session = _session.path_is_within_session (path);
513 }
514
515 int
516 FileSource::unstubify ()
517 {
518         string::size_type pos = _path.find (stub_dir_name);
519
520         if (pos == string::npos || (_flags & Destructive)) {
521                 return 0;
522         }
523
524         vector<string> v;
525
526         v.push_back (Glib::path_get_dirname (Glib::path_get_dirname (_path)));
527         v.push_back (Glib::path_get_basename(_path));
528         
529         string newpath = Glib::build_filename (v);
530
531         if (::rename (_path.c_str(), newpath.c_str()) != 0) {
532                 error << string_compose (_("rename from %1 to %2 failed: %3)"), _path, newpath, strerror (errno)) << endmsg;
533                 return -1;
534         }
535
536         set_path (newpath);
537
538         return 0;
539 }
540
541 void
542 FileSource::set_path (const std::string& newpath)
543 {
544         _path = newpath;
545 }
546
547 void 
548 FileSource::inc_use_count ()
549 {
550         Source::inc_use_count ();
551 }
552