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::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 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> dirs;
250                 vector<string> hits;
251                 string fullpath;
252
253                 string search_path = s.source_search_path (type);
254
255                 if (search_path.length() == 0) {
256                         error << _("FileSource: search path not set") << endmsg;
257                         goto out;
258                 }
259
260                 split (search_path, dirs, ':');
261
262                 hits.clear ();
263
264                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
265
266                         fullpath = Glib::build_filename (*i, path);
267
268                         if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
269                                 keeppath = fullpath;
270                                 hits.push_back (fullpath);
271                         }
272                 }
273
274                 /* Remove duplicate inodes from the list of ambiguous files, since if there are symlinks
275                    in the session path it is possible to arrive at the same file via more than one path.
276
277                    I suppose this is not necessary on Windows.
278                 */
279
280                 vector<string> de_duped_hits;
281
282                 for (vector<string>::iterator i = hits.begin(); i != hits.end(); ++i) {
283
284                         vector<string>::iterator j = i;
285                         ++j;
286                         
287                         while (j != hits.end()) {
288                                 if (PBD::equivalent_paths (*i, *j)) {
289                                         /* *i and *j are the same file; break out of the loop early */
290                                         break;
291                                 }
292
293                                 ++j;
294                         }
295
296                         if (j == hits.end ()) {
297                                 de_duped_hits.push_back (*i);
298                         }
299                 }
300
301                 if (de_duped_hits.size() > 1) {
302
303                         /* more than one match: ask the user */
304
305                         int which = FileSource::AmbiguousFileName (path, search_path, de_duped_hits).get_value_or (-1);
306
307                         if (which < 0) {
308                                 goto out;
309                         } else {
310                                 keeppath = de_duped_hits[which];
311                         }
312
313                 } else if (de_duped_hits.size() == 0) {
314
315                         /* no match: error */
316
317                         if (must_exist) {
318                                 error << string_compose(
319                                         _("Filesource: cannot find required file (%1): while searching %2"),
320                                         path, search_path) << endmsg;
321                                 goto out;
322                         } else {
323                                 isnew = true;
324                         }
325                 } else {
326
327                         /* only one match: happy days */
328                         
329                         keeppath = de_duped_hits[0];
330                 }
331                                                   
332         } else {
333                 keeppath = path;
334         }
335
336         /* Current find() is unable to parse relative path names to yet non-existant
337            sources. QuickFix(tm)
338         */
339         if (keeppath == "") {
340                 if (must_exist) {
341                         error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
342                 } else {
343                         keeppath = path;
344                 }
345         }
346
347         found_path = keeppath;
348
349         ret = true;
350
351   out:
352         return ret;
353 }
354
355 /** Find the actual source file based on \a filename.
356  *
357  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
358  * If the source is external, \a filename should be a full path.
359  * In either case, found_path is set to the complete absolute path of the source file.
360  * \return true iff the file was found.
361  */
362 bool
363 FileSource::find_2X (Session& s, DataType type, const string& path, bool must_exist,
364                      bool& isnew, uint16_t& chan, string& found_path)
365 {
366         string search_path = s.source_search_path (type);
367
368         string pathstr = path;
369         string::size_type pos;
370         bool ret = false;
371
372         isnew = false;
373
374         if (!Glib::path_is_absolute (pathstr)) {
375
376                 /* non-absolute pathname: find pathstr in search path */
377
378                 vector<string> dirs;
379                 int cnt;
380                 string fullpath;
381                 string keeppath;
382
383                 if (search_path.length() == 0) {
384                         error << _("FileSource: search path not set") << endmsg;
385                         goto out;
386                 }
387
388                 split (search_path, dirs, ':');
389
390                 cnt = 0;
391
392                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
393
394                         fullpath = Glib::build_filename (*i, pathstr);
395
396                         /* i (paul) made a nasty design error by using ':' as a special character in
397                            Ardour 0.99 .. this hack tries to make things sort of work.
398                         */
399
400                         if ((pos = pathstr.find_last_of (':')) != string::npos) {
401
402                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
403
404                                         /* its a real file, no problem */
405
406                                         keeppath = fullpath;
407                                         ++cnt;
408
409                                 } else {
410
411                                         if (must_exist) {
412
413                                                 /* might be an older session using file:channel syntax. see if the version
414                                                    without the :suffix exists
415                                                  */
416
417                                                 string shorter = pathstr.substr (0, pos);
418                                                 fullpath = Glib::build_filename (*i, shorter);
419
420                                                 if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
421                                                         chan = atoi (pathstr.substr (pos+1));
422                                                         pathstr = shorter;
423                                                         keeppath = fullpath;
424                                                         ++cnt;
425                                                 }
426
427                                         } else {
428
429                                                 /* new derived file (e.g. for timefx) being created in a newer session */
430
431                                         }
432                                 }
433
434                         } else {
435
436                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
437                                         keeppath = fullpath;
438                                         ++cnt;
439                                 }
440                         }
441                 }
442
443                 if (cnt > 1) {
444
445                         error << string_compose (
446                                         _("FileSource: \"%1\" is ambigous when searching %2\n\t"),
447                                         pathstr, search_path) << endmsg;
448                         goto out;
449
450                 } else if (cnt == 0) {
451
452                         if (must_exist) {
453                                 error << string_compose(
454                                                 _("Filesource: cannot find required file (%1): while searching %2"),
455                                                 pathstr, search_path) << endmsg;
456                                 goto out;
457                         } else {
458                                 isnew = true;
459                         }
460                 }
461
462                 /* Current find() is unable to parse relative path names to yet non-existant
463                    sources. QuickFix(tm) */
464                 if (keeppath == "") {
465                         if (must_exist) {
466                                 error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
467                         } else {
468                                 keeppath = pathstr;
469                         }
470                 }
471
472                 found_path = keeppath;
473
474                 ret = true;
475
476         } else {
477
478                 /* external files and/or very very old style sessions include full paths */
479
480                 /* ugh, handle ':' situation */
481
482                 if ((pos = pathstr.find_last_of (':')) != string::npos) {
483
484                         string shorter = pathstr.substr (0, pos);
485
486                         if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
487                                 chan = atoi (pathstr.substr (pos+1));
488                                 pathstr = shorter;
489                         }
490                 }
491
492                 found_path = pathstr;
493
494                 if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
495
496                         /* file does not exist or we cannot read it */
497
498                         if (must_exist) {
499                                 error << string_compose(
500                                                 _("Filesource: cannot find required file (%1): %2"),
501                                                 path, strerror (errno)) << endmsg;
502                                 goto out;
503                         }
504
505                         if (errno != ENOENT) {
506                                 error << string_compose(
507                                                 _("Filesource: cannot check for existing file (%1): %2"),
508                                                 path, strerror (errno)) << endmsg;
509                                 goto out;
510                         }
511
512                         /* a new file */
513                         isnew = true;
514                         ret = true;
515
516                 } else {
517
518                         /* already exists */
519                         ret = true;
520                 }
521         }
522
523 out:
524         return ret;
525 }
526
527 void
528 FileSource::mark_immutable ()
529 {
530         /* destructive sources stay writable, and their other flags don't change.  */
531         if (!(_flags & Destructive)) {
532                 _flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
533         }
534 }
535
536 void
537 FileSource::mark_immutable_except_write ()
538 {
539         /* destructive sources stay writable, and their other flags don't change.  */
540         if (!(_flags & Destructive)) {
541                 _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
542         }
543 }
544
545 void
546 FileSource::mark_nonremovable ()
547 {
548         _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
549 }
550
551 void
552 FileSource::set_within_session_from_path (const std::string& path)
553 {
554         _within_session = _session.path_is_within_session (path);
555 }
556
557 void
558 FileSource::set_path (const std::string& newpath)
559 {
560         _path = newpath;
561 }
562
563 void
564 FileSource::inc_use_count ()
565 {
566         Source::inc_use_count ();
567 }
568
569 bool
570 FileSource::is_stub () const
571 {
572         if (!empty()) {
573                 return false;
574         }
575         
576         if (!removable()) {
577                 return false;
578         }
579
580         if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
581                 return false;
582         }
583
584         return true;
585 }
586