Fixup prev commit (LV2 X11 UI) -- #7837
[ardour.git] / libs / ardour / file_source.cc
1 /*
2  * Copyright (C) 2009-2012 Carl Hetherington <carl@carlh.net>
3  * Copyright (C) 2009-2014 David Robillard <d@drobilla.net>
4  * Copyright (C) 2009-2016 Paul Davis <paul@linuxaudiosystems.com>
5  * Copyright (C) 2012-2017 Tim Mayberry <mojofunk@gmail.com>
6  * Copyright (C) 2015-2018 Robin Gareus <robin@gareus.org>
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License along
19  * with this program; if not, write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21  */
22
23 #include <vector>
24
25 #include <sys/time.h>
26 #include <sys/stat.h>
27 #include <unistd.h>
28 #include <fcntl.h>
29 #include <errno.h>
30
31 #include <glib.h>
32 #include <glib/gstdio.h>
33
34 #include "pbd/convert.h"
35 #include "pbd/basename.h"
36 #include "pbd/stl_delete.h"
37 #include "pbd/strsplit.h"
38 #include "pbd/shortpath.h"
39 #include "pbd/enumwriter.h"
40 #include "pbd/file_utils.h"
41
42 #include <glibmm/miscutils.h>
43 #include <glibmm/fileutils.h>
44 #include <glibmm/threads.h>
45
46 #include "ardour/data_type.h"
47 #include "ardour/file_source.h"
48 #include "ardour/session.h"
49 #include "ardour/source.h"
50 #include "ardour/utils.h"
51
52 #include "pbd/i18n.h"
53
54 using namespace std;
55 using namespace ARDOUR;
56 using namespace PBD;
57 using namespace Glib;
58
59 PBD::Signal2<int,std::string,std::vector<std::string> > FileSource::AmbiguousFileName;
60
61 FileSource::FileSource (Session& session, DataType type, const string& path, const string& origin, Source::Flag flag)
62         : Source(session, type, path, flag)
63         , _path (path)
64         , _file_is_new (!origin.empty()) // if origin is left unspecified (empty string) then file must exist
65         , _channel (0)
66         , _origin (origin)
67         , _gain (1.f)
68 {
69         set_within_session_from_path (path);
70 }
71
72 FileSource::FileSource (Session& session, const XMLNode& node, bool /*must_exist*/)
73         : Source (session, node)
74         , _file_is_new (false)
75         , _channel (0)
76         , _gain (1.f)
77 {
78         /* this setting of _path is temporary - we expect derived classes
79            to call ::init() which will actually locate the file
80            and reset _path and _within_session correctly.
81         */
82
83         _path = _name;
84         _within_session = true;
85 }
86
87 FileSource::~FileSource()
88 {
89 }
90
91 void
92 FileSource::existence_check ()
93 {
94         if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
95                 prevent_deletion ();
96         }
97 }
98
99 void
100 FileSource::prevent_deletion ()
101 {
102         if (!(_flags & Destructive)) {
103                 mark_immutable ();
104         } else {
105                 _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
106         }
107 }
108
109 bool
110 FileSource::removable () const
111 {
112         bool r = ((_flags & Removable)
113                   && ((_flags & RemoveAtDestroy) ||
114                       ((_flags & RemovableIfEmpty) && empty())));
115
116         return r;
117 }
118
119 int
120 FileSource::init (const string& pathstr, bool must_exist)
121 {
122         if (Stateful::loading_state_version < 3000) {
123                 if (!find_2X (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
124                         throw MissingSource (pathstr, _type);
125                 }
126         } else {
127                 if (!find (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
128                         throw MissingSource (pathstr, _type);
129                 }
130         }
131
132         set_within_session_from_path (_path);
133
134         _name = Glib::path_get_basename (_path);
135
136         if (must_exist) {
137                 if (!Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
138                         throw MissingSource (pathstr, _type);
139                 }
140         }
141
142         return 0;
143 }
144
145 int
146 FileSource::set_state (const XMLNode& node, int /*version*/)
147 {
148         if (!node.get_property (X_("channel"), _channel)) {
149                 _channel = 0;
150         }
151
152         node.get_property (X_("origin"), _origin);
153
154         if (!node.get_property (X_("gain"), _gain)) {
155                 _gain = 1.f;
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 (::g_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, g_strerror (errno)) << endmsg;
215                 return -1;
216         }
217
218         if (move_dependents_to_trash() != 0) {
219                 /* try to back out */
220                 ::g_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 path should be a simple filename (no slashes).
235  * If the source is external, \a path 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 if 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                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
259
260                         fullpath = Glib::build_filename (*i, path);
261
262                         if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
263                                 keeppath = fullpath;
264                                 hits.push_back (fullpath);
265                         }
266                 }
267
268                 /* Remove duplicate inodes from the list of ambiguous files, since if there are symlinks
269                    in the session path it is possible to arrive at the same file via more than one path.
270
271                    I suppose this is not necessary on Windows.
272                 */
273
274                 vector<string> de_duped_hits;
275
276                 for (vector<string>::iterator i = hits.begin(); i != hits.end(); ++i) {
277
278                         vector<string>::iterator j = i;
279                         ++j;
280
281                         while (j != hits.end()) {
282                                 if (PBD::equivalent_paths (*i, *j)) {
283                                         /* *i and *j are the same file; break out of the loop early */
284                                         break;
285                                 }
286
287                                 ++j;
288                         }
289
290                         if (j == hits.end ()) {
291                                 de_duped_hits.push_back (*i);
292                         }
293                 }
294
295                 if (de_duped_hits.size() > 1) {
296
297                         /* more than one match: ask the user */
298
299                         int which = FileSource::AmbiguousFileName (path, de_duped_hits).get_value_or (-1);
300
301                         if (which < 0) {
302                                 goto out;
303                         } else {
304                                 keeppath = de_duped_hits[which];
305                         }
306
307                 } else if (de_duped_hits.size() == 0) {
308
309                         /* no match: error */
310
311                         if (must_exist) {
312                                 /* do not generate an error here, leave that to
313                                    whoever deals with the false return value.
314                                 */
315                                 goto out;
316                         } else {
317                                 isnew = true;
318                         }
319                 } else {
320
321                         /* only one match: happy days */
322
323                         keeppath = de_duped_hits[0];
324                 }
325
326        } else {
327                 keeppath = path;
328         }
329
330         /* Current find() is unable to parse relative path names to yet non-existant
331            sources. QuickFix(tm)
332         */
333
334         if (keeppath.empty()) {
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         ret = true;
344
345   out:
346         return ret;
347 }
348
349 /** Find the actual source file based on \a filename.
350  *
351  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
352  * If the source is external, \a filename should be a full path.
353  * In either case, found_path is set to the complete absolute path of the source file.
354  * \return true iff the file was found.
355  */
356 bool
357 FileSource::find_2X (Session& s, DataType type, const string& path, bool must_exist,
358                      bool& isnew, uint16_t& chan, string& found_path)
359 {
360         string pathstr = path;
361         string::size_type pos;
362         bool ret = false;
363
364         isnew = false;
365
366         if (!Glib::path_is_absolute (pathstr)) {
367
368                 /* non-absolute pathname: find pathstr in search path */
369
370                 vector<string> dirs = s.source_search_path (type);
371
372                 int cnt;
373                 string fullpath;
374                 string keeppath;
375
376                 if (dirs.size() == 0) {
377                         error << _("FileSource: search path not set") << endmsg;
378                         goto out;
379                 }
380
381                 cnt = 0;
382
383                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
384
385                         fullpath = Glib::build_filename (*i, pathstr);
386
387                         /* i (paul) made a nasty design error by using ':' as a special character in
388                            Ardour 0.99 .. this hack tries to make things sort of work.
389                         */
390
391                         if ((pos = pathstr.find_last_of (':')) != string::npos) {
392
393                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
394
395                                         /* its a real file, no problem */
396
397                                         keeppath = fullpath;
398                                         ++cnt;
399
400                                 } else {
401
402                                         if (must_exist) {
403
404                                                 /* might be an older session using file:channel syntax. see if the version
405                                                    without the :suffix exists
406                                                  */
407
408                                                 string shorter = pathstr.substr (0, pos);
409                                                 fullpath = Glib::build_filename (*i, shorter);
410
411                                                 if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
412                                                         chan = atoi (pathstr.substr (pos+1));
413                                                         pathstr = shorter;
414                                                         keeppath = fullpath;
415                                                         ++cnt;
416                                                 }
417
418                                         } else {
419
420                                                 /* new derived file (e.g. for timefx) being created in a newer session */
421
422                                         }
423                                 }
424
425                         } else {
426
427                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
428                                         keeppath = fullpath;
429                                         ++cnt;
430                                 }
431                         }
432                 }
433
434                 if (cnt > 1) {
435
436                         error << string_compose (
437                                         _("FileSource: \"%1\" is ambiguous when searching\n\t"), pathstr) << endmsg;
438                         goto out;
439
440                 } else if (cnt == 0) {
441
442                         if (must_exist) {
443                                 error << string_compose(
444                                                 _("Filesource: cannot find required file (%1)"), pathstr) << endmsg;
445                                 goto out;
446                         } else {
447                                 isnew = true;
448                         }
449                 }
450
451                 /* Current find() is unable to parse relative path names to yet non-existant
452                    sources. QuickFix(tm) */
453                 if (keeppath == "") {
454                         if (must_exist) {
455                                 error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
456                         } else {
457                                 keeppath = pathstr;
458                         }
459                 }
460
461                 found_path = keeppath;
462
463                 ret = true;
464
465         } else {
466
467                 /* external files and/or very very old style sessions include full paths */
468
469                 /* ugh, handle ':' situation */
470
471                 if ((pos = pathstr.find_last_of (':')) != string::npos) {
472
473                         string shorter = pathstr.substr (0, pos);
474
475                         if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
476                                 chan = atoi (pathstr.substr (pos+1));
477                                 pathstr = shorter;
478                         }
479                 }
480
481                 found_path = pathstr;
482
483                 if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
484
485                         /* file does not exist or we cannot read it */
486
487                         if (must_exist) {
488                                 error << string_compose(
489                                                 _("Filesource: cannot find required file (%1): %2"),
490                                                 path, g_strerror (errno)) << endmsg;
491                                 goto out;
492                         }
493
494 #ifndef PLATFORM_WINDOWS
495                         if (errno != ENOENT) {
496                                 error << string_compose(
497                                                 _("Filesource: cannot check for existing file (%1): %2"),
498                                                 path, g_strerror (errno)) << endmsg;
499                                 goto out;
500                         }
501 #endif
502                         /* a new file */
503                         isnew = true;
504                         ret = true;
505
506                 } else {
507
508                         /* already exists */
509                         ret = true;
510                 }
511         }
512
513 out:
514         return ret;
515 }
516
517 void
518 FileSource::mark_immutable ()
519 {
520         /* destructive sources stay writable, and their other flags don't change.  */
521         if (!(_flags & Destructive)) {
522                 _flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
523                 close();
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         close ();
552         _path = newpath;
553         set_within_session_from_path (newpath);
554         if (_within_session) {
555                 _origin = Glib::path_get_basename (newpath);
556         } else {
557                 _origin = newpath;
558         }
559 }
560
561
562 void
563 FileSource::replace_file (const std::string& newpath)
564 {
565         close ();
566         _path = newpath;
567         _name = Glib::path_get_basename (newpath);
568 }
569
570 void
571 FileSource::inc_use_count ()
572 {
573         Source::inc_use_count ();
574 }
575
576 bool
577 FileSource::is_stub () const
578 {
579         if (!empty()) {
580                 return false;
581         }
582
583         if (!removable()) {
584                 return false;
585         }
586
587         if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
588                 return false;
589         }
590
591         return true;
592 }
593
594 int
595 FileSource::rename (const string& newpath)
596 {
597         Glib::Threads::Mutex::Lock lm (_lock);
598         string oldpath = _path;
599
600         // Test whether newpath exists, if yes notify the user but continue.
601         if (Glib::file_test (newpath, Glib::FILE_TEST_EXISTS)) {
602                 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;
603                 return -1;
604         }
605
606         if (Glib::file_test (oldpath.c_str(), Glib::FILE_TEST_EXISTS)) {
607                 /* rename only needed if file exists on disk */
608                 if (::g_rename (oldpath.c_str(), newpath.c_str()) != 0) {
609                         error << string_compose (_("cannot rename file %1 to %2 (%3)"), oldpath, newpath, g_strerror(errno)) << endmsg;
610                         return -1;
611                 }
612         }
613
614         _name = Glib::path_get_basename (newpath);
615         _path = newpath;
616
617         return 0;
618 }
619
620