writable session patch, forward ported from 2.X
[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/mountpoint.h"
32 #include "pbd/stl_delete.h"
33 #include "pbd/strsplit.h"
34 #include "pbd/shortpath.h"
35 #include "pbd/enumwriter.h"
36
37 #include <glibmm/miscutils.h>
38 #include <glibmm/fileutils.h>
39 #include <glibmm/thread.h>
40
41 #include "ardour/file_source.h"
42 #include "ardour/session.h"
43 #include "ardour/session_directory.h"
44 #include "ardour/source_factory.h"
45 #include "ardour/filename_extensions.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 static const std::string PATH_SEP = "/"; // I don't do windows
55
56 map<DataType, ustring> FileSource::search_paths;
57
58 FileSource::FileSource (Session& session, DataType type,
59                 const ustring& path, bool embedded, Source::Flag flag)
60         : Source(session, type, path, flag)
61         , _path(path)
62         , _file_is_new(true)
63         , _channel (0)
64         , _is_embedded(embedded)
65 {
66 }
67
68 FileSource::FileSource (Session& session, const XMLNode& node, bool /*must_exist*/)
69         : Source(session, node)
70         , _file_is_new (false)
71 {
72         _path = _name;
73         _is_embedded = (_path.find(PATH_SEP) != string::npos);
74 }
75
76 bool
77 FileSource::removable () const
78 {
79         return (_flags & Removable)
80                 && (   (_flags & RemoveAtDestroy)
81                         || ((_flags & RemovableIfEmpty) && length(timeline_position()) == 0));
82 }
83
84 int
85 FileSource::init (const ustring& pathstr, bool must_exist)
86 {
87         _timeline_position = 0;
88
89         if (!find (_type, pathstr, must_exist, _file_is_new, _channel, _path)) {
90                 throw MissingSource ();
91         }
92
93         /* XXX is this necessary? or even wise? */
94
95         if (_is_embedded) {
96                 _name = Glib::path_get_basename (_name);
97         }
98
99         if (_file_is_new && must_exist) {
100                 return -1;
101         }
102         
103         return 0;
104 }
105
106 int
107 FileSource::set_state (const XMLNode& node)
108 {
109         const XMLProperty* prop;
110
111         if ((prop = node.property (X_("channel"))) != 0) {
112                 _channel = atoi (prop->value());
113         } else {
114                 _channel = 0;
115         }
116
117         _is_embedded = (_name.find(PATH_SEP) == string::npos);
118
119         return 0;
120 }
121
122 void
123 FileSource::mark_take (const ustring& id)
124 {
125         if (writable ()) {
126                 _take_id = id;
127         }
128 }
129
130 int
131 FileSource::move_to_trash (const ustring& trash_dir_name)
132 {
133         if (is_embedded()) {
134                 cerr << "tried to move an embedded region to trash" << endl;
135                 return -1;
136         }
137
138         if (!writable()) {
139                 return -1;
140         }
141
142         /* don't move the file across filesystems, just stick it in the
143            trash_dir_name directory on whichever filesystem it was already on
144         */
145         
146         ustring newpath;
147         newpath = Glib::path_get_dirname (_path);
148         newpath = Glib::path_get_dirname (newpath); 
149
150         newpath += string(PATH_SEP) + trash_dir_name + PATH_SEP;
151         newpath += Glib::path_get_basename (_path);
152
153         /* the new path already exists, try versioning */
154         if (access (newpath.c_str(), F_OK) == 0) {
155                 char buf[PATH_MAX+1];
156                 int version = 1;
157                 ustring newpath_v;
158
159                 snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), version);
160                 newpath_v = buf;
161
162                 while (access (newpath_v.c_str(), F_OK) == 0 && version < 999) {
163                         snprintf (buf, sizeof (buf), "%s.%d", newpath.c_str(), ++version);
164                         newpath_v = buf;
165                 }
166                 
167                 if (version == 999) {
168                         PBD::error << string_compose (
169                                         _("there are already 1000 files with names like %1; versioning discontinued"),
170                                         newpath) << endmsg;
171                 } else {
172                         newpath = newpath_v;
173                 }
174         }
175
176         if (::rename (_path.c_str(), newpath.c_str()) != 0) {
177                 PBD::error << string_compose (
178                                 _("cannot rename file source from %1 to %2 (%3)"),
179                                 _path, newpath, strerror (errno)) << endmsg;
180                 return -1;
181         }
182
183         if (move_dependents_to_trash() != 0) {
184                 /* try to back out */
185                 rename (newpath.c_str(), _path.c_str());
186                 return -1;
187         }
188             
189         _path = newpath;
190         
191         /* file can not be removed twice, since the operation is not idempotent */
192         _flags = Flag (_flags & ~(RemoveAtDestroy|Removable|RemovableIfEmpty));
193
194         return 0;
195 }
196
197 /** Find the actual source file based on \a filename.
198  * 
199  * If the source is embedded, \a filename should be a simple filename (no slashes).
200  * If the source is external, \a filename should be a full path.
201  * In either case, found_path is set to the complete absolute path of the source file.
202  * \return true iff the file was found.
203  */
204 bool
205 FileSource::find (DataType type, const ustring& path, bool must_exist,
206                   bool& isnew, uint16_t& chan, ustring& found_path)
207 {
208         Glib::ustring search_path = search_paths[type];
209
210         ustring pathstr = path;
211         ustring::size_type pos;
212         bool ret = false;
213
214         isnew = false;
215
216         if (pathstr[0] != '/') {
217
218                 /* non-absolute pathname: find pathstr in search path */
219
220                 vector<ustring> dirs;
221                 int cnt;
222                 ustring fullpath;
223                 ustring keeppath;
224
225                 if (search_path.length() == 0) {
226                         error << _("FileSource: search path not set") << endmsg;
227                         goto out;
228                 }
229
230                 split (search_path, dirs, ':');
231
232                 cnt = 0;
233                 
234                 for (vector<ustring>::iterator i = dirs.begin(); i != dirs.end(); ++i) {
235                         fullpath = *i;
236                         if (fullpath[fullpath.length()-1] != '/') {
237                                 fullpath += '/';
238                         }
239
240                         fullpath += pathstr;
241
242                         /* i (paul) made a nasty design error by using ':' as a special character in
243                            Ardour 0.99 .. this hack tries to make things sort of work.
244                         */
245                         
246                         if ((pos = pathstr.find_last_of (':')) != ustring::npos) {
247                                 
248                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
249
250                                         /* its a real file, no problem */
251                                         
252                                         keeppath = fullpath;
253                                         ++cnt;
254
255                                 } else {
256                                         
257                                         if (must_exist) {
258                                                 
259                                                 /* might be an older session using file:channel syntax. see if the version
260                                                    without the :suffix exists
261                                                  */
262                                                 
263                                                 ustring shorter = pathstr.substr (0, pos);
264                                                 fullpath = *i;
265
266                                                 if (fullpath[fullpath.length()-1] != '/') {
267                                                         fullpath += '/';
268                                                 }
269
270                                                 fullpath += shorter;
271
272                                                 if (Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
273                                                         chan = atoi (pathstr.substr (pos+1));
274                                                         pathstr = shorter;
275                                                         keeppath = fullpath;
276                                                         ++cnt;
277                                                 } 
278                                                 
279                                         } else {
280                                                 
281                                                 /* new derived file (e.g. for timefx) being created in a newer session */
282                                                 
283                                         }
284                                 }
285
286                         } else {
287
288                                 if (Glib::file_test (fullpath, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
289                                         keeppath = fullpath;
290                                         ++cnt;
291                                 } 
292                         }
293                 }
294
295                 if (cnt > 1) {
296
297                         error << string_compose (
298                                         _("FileSource: \"%1\" is ambigous when searching %2\n\t"),
299                                         pathstr, search_path) << endmsg;
300                         goto out;
301
302                 } else if (cnt == 0) {
303
304                         if (must_exist) {
305                                 error << string_compose(
306                                                 _("Filesource: cannot find required file (%1): while searching %2"),
307                                                 pathstr, search_path) << endmsg;
308                                 goto out;
309                         } else {
310                                 isnew = true;
311                         }
312                 }
313
314                 /* Current find() is unable to parse relative path names to yet non-existant
315                    sources. QuickFix(tm) */
316                 if (keeppath == "") {
317                         if (must_exist) {
318                                 error << "FileSource::find(), keeppath = \"\", but the file must exist" << endl;
319                         } else {
320                                 keeppath = pathstr;
321                         }
322                 }
323
324                 found_path = keeppath;
325                 
326                 ret = true;
327
328         } else {
329                 
330                 /* external files and/or very very old style sessions include full paths */
331
332                 /* ugh, handle ':' situation */
333
334                 if ((pos = pathstr.find_last_of (':')) != ustring::npos) {
335                         
336                         ustring shorter = pathstr.substr (0, pos);
337
338                         if (Glib::file_test (shorter, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
339                                 chan = atoi (pathstr.substr (pos+1));
340                                 pathstr = shorter;
341                         }
342                 }
343                 
344                 found_path = pathstr;
345
346                 if (!Glib::file_test (pathstr, Glib::FILE_TEST_EXISTS|Glib::FILE_TEST_IS_REGULAR)) {
347
348                         /* file does not exist or we cannot read it */
349                         
350                         if (must_exist) {
351                                 error << string_compose(
352                                                 _("Filesource: cannot find required file (%1): %2"),
353                                                 path, strerror (errno)) << endmsg;
354                                 goto out;
355                         }
356                         
357                         if (errno != ENOENT) {
358                                 error << string_compose(
359                                                 _("Filesource: cannot check for existing file (%1): %2"),
360                                                 path, strerror (errno)) << endmsg;
361                                 goto out;
362                         }
363                         
364                         /* a new file */
365                         isnew = true;
366                         ret = true;
367
368                 } else {
369                         
370                         /* already exists */
371                         ret = true;
372                 }
373         }
374         
375 out:
376         return ret;
377 }
378
379 int
380 FileSource::set_source_name (const ustring& newname, bool destructive)
381 {
382         Glib::Mutex::Lock lm (_lock);
383         ustring oldpath = _path;
384         ustring newpath = Session::change_source_path_by_name (oldpath, _name, newname, destructive);
385         
386         if (newpath.empty()) {
387                 error << string_compose (_("programming error: %1"), "cannot generate a changed file path") << endmsg;
388                 return -1;
389         }
390
391         // Test whether newpath exists, if yes notify the user but continue. 
392         if (access(newpath.c_str(),F_OK) == 0) {
393                 error << _("Programming error! Ardour tried to rename a file over another file! It's safe to continue working, but please report this to the developers.") << endmsg;
394                 return -1;
395         }
396
397         if (rename (oldpath.c_str(), newpath.c_str()) != 0) {
398                 error << string_compose (_("cannot rename audio file %1 to %2"), _name, newpath) << endmsg;
399                 return -1;
400         }
401
402         _name = Glib::path_get_basename (newpath);
403         _path = newpath;
404
405         return 0;
406 }
407
408 void
409 FileSource::set_search_path (DataType type, const ustring& p)
410 {
411         search_paths[type] = p;
412 }
413
414 void
415 FileSource::mark_immutable ()
416 {
417         /* destructive sources stay writable, and their other flags don't change.  */
418         if (!(_flags & Destructive)) {
419                 _flags = Flag (_flags & ~(Writable|Removable|RemovableIfEmpty|RemoveAtDestroy|CanRename));
420         }
421 }
422