further code simplification and rationalization related to MIDI source/file renaming
[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::Signal2<int,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 FileSource::~FileSource()
85 {
86 }
87
88 void
89 FileSource::prevent_deletion ()
90 {
91         /* if this file already exists, it cannot be removed, ever
92          */
93
94         if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
95                 cerr << " ... " << _path << " already  exists, marking immutable\n";
96
97                 if (!(_flags & Destructive)) {
98                         mark_immutable ();
99                 } else {
100                         _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
101                 }
102         }
103 }
104
105 bool
106 FileSource::removable () const
107 {
108         bool r = ((_flags & Removable)
109                   && ((_flags & RemoveAtDestroy) ||
110                       ((_flags & RemovableIfEmpty) && empty())));
111
112         return r;
113 }
114
115 int
116 FileSource::init (const string& pathstr, bool must_exist)
117 {
118         _timeline_position = 0;
119
120         if (Stateful::loading_state_version < 3000) {
121                 if (!find_2X (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
122                         throw MissingSource (pathstr, _type);
123                 }
124         } else {
125                 if (!find (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
126                         throw MissingSource (pathstr, _type);
127                 }
128         }
129
130         set_within_session_from_path (_path);
131
132         _name = Glib::path_get_basename (_path);
133
134         if (must_exist) {
135                 if (!Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
136                         throw MissingSource (pathstr, _type);
137                 }
138         }
139
140         return 0;
141 }
142
143 int
144 FileSource::set_state (const XMLNode& node, int /*version*/)
145 {
146         const XMLProperty* prop;
147
148         if ((prop = node.property (X_("channel"))) != 0) {
149                 _channel = atoi (prop->value());
150         } else {
151                 _channel = 0;
152         }
153
154         if ((prop = node.property (X_("origin"))) != 0) {
155                 _origin = prop->value();
156         }
157
158         return 0;
159 }
160
161 void
162 FileSource::mark_take (const string& id)
163 {
164         if (writable ()) {
165                 _take_id = id;
166         }
167 }
168
169 int
170 FileSource::move_to_trash (const string& trash_dir_name)
171 {
172         if (!within_session() || !writable()) {
173                 return -1;
174         }
175
176         /* don't move the file across filesystems, just stick it in the
177            trash_dir_name directory on whichever filesystem it was already on
178         */
179
180         vector<string> v;
181         v.push_back (Glib::path_get_dirname (Glib::path_get_dirname (_path)));
182         v.push_back (trash_dir_name);
183         v.push_back (Glib::path_get_basename (_path));
184
185         string newpath = Glib::build_filename (v);
186
187         /* the new path already exists, try versioning */
188
189         if (Glib::file_test (newpath.c_str(), Glib::FILE_TEST_EXISTS)) {
190                 char buf[PATH_MAX+1];
191                 int version = 1;
192                 string newpath_v;
193
194                 snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), version);
195                 newpath_v = buf;
196
197                 while (Glib::file_test (newpath_v, Glib::FILE_TEST_EXISTS) && version < 999) {
198                         snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), ++version);
199                         newpath_v = buf;
200                 }
201
202                 if (version == 999) {
203                         PBD::error << string_compose (
204                                         _("there are already 1000 files with names like %1; versioning discontinued"),
205                                         newpath) << endmsg;
206                 } else {
207                         newpath = newpath_v;
208                 }
209         }
210
211         if (::rename (_path.c_str(), newpath.c_str()) != 0) {
212                 PBD::error << string_compose (
213                                 _("cannot rename file source from %1 to %2 (%3)"),
214                                 _path, newpath, strerror (errno)) << endmsg;
215                 return -1;
216         }
217
218         if (move_dependents_to_trash() != 0) {
219                 /* try to back out */
220                 rename (newpath.c_str(), _path.c_str());
221                 return -1;
222         }
223
224         _path = newpath;
225
226         /* file can not be removed twice, since the operation is not idempotent */
227         _flags = Flag (_flags & ~(RemoveAtDestroy|Removable|RemovableIfEmpty));
228
229         return 0;
230 }
231
232 /** Find the actual source file based on \a filename.
233  *
234  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
235  * If the source is external, \a filename should be a full path.
236  * In either case, found_path is set to the complete absolute path of the source file.
237  * \return true iff the file was found.
238  */
239 bool
240 FileSource::find (Session& s, DataType type, const string& path, bool must_exist,
241                   bool& isnew, uint16_t& /* chan */, string& found_path)
242 {
243         bool ret = false;
244         string keeppath;
245
246         isnew = false;
247
248         if (!Glib::path_is_absolute (path)) {
249                 vector<string> hits;
250                 string fullpath;
251                 std::vector<std::string> dirs = s.source_search_path (type);
252
253                 if (dirs.size() == 0) {
254                         error << _("FileSource: search path not set") << endmsg;
255                         goto out;
256                 }
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, 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)"), path) << endmsg;
316                                 goto out;
317                         } else {
318                                 isnew = true;
319                         }
320                 } else {
321
322                         /* only one match: happy days */
323                         
324                         keeppath = de_duped_hits[0];
325                 }
326                                                   
327         } else {
328                 keeppath = path;
329         }
330
331         /* Current find() is unable to parse relative path names to yet non-existant
332            sources. QuickFix(tm)
333         */
334         if (keeppath == "") {
335                 if (must_exist) {
336                         error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
337                 } else {
338                         keeppath = path;
339                 }
340         }
341
342         found_path = keeppath;
343
344         ret = true;
345
346   out:
347         return ret;
348 }
349
350 /** Find the actual source file based on \a filename.
351  *
352  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
353  * If the source is external, \a filename should be a full path.
354  * In either case, found_path is set to the complete absolute path of the source file.
355  * \return true iff the file was found.
356  */
357 bool
358 FileSource::find_2X (Session& s, DataType type, const string& path, bool must_exist,
359                      bool& isnew, uint16_t& chan, string& found_path)
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 = s.source_search_path (type);
372
373                 int cnt;
374                 string fullpath;
375                 string keeppath;
376
377                 if (dirs.size() == 0) {
378                         error << _("FileSource: search path not set") << endmsg;
379                         goto out;
380                 }
381
382                 cnt = 0;
383
384                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
385
386                         fullpath = Glib::build_filename (*i, pathstr);
387
388                         /* i (paul) made a nasty design error by using ':' as a special character in
389                            Ardour 0.99 .. this hack tries to make things sort of work.
390                         */
391
392                         if ((pos = pathstr.find_last_of (':')) != string::npos) {
393
394                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
395
396                                         /* its a real file, no problem */
397
398                                         keeppath = fullpath;
399                                         ++cnt;
400
401                                 } else {
402
403                                         if (must_exist) {
404
405                                                 /* might be an older session using file:channel syntax. see if the version
406                                                    without the :suffix exists
407                                                  */
408
409                                                 string shorter = pathstr.substr (0, pos);
410                                                 fullpath = Glib::build_filename (*i, shorter);
411
412                                                 if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
413                                                         chan = atoi (pathstr.substr (pos+1));
414                                                         pathstr = shorter;
415                                                         keeppath = fullpath;
416                                                         ++cnt;
417                                                 }
418
419                                         } else {
420
421                                                 /* new derived file (e.g. for timefx) being created in a newer session */
422
423                                         }
424                                 }
425
426                         } else {
427
428                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
429                                         keeppath = fullpath;
430                                         ++cnt;
431                                 }
432                         }
433                 }
434
435                 if (cnt > 1) {
436
437                         error << string_compose (
438                                         _("FileSource: \"%1\" is ambigous when searching\n\t"), pathstr) << endmsg;
439                         goto out;
440
441                 } else if (cnt == 0) {
442
443                         if (must_exist) {
444                                 error << string_compose(
445                                                 _("Filesource: cannot find required file (%1)"), pathstr) << endmsg;
446                                 goto out;
447                         } else {
448                                 isnew = true;
449                         }
450                 }
451
452                 /* Current find() is unable to parse relative path names to yet non-existant
453                    sources. QuickFix(tm) */
454                 if (keeppath == "") {
455                         if (must_exist) {
456                                 error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
457                         } else {
458                                 keeppath = pathstr;
459                         }
460                 }
461
462                 found_path = keeppath;
463
464                 ret = true;
465
466         } else {
467
468                 /* external files and/or very very old style sessions include full paths */
469
470                 /* ugh, handle ':' situation */
471
472                 if ((pos = pathstr.find_last_of (':')) != string::npos) {
473
474                         string shorter = pathstr.substr (0, pos);
475
476                         if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
477                                 chan = atoi (pathstr.substr (pos+1));
478                                 pathstr = shorter;
479                         }
480                 }
481
482                 found_path = pathstr;
483
484                 if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
485
486                         /* file does not exist or we cannot read it */
487
488                         if (must_exist) {
489                                 error << string_compose(
490                                                 _("Filesource: cannot find required file (%1): %2"),
491                                                 path, strerror (errno)) << endmsg;
492                                 goto out;
493                         }
494
495 #ifndef PLATFORM_WINDOWS
496                         if (errno != ENOENT) {
497                                 error << string_compose(
498                                                 _("Filesource: cannot check for existing file (%1): %2"),
499                                                 path, strerror (errno)) << endmsg;
500                                 goto out;
501                         }
502 #endif
503                         /* a new file */
504                         isnew = true;
505                         ret = true;
506
507                 } else {
508
509                         /* already exists */
510                         ret = true;
511                 }
512         }
513
514 out:
515         return ret;
516 }
517
518 void
519 FileSource::mark_immutable ()
520 {
521         /* destructive sources stay writable, and their other flags don't change.  */
522         if (!(_flags & Destructive)) {
523                 _flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
524         }
525 }
526
527 void
528 FileSource::mark_immutable_except_write ()
529 {
530         /* destructive sources stay writable, and their other flags don't change.  */
531         if (!(_flags & Destructive)) {
532                 _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
533         }
534 }
535
536 void
537 FileSource::mark_nonremovable ()
538 {
539         _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
540 }
541
542 void
543 FileSource::set_within_session_from_path (const std::string& path)
544 {
545         _within_session = _session.path_is_within_session (path);
546 }
547
548 void
549 FileSource::set_path (const std::string& newpath)
550 {
551         _path = newpath;
552 }
553
554 void
555 FileSource::inc_use_count ()
556 {
557         Source::inc_use_count ();
558 }
559
560 bool
561 FileSource::is_stub () const
562 {
563         if (!empty()) {
564                 return false;
565         }
566         
567         if (!removable()) {
568                 return false;
569         }
570
571         if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
572                 return false;
573         }
574
575         return true;
576 }
577