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 
FileSource(Session & session,DataType type,const string & path,const string & origin,Source::Flag flag)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 
FileSource(Session & session,const XMLNode & node,bool)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 
~FileSource()87 FileSource::~FileSource()
88 {
89 }
90 
91 void
existence_check()92 FileSource::existence_check ()
93 {
94         if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
95 		prevent_deletion ();
96 	}
97 }
98 
99 void
prevent_deletion()100 FileSource::prevent_deletion ()
101 {
102 	mark_immutable ();
103 }
104 
105 bool
removable() const106 FileSource::removable () const
107 {
108         bool r = ((_flags & Removable)
109                   && ((_flags & RemoveAtDestroy) ||
110                       ((_flags & RemovableIfEmpty) && empty())));
111 
112         return r;
113 }
114 
115 int
init(const string & pathstr,bool must_exist)116 FileSource::init (const string& pathstr, bool must_exist)
117 {
118 	if (Stateful::loading_state_version < 3000) {
119 		if (!find_2X (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
120 			throw MissingSource (pathstr, _type);
121 		}
122 	} else {
123 		if (!find (_session, _type, pathstr, must_exist, _file_is_new, _channel, _path)) {
124 			throw MissingSource (pathstr, _type);
125 		}
126 	}
127 
128 	set_within_session_from_path (_path);
129 
130         _name = Glib::path_get_basename (_path);
131 
132 	if (must_exist) {
133 		if (!Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
134 			throw MissingSource (pathstr, _type);
135 		}
136 	}
137 
138 	return 0;
139 }
140 
141 int
set_state(const XMLNode & node,int)142 FileSource::set_state (const XMLNode& node, int /*version*/)
143 {
144 	if (!node.get_property (X_("channel"), _channel)) {
145 		_channel = 0;
146 	}
147 
148 	node.get_property (X_("origin"), _origin);
149 
150 	if (!node.get_property (X_("gain"), _gain)) {
151 		_gain = 1.f;
152 	}
153 
154 	return 0;
155 }
156 
157 void
mark_take(const string & id)158 FileSource::mark_take (const string& id)
159 {
160 	if (writable ()) {
161 		_take_id = id;
162 	}
163 }
164 
165 int
move_to_trash(const string & trash_dir_name)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 (::g_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, g_strerror (errno)) << endmsg;
211 		return -1;
212 	}
213 
214 	if (move_dependents_to_trash() != 0) {
215 		/* try to back out */
216 		::g_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 path should be a simple filename (no slashes).
231  * If the source is external, \a path 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 if the file was found.
234  */
235 bool
find(Session & s,DataType type,const string & path,bool must_exist,bool & isnew,uint16_t &,string & found_path)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> hits;
246                 string fullpath;
247 		std::vector<std::string> dirs = s.source_search_path (type);
248 
249                 if (dirs.size() == 0) {
250                         error << _("FileSource: search path not set") << endmsg;
251                         goto out;
252                 }
253 
254                 for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
255 
256                         fullpath = Glib::build_filename (*i, path);
257 
258                         if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
259                                 keeppath = fullpath;
260                                 hits.push_back (fullpath);
261                         }
262                 }
263 
264 		/* Remove duplicate inodes from the list of ambiguous files, since if there are symlinks
265 		   in the session path it is possible to arrive at the same file via more than one path.
266 
267 		   I suppose this is not necessary on Windows.
268 		*/
269 
270 		vector<string> de_duped_hits;
271 
272 		for (vector<string>::iterator i = hits.begin(); i != hits.end(); ++i) {
273 
274 			vector<string>::iterator j = i;
275 			++j;
276 
277 			while (j != hits.end()) {
278 				if (PBD::equivalent_paths (*i, *j)) {
279 					/* *i and *j are the same file; break out of the loop early */
280 					break;
281 				}
282 
283 				++j;
284 			}
285 
286 			if (j == hits.end ()) {
287 				de_duped_hits.push_back (*i);
288 			}
289 		}
290 
291                 if (de_duped_hits.size() > 1) {
292 
293 			/* more than one match: ask the user */
294 
295                         int which = FileSource::AmbiguousFileName (path, de_duped_hits).value_or (-1);
296 
297                         if (which < 0) {
298                                 goto out;
299                         } else {
300                                 keeppath = de_duped_hits[which];
301                         }
302 
303                 } else if (de_duped_hits.size() == 0) {
304 
305 			/* no match: error */
306 
307                         if (must_exist) {
308 				/* do not generate an error here, leave that to
309 				   whoever deals with the false return value.
310 				*/
311                                 goto out;
312                         } else {
313                                 isnew = true;
314                         }
315                 } else {
316 
317 			/* only one match: happy days */
318 
319 			keeppath = de_duped_hits[0];
320 		}
321 
322        } else {
323                 keeppath = path;
324         }
325 
326         /* Current find() is unable to parse relative path names to yet non-existant
327            sources. QuickFix(tm)
328         */
329 
330 	if (keeppath.empty()) {
331 		if (must_exist) {
332                         error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
333                 } else {
334                         keeppath = path;
335                 }
336         }
337 
338         found_path = keeppath;
339         ret = true;
340 
341   out:
342 	return ret;
343 }
344 
345 /** Find the actual source file based on \a filename.
346  *
347  * If the source is within the session tree, \a filename should be a simple filename (no slashes).
348  * If the source is external, \a filename should be a full path.
349  * In either case, found_path is set to the complete absolute path of the source file.
350  * \return true iff the file was found.
351  */
352 bool
find_2X(Session & s,DataType type,const string & path,bool must_exist,bool & isnew,uint16_t & chan,string & found_path)353 FileSource::find_2X (Session& s, DataType type, const string& path, bool must_exist,
354                      bool& isnew, uint16_t& chan, string& found_path)
355 {
356 	string pathstr = path;
357 	string::size_type pos;
358 	bool ret = false;
359 
360 	isnew = false;
361 
362 	if (!Glib::path_is_absolute (pathstr)) {
363 
364 		/* non-absolute pathname: find pathstr in search path */
365 
366 		vector<string> dirs = s.source_search_path (type);
367 
368 		int cnt;
369 		string fullpath;
370 		string keeppath;
371 
372 		if (dirs.size() == 0) {
373 			error << _("FileSource: search path not set") << endmsg;
374 			goto out;
375 		}
376 
377 		cnt = 0;
378 
379 		for (vector<string>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
380 
381                         fullpath = Glib::build_filename (*i, pathstr);
382 
383 			/* i (paul) made a nasty design error by using ':' as a special character in
384 			   Ardour 0.99 .. this hack tries to make things sort of work.
385 			*/
386 
387 			if ((pos = pathstr.find_last_of (':')) != string::npos) {
388 
389 				if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
390 
391 					/* its a real file, no problem */
392 
393 					keeppath = fullpath;
394 					++cnt;
395 
396 				} else {
397 
398 					if (must_exist) {
399 
400 						/* might be an older session using file:channel syntax. see if the version
401 						   without the :suffix exists
402 						 */
403 
404 						string shorter = pathstr.substr (0, pos);
405                                                 fullpath = Glib::build_filename (*i, shorter);
406 
407 						if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
408 							chan = atoi (pathstr.substr (pos+1));
409 							pathstr = shorter;
410 							keeppath = fullpath;
411 							++cnt;
412 						}
413 
414 					} else {
415 
416 						/* new derived file (e.g. for timefx) being created in a newer session */
417 
418 					}
419 				}
420 
421 			} else {
422 
423 				if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
424 					keeppath = fullpath;
425 					++cnt;
426 				}
427 			}
428 		}
429 
430 		if (cnt > 1) {
431 
432 			error << string_compose (
433 					_("FileSource: \"%1\" is ambiguous when searching\n\t"), pathstr) << endmsg;
434 			goto out;
435 
436 		} else if (cnt == 0) {
437 
438 			if (must_exist) {
439 				error << string_compose(
440 						_("Filesource: cannot find required file (%1)"), pathstr) << endmsg;
441 				goto out;
442 			} else {
443 				isnew = true;
444 			}
445 		}
446 
447 		/* Current find() is unable to parse relative path names to yet non-existant
448 		   sources. QuickFix(tm) */
449 		if (keeppath == "") {
450 			if (must_exist) {
451 				error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
452 			} else {
453 				keeppath = pathstr;
454 			}
455 		}
456 
457 		found_path = keeppath;
458 
459 		ret = true;
460 
461 	} else {
462 
463 		/* external files and/or very very old style sessions include full paths */
464 
465 		/* ugh, handle ':' situation */
466 
467 		if ((pos = pathstr.find_last_of (':')) != string::npos) {
468 
469 			string shorter = pathstr.substr (0, pos);
470 
471 			if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
472 				chan = atoi (pathstr.substr (pos+1));
473 				pathstr = shorter;
474 			}
475 		}
476 
477 		found_path = pathstr;
478 
479 		if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
480 
481 			/* file does not exist or we cannot read it */
482 
483 			if (must_exist) {
484 				error << string_compose(
485 						_("Filesource: cannot find required file (%1): %2"),
486 						path, g_strerror (errno)) << endmsg;
487 				goto out;
488 			}
489 
490 #ifndef PLATFORM_WINDOWS
491 			if (errno != ENOENT) {
492 				error << string_compose(
493 						_("Filesource: cannot check for existing file (%1): %2"),
494 						path, g_strerror (errno)) << endmsg;
495 				goto out;
496 			}
497 #endif
498 			/* a new file */
499 			isnew = true;
500 			ret = true;
501 
502 		} else {
503 
504 			/* already exists */
505 			ret = true;
506 		}
507 	}
508 
509 out:
510 	return ret;
511 }
512 
513 void
mark_immutable()514 FileSource::mark_immutable ()
515 {
516 	_flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
517 	close();
518 }
519 
520 void
mark_immutable_except_write()521 FileSource::mark_immutable_except_write ()
522 {
523 	_flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
524 }
525 
526 void
mark_nonremovable()527 FileSource::mark_nonremovable ()
528 {
529         _flags = Flag (_flags & ~(Removable|RemovableIfEmpty|RemoveAtDestroy));
530 }
531 
532 void
set_within_session_from_path(const std::string & path)533 FileSource::set_within_session_from_path (const std::string& path)
534 {
535 	_within_session = _session.path_is_within_session (path);
536 }
537 
538 void
set_path(const std::string & newpath)539 FileSource::set_path (const std::string& newpath)
540 {
541 	close ();
542         _path = newpath;
543 	set_within_session_from_path (newpath);
544 	if (_within_session) {
545 		_origin = Glib::path_get_basename (newpath);
546 	} else {
547 		_origin = newpath;
548 	}
549 }
550 
551 
552 void
replace_file(const std::string & newpath)553 FileSource::replace_file (const std::string& newpath)
554 {
555 	close ();
556 	_path = newpath;
557 	_name = Glib::path_get_basename (newpath);
558 }
559 
560 void
inc_use_count()561 FileSource::inc_use_count ()
562 {
563         Source::inc_use_count ();
564 }
565 
566 bool
is_stub() const567 FileSource::is_stub () const
568 {
569 	if (!empty()) {
570 		return false;
571 	}
572 
573 	if (!removable()) {
574 		return false;
575 	}
576 
577 	if (Glib::file_test (_path, Glib::FILE_TEST_EXISTS)) {
578 		return false;
579 	}
580 
581 	return true;
582 }
583 
584 int
rename(const string & newpath)585 FileSource::rename (const string& newpath)
586 {
587 	Glib::Threads::Mutex::Lock lm (_lock);
588 	string oldpath = _path;
589 
590 	// Test whether newpath exists, if yes notify the user but continue.
591 	if (Glib::file_test (newpath, Glib::FILE_TEST_EXISTS)) {
592 		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;
593 		return -1;
594 	}
595 
596 	if (Glib::file_test (oldpath.c_str(), Glib::FILE_TEST_EXISTS)) {
597 		/* rename only needed if file exists on disk */
598 		if (::g_rename (oldpath.c_str(), newpath.c_str()) != 0) {
599 			error << string_compose (_("cannot rename file %1 to %2 (%3)"), oldpath, newpath, g_strerror(errno)) << endmsg;
600 			return -1;
601 		}
602 	}
603 
604 	_name = Glib::path_get_basename (newpath);
605 	_path = newpath;
606 
607 	return 0;
608 }
609 
610 
611