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