comparison data/fileio/FileSource.cpp @ 317:c324d410b096

* RemoteFile -> FileSource now it's used all over the place for local files as well.
author Chris Cannam
date Thu, 18 Oct 2007 16:20:26 +0000
parents data/fileio/RemoteFile.cpp@3a6725f285d6
children 82a2d3161e14
comparison
equal deleted inserted replaced
316:3a6725f285d6 317:c324d410b096
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2
3 /*
4 Sonic Visualiser
5 An audio file viewer and annotation editor.
6 Centre for Digital Music, Queen Mary, University of London.
7 This file copyright 2007 QMUL.
8
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License as
11 published by the Free Software Foundation; either version 2 of the
12 License, or (at your option) any later version. See the file
13 COPYING included with this distribution for more information.
14 */
15
16 #include "FileSource.h"
17 #include "base/TempDirectory.h"
18 #include "base/Exceptions.h"
19
20 #include <QHttp>
21 #include <QFtp>
22 #include <QFileInfo>
23 #include <QDir>
24 #include <QApplication>
25 #include <QProgressDialog>
26 #include <QHttpResponseHeader>
27
28 #include <iostream>
29
30 int
31 FileSource::m_count = 0;
32
33 QMutex
34 FileSource::m_fileCreationMutex;
35
36 FileSource::RemoteRefCountMap
37 FileSource::m_refCountMap;
38
39 FileSource::RemoteLocalMap
40 FileSource::m_remoteLocalMap;
41
42 QMutex
43 FileSource::m_mapMutex;
44
45 FileSource::FileSource(QString fileOrUrl, bool showProgress) :
46 m_url(fileOrUrl),
47 m_ftp(0),
48 m_http(0),
49 m_localFile(0),
50 m_ok(false),
51 m_lastStatus(0),
52 m_remote(isRemote(fileOrUrl)),
53 m_done(false),
54 m_leaveLocalFile(false),
55 m_progressDialog(0),
56 m_progressShowTimer(this),
57 m_refCounted(false)
58 {
59 std::cerr << "FileSource::FileSource(" << fileOrUrl.toStdString() << ")" << std::endl;
60
61 if (!canHandleScheme(m_url)) {
62 std::cerr << "FileSource::FileSource: ERROR: Unsupported scheme in URL \"" << m_url.toString().toStdString() << "\"" << std::endl;
63 m_errorString = tr("Unsupported scheme in URL");
64 return;
65 }
66
67 init(showProgress);
68
69 if (isRemote() &&
70 (fileOrUrl.contains('%') ||
71 fileOrUrl.contains("--"))) { // for IDNA
72
73 waitForStatus();
74
75 if (!isAvailable()) {
76 // The URL was created on the assumption that the string
77 // was human-readable. Let's try again, this time
78 // assuming it was already encoded.
79 std::cerr << "FileSource::FileSource: Failed to retrieve URL \""
80 << fileOrUrl.toStdString()
81 << "\" as human-readable URL; "
82 << "trying again treating it as encoded URL"
83 << std::endl;
84 m_url.setEncodedUrl(fileOrUrl.toAscii());
85 init(showProgress);
86 }
87 }
88 }
89
90 FileSource::FileSource(QUrl url, bool showProgress) :
91 m_url(url),
92 m_ftp(0),
93 m_http(0),
94 m_localFile(0),
95 m_ok(false),
96 m_lastStatus(0),
97 m_remote(isRemote(url.toString())),
98 m_done(false),
99 m_leaveLocalFile(false),
100 m_progressDialog(0),
101 m_progressShowTimer(this),
102 m_refCounted(false)
103 {
104 std::cerr << "FileSource::FileSource(" << url.toString().toStdString() << ") [as url]" << std::endl;
105
106 if (!canHandleScheme(m_url)) {
107 std::cerr << "FileSource::FileSource: ERROR: Unsupported scheme in URL \"" << m_url.toString().toStdString() << "\"" << std::endl;
108 m_errorString = tr("Unsupported scheme in URL");
109 return;
110 }
111
112 init(showProgress);
113 }
114
115 FileSource::FileSource(const FileSource &rf) :
116 QObject(),
117 m_url(rf.m_url),
118 m_ftp(0),
119 m_http(0),
120 m_localFile(0),
121 m_ok(rf.m_ok),
122 m_lastStatus(rf.m_lastStatus),
123 m_remote(rf.m_remote),
124 m_done(false),
125 m_leaveLocalFile(false),
126 m_progressDialog(0),
127 m_progressShowTimer(0),
128 m_refCounted(false)
129 {
130 std::cerr << "FileSource::FileSource(" << m_url.toString().toStdString() << ") [copy ctor]" << std::endl;
131
132 if (!canHandleScheme(m_url)) {
133 std::cerr << "FileSource::FileSource: ERROR: Unsupported scheme in URL \"" << m_url.toString().toStdString() << "\"" << std::endl;
134 m_errorString = tr("Unsupported scheme in URL");
135 return;
136 }
137
138 if (!isRemote()) {
139 m_localFilename = rf.m_localFilename;
140 } else {
141 QMutexLocker locker(&m_mapMutex);
142 std::cerr << "FileSource::FileSource(copy ctor): ref count is "
143 << m_refCountMap[m_url] << std::endl;
144 if (m_refCountMap[m_url] > 0) {
145 m_refCountMap[m_url]++;
146 std::cerr << "raised it to " << m_refCountMap[m_url] << std::endl;
147 m_localFilename = m_remoteLocalMap[m_url];
148 m_refCounted = true;
149 } else {
150 m_ok = false;
151 m_lastStatus = 404;
152 }
153 }
154
155 m_done = true;
156 }
157
158 FileSource::~FileSource()
159 {
160 std::cerr << "FileSource(" << m_url.toString().toStdString() << ")::~FileSource" << std::endl;
161
162 cleanup();
163
164 if (isRemote() && !m_leaveLocalFile) deleteCacheFile();
165 }
166
167 void
168 FileSource::init(bool showProgress)
169 {
170 if (!isRemote()) {
171 m_localFilename = m_url.toLocalFile();
172 m_ok = true;
173 if (!QFileInfo(m_localFilename).exists()) {
174 m_lastStatus = 404;
175 } else {
176 m_lastStatus = 200;
177 }
178 m_done = true;
179 return;
180 }
181
182 if (createCacheFile()) {
183 std::cerr << "FileSource::init: Already have this one" << std::endl;
184 m_ok = true;
185 if (!QFileInfo(m_localFilename).exists()) {
186 m_lastStatus = 404;
187 } else {
188 m_lastStatus = 200;
189 }
190 m_done = true;
191 return;
192 }
193
194 if (m_localFilename == "") return;
195 m_localFile = new QFile(m_localFilename);
196 m_localFile->open(QFile::WriteOnly);
197
198 QString scheme = m_url.scheme().toLower();
199
200 std::cerr << "FileSource::init: Don't have local copy of \""
201 << m_url.toString().toStdString() << "\", retrieving" << std::endl;
202
203 if (scheme == "http") {
204 initHttp();
205 } else if (scheme == "ftp") {
206 initFtp();
207 } else {
208 m_remote = false;
209 m_ok = false;
210 }
211
212 if (m_ok) {
213
214 QMutexLocker locker(&m_mapMutex);
215
216 if (m_refCountMap[m_url] > 0) {
217 // someone else has been doing the same thing at the same time,
218 // but has got there first
219 cleanup();
220 m_refCountMap[m_url]++;
221 std::cerr << "FileSource::init: Another FileSource has got there first, abandoning our download and using theirs" << std::endl;
222 m_localFilename = m_remoteLocalMap[m_url];
223 m_refCounted = true;
224 m_ok = true;
225 if (!QFileInfo(m_localFilename).exists()) {
226 m_lastStatus = 404;
227 }
228 m_done = true;
229 return;
230 }
231
232 m_remoteLocalMap[m_url] = m_localFilename;
233 m_refCountMap[m_url]++;
234 m_refCounted = true;
235
236 if (showProgress) {
237 m_progressDialog = new QProgressDialog(tr("Downloading %1...").arg(m_url.toString()), tr("Cancel"), 0, 100);
238 m_progressDialog->hide();
239 connect(&m_progressShowTimer, SIGNAL(timeout()),
240 this, SLOT(showProgressDialog()));
241 connect(m_progressDialog, SIGNAL(canceled()), this, SLOT(cancelled()));
242 m_progressShowTimer.setSingleShot(true);
243 m_progressShowTimer.start(2000);
244 }
245 }
246 }
247
248 void
249 FileSource::initHttp()
250 {
251 m_ok = true;
252 m_http = new QHttp(m_url.host(), m_url.port(80));
253 connect(m_http, SIGNAL(done(bool)), this, SLOT(done(bool)));
254 connect(m_http, SIGNAL(dataReadProgress(int, int)),
255 this, SLOT(dataReadProgress(int, int)));
256 connect(m_http, SIGNAL(responseHeaderReceived(const QHttpResponseHeader &)),
257 this, SLOT(httpResponseHeaderReceived(const QHttpResponseHeader &)));
258
259 // I don't quite understand this. url.path() returns a path
260 // without percent encoding; for example, spaces appear as
261 // literal spaces. This generally won't work if sent to the
262 // server directly. You can retrieve a correctly encoded URL
263 // from QUrl using url.toEncoded(), but that gives you the
264 // whole URL; there doesn't seem to be any way to retrieve
265 // only an encoded path. Furthermore there doesn't seem to be
266 // any way to convert a retrieved path into an encoded path
267 // without explicitly specifying that you don't want the path
268 // separators ("/") to be encoded. (Besides being painful to
269 // manage, I don't see how this can work correctly in any case
270 // where a percent-encoded "/" is supposed to appear within a
271 // path element?) There also seems to be no way to retrieve
272 // the path plus query string, i.e. everything that I need to
273 // send to the HTTP server. And no way for QHttp to take a
274 // QUrl argument. I'm obviously missing something.
275
276 // So, two ways to do this: query the bits from the URL,
277 // encode them individually, and glue them back together
278 // again...
279 /*
280 QString path = QUrl::toPercentEncoding(m_url.path(), "/");
281 QList<QPair<QString, QString> > query = m_url.queryItems();
282 if (!query.empty()) {
283 QStringList q2;
284 for (QList<QPair<QString, QString> >::iterator i = query.begin();
285 i != query.end(); ++i) {
286 q2.push_back(QString("%1=%3")
287 .arg(QString(QUrl::toPercentEncoding(i->first)))
288 .arg(QString(QUrl::toPercentEncoding(i->second))));
289 }
290 path = QString("%1%2%3")
291 .arg(path).arg("?")
292 .arg(q2.join("&"));
293 }
294 */
295
296 // ...or, much simpler but relying on knowledge about the
297 // scheme://host/path/path/query etc format of the URL, we can
298 // get the whole URL ready-encoded and then split it on "/" as
299 // appropriate...
300
301 QString path = "/" + QString(m_url.toEncoded()).section('/', 3);
302
303 std::cerr << "FileSource: path is \""
304 << path.toStdString() << "\"" << std::endl;
305
306 m_http->get(path, m_localFile);
307 }
308
309 void
310 FileSource::initFtp()
311 {
312 m_ok = true;
313 m_ftp = new QFtp;
314 connect(m_ftp, SIGNAL(done(bool)), this, SLOT(done(bool)));
315 connect(m_ftp, SIGNAL(commandFinished(int, bool)),
316 this, SLOT(ftpCommandFinished(int, bool)));
317 connect(m_ftp, SIGNAL(dataTransferProgress(qint64, qint64)),
318 this, SLOT(dataTransferProgress(qint64, qint64)));
319 m_ftp->connectToHost(m_url.host(), m_url.port(21));
320
321 QString username = m_url.userName();
322 if (username == "") {
323 username = "anonymous";
324 }
325
326 QString password = m_url.password();
327 if (password == "") {
328 password = QString("%1@%2").arg(getenv("USER")).arg(getenv("HOST"));
329 }
330
331 m_ftp->login(username, password);
332
333 QString dirpath = m_url.path().section('/', 0, -2);
334 QString filename = m_url.path().section('/', -1);
335
336 if (dirpath == "") dirpath = "/";
337 m_ftp->cd(dirpath);
338 m_ftp->get(filename, m_localFile);
339 }
340
341 void
342 FileSource::cleanup()
343 {
344 m_done = true;
345 if (m_http) {
346 QHttp *h = m_http;
347 m_http = 0;
348 h->abort();
349 h->deleteLater();
350 }
351 if (m_ftp) {
352 QFtp *f = m_ftp;
353 m_ftp = 0;
354 f->abort();
355 f->deleteLater();
356 }
357 delete m_progressDialog;
358 m_progressDialog = 0;
359 delete m_localFile; // does not actually delete the file
360 m_localFile = 0;
361 }
362
363 bool
364 FileSource::isRemote(QString fileOrUrl)
365 {
366 QString scheme = QUrl(fileOrUrl).scheme().toLower();
367 return (scheme == "http" || scheme == "ftp");
368 }
369
370 bool
371 FileSource::canHandleScheme(QUrl url)
372 {
373 QString scheme = url.scheme().toLower();
374 return (scheme == "http" || scheme == "ftp" ||
375 scheme == "file" || scheme == "");
376 }
377
378 bool
379 FileSource::isAvailable()
380 {
381 waitForStatus();
382 bool available = true;
383 if (!m_ok) available = false;
384 else available = (m_lastStatus / 100 == 2);
385 std::cerr << "FileSource::isAvailable: " << (available ? "yes" : "no")
386 << std::endl;
387 return available;
388 }
389
390 void
391 FileSource::waitForStatus()
392 {
393 while (m_ok && (!m_done && m_lastStatus == 0)) {
394 // std::cerr << "waitForStatus: processing (last status " << m_lastStatus << ")" << std::endl;
395 QApplication::processEvents();
396 }
397 }
398
399 void
400 FileSource::waitForData()
401 {
402 while (m_ok && !m_done) {
403 QApplication::processEvents();
404 }
405 }
406
407 void
408 FileSource::setLeaveLocalFile(bool leave)
409 {
410 m_leaveLocalFile = leave;
411 }
412
413 bool
414 FileSource::isOK() const
415 {
416 return m_ok;
417 }
418
419 bool
420 FileSource::isDone() const
421 {
422 return m_done;
423 }
424
425 bool
426 FileSource::isRemote() const
427 {
428 return m_remote;
429 }
430
431 QString
432 FileSource::getLocation() const
433 {
434 return m_url.toString();
435 }
436
437 QString
438 FileSource::getLocalFilename() const
439 {
440 return m_localFilename;
441 }
442
443 QString
444 FileSource::getContentType() const
445 {
446 return m_contentType;
447 }
448
449 QString
450 FileSource::getExtension() const
451 {
452 if (m_localFilename != "") {
453 return QFileInfo(m_localFilename).suffix().toLower();
454 } else {
455 return QFileInfo(m_url.toLocalFile()).suffix().toLower();
456 }
457 }
458
459 QString
460 FileSource::getErrorString() const
461 {
462 return m_errorString;
463 }
464
465 void
466 FileSource::dataReadProgress(int done, int total)
467 {
468 dataTransferProgress(done, total);
469 }
470
471 void
472 FileSource::httpResponseHeaderReceived(const QHttpResponseHeader &resp)
473 {
474 m_lastStatus = resp.statusCode();
475 if (m_lastStatus / 100 >= 4) {
476 m_errorString = QString("%1 %2")
477 .arg(resp.statusCode()).arg(resp.reasonPhrase());
478 std::cerr << "FileSource::responseHeaderReceived: "
479 << m_errorString.toStdString() << std::endl;
480 } else {
481 std::cerr << "FileSource::responseHeaderReceived: "
482 << m_lastStatus << std::endl;
483 if (resp.hasContentType()) m_contentType = resp.contentType();
484 }
485 }
486
487 void
488 FileSource::ftpCommandFinished(int id, bool error)
489 {
490 std::cerr << "FileSource::ftpCommandFinished(" << id << ", " << error << ")" << std::endl;
491
492 if (!m_ftp) return;
493
494 QFtp::Command command = m_ftp->currentCommand();
495
496 if (!error) {
497 std::cerr << "FileSource::ftpCommandFinished: success for command "
498 << command << std::endl;
499 return;
500 }
501
502 if (command == QFtp::ConnectToHost) {
503 m_errorString = tr("Failed to connect to FTP server");
504 } else if (command == QFtp::Login) {
505 m_errorString = tr("Login failed");
506 } else if (command == QFtp::Cd) {
507 m_errorString = tr("Failed to change to correct directory");
508 } else if (command == QFtp::Get) {
509 m_errorString = tr("FTP download aborted");
510 }
511
512 m_lastStatus = 400; // for done()
513 }
514
515 void
516 FileSource::dataTransferProgress(qint64 done, qint64 total)
517 {
518 if (!m_progressDialog) return;
519
520 int percent = int((double(done) / double(total)) * 100.0 - 0.1);
521 emit progress(percent);
522
523 if (percent > 0) {
524 m_progressDialog->setValue(percent);
525 m_progressDialog->show();
526 }
527 }
528
529 void
530 FileSource::cancelled()
531 {
532 m_done = true;
533 cleanup();
534
535 m_ok = false;
536 m_errorString = tr("Download cancelled");
537 }
538
539 void
540 FileSource::done(bool error)
541 {
542 std::cerr << "FileSource::done(" << error << ")" << std::endl;
543
544 if (m_done) return;
545
546 emit progress(100);
547
548 if (error) {
549 if (m_http) {
550 m_errorString = m_http->errorString();
551 } else if (m_ftp) {
552 m_errorString = m_ftp->errorString();
553 }
554 }
555
556 if (m_lastStatus / 100 >= 4) {
557 error = true;
558 }
559
560 cleanup();
561
562 if (!error) {
563 QFileInfo fi(m_localFilename);
564 if (!fi.exists()) {
565 m_errorString = tr("Failed to create local file %1").arg(m_localFilename);
566 error = true;
567 } else if (fi.size() == 0) {
568 m_errorString = tr("File contains no data!");
569 error = true;
570 }
571 }
572
573 if (error) {
574 std::cerr << "FileSource::done: error is " << error << ", deleting cache file" << std::endl;
575 deleteCacheFile();
576 }
577
578 m_ok = !error;
579 m_done = true;
580 emit ready();
581 }
582
583 void
584 FileSource::deleteCacheFile()
585 {
586 std::cerr << "FileSource::deleteCacheFile(\"" << m_localFilename.toStdString() << "\")" << std::endl;
587
588 cleanup();
589
590 if (m_localFilename == "") {
591 return;
592 }
593
594 if (!isRemote()) {
595 std::cerr << "not a cache file" << std::endl;
596 return;
597 }
598
599 if (m_refCounted) {
600
601 QMutexLocker locker(&m_mapMutex);
602 m_refCounted = false;
603
604 if (m_refCountMap[m_url] > 0) {
605 m_refCountMap[m_url]--;
606 std::cerr << "reduced ref count to " << m_refCountMap[m_url] << std::endl;
607 if (m_refCountMap[m_url] > 0) {
608 m_done = true;
609 return;
610 }
611 }
612 }
613
614 m_fileCreationMutex.lock();
615
616 if (!QFile(m_localFilename).remove()) {
617 std::cerr << "FileSource::deleteCacheFile: ERROR: Failed to delete file \"" << m_localFilename.toStdString() << "\"" << std::endl;
618 } else {
619 std::cerr << "FileSource::deleteCacheFile: Deleted cache file \"" << m_localFilename.toStdString() << "\"" << std::endl;
620 m_localFilename = "";
621 }
622
623 m_fileCreationMutex.unlock();
624
625 m_done = true;
626 }
627
628 void
629 FileSource::showProgressDialog()
630 {
631 if (m_progressDialog) m_progressDialog->show();
632 }
633
634 bool
635 FileSource::createCacheFile()
636 {
637 {
638 QMutexLocker locker(&m_mapMutex);
639
640 std::cerr << "FileSource::createCacheFile: refcount is " << m_refCountMap[m_url] << std::endl;
641
642 if (m_refCountMap[m_url] > 0) {
643 m_refCountMap[m_url]++;
644 m_localFilename = m_remoteLocalMap[m_url];
645 std::cerr << "raised it to " << m_refCountMap[m_url] << std::endl;
646 m_refCounted = true;
647 return true;
648 }
649 }
650
651 QDir dir;
652 try {
653 dir = TempDirectory::getInstance()->getSubDirectoryPath("download");
654 } catch (DirectoryCreationFailed f) {
655 std::cerr << "FileSource::createCacheFile: ERROR: Failed to create temporary directory: " << f.what() << std::endl;
656 return "";
657 }
658
659 QString filepart = m_url.path().section('/', -1, -1,
660 QString::SectionSkipEmpty);
661
662 QString extension = filepart.section('.', -1);
663 QString base = filepart;
664 if (extension != "") {
665 base = base.left(base.length() - extension.length() - 1);
666 }
667 if (base == "") base = "remote";
668
669 QString filename;
670
671 if (extension == "") {
672 filename = base;
673 } else {
674 filename = QString("%1.%2").arg(base).arg(extension);
675 }
676
677 QString filepath(dir.filePath(filename));
678
679 std::cerr << "FileSource::createCacheFile: URL is \"" << m_url.toString().toStdString() << "\", dir is \"" << dir.path().toStdString() << "\", base \"" << base.toStdString() << "\", extension \"" << extension.toStdString() << "\", filebase \"" << filename.toStdString() << "\", filename \"" << filepath.toStdString() << "\"" << std::endl;
680
681 QMutexLocker fcLocker(&m_fileCreationMutex);
682
683 ++m_count;
684
685 if (QFileInfo(filepath).exists() ||
686 !QFile(filepath).open(QFile::WriteOnly)) {
687
688 std::cerr << "FileSource::createCacheFile: Failed to create local file \""
689 << filepath.toStdString() << "\" for URL \""
690 << m_url.toString().toStdString() << "\" (or file already exists): appending suffix instead" << std::endl;
691
692
693 if (extension == "") {
694 filename = QString("%1_%2").arg(base).arg(m_count);
695 } else {
696 filename = QString("%1_%2.%3").arg(base).arg(m_count).arg(extension);
697 }
698 filepath = dir.filePath(filename);
699
700 if (QFileInfo(filepath).exists() ||
701 !QFile(filepath).open(QFile::WriteOnly)) {
702
703 std::cerr << "FileSource::createCacheFile: ERROR: Failed to create local file \""
704 << filepath.toStdString() << "\" for URL \""
705 << m_url.toString().toStdString() << "\" (or file already exists)" << std::endl;
706
707 return "";
708 }
709 }
710
711 std::cerr << "FileSource::createCacheFile: url "
712 << m_url.toString().toStdString() << " -> local filename "
713 << filepath.toStdString() << std::endl;
714
715 m_localFilename = filepath;
716
717 return false;
718 }