aria2 sends the url in the progress if there is no response from the server, handle...
[platform/upstream/libzypp.git] / zypp / media / MediaAria2c.cc
1 /*---------------------------------------------------------------------\
2 |                          ____ _   __ __ ___                          |
3 |                         |__  / \ / / . \ . \                         |
4 |                           / / \ V /|  _/  _/                         |
5 |                          / /__ | | | | | |                           |
6 |                         /_____||_| |_| |_|                           |
7 |                                                                      |
8 \---------------------------------------------------------------------*/
9 /** \file zypp/media/MediaAria2c.cc
10  *
11 */
12
13 #include <iostream>
14 #include <list>
15 #include <vector>
16 #include <fstream>
17 #include <boost/lexical_cast.hpp>
18
19 #include "zypp/base/Logger.h"
20 #include "zypp/ExternalProgram.h"
21 #include "zypp/ProgressData.h"
22 #include "zypp/base/String.h"
23 #include "zypp/base/Gettext.h"
24 #include "zypp/base/Sysconfig.h"
25 #include "zypp/base/Gettext.h"
26 #include "zypp/ZYppCallbacks.h"
27
28 #include "zypp/Edition.h"
29 #include "zypp/Target.h"
30 #include "zypp/ZYppFactory.h"
31 #include "zypp/ZConfig.h"
32
33 #include "zypp/TmpPath.h"
34
35 #include "zypp/media/MediaAria2c.h"
36 #include "zypp/media/proxyinfo/ProxyInfos.h"
37 #include "zypp/media/ProxyInfo.h"
38 #include "zypp/media/MediaUserAuth.h"
39 #include "zypp/media/MediaCurl.h"
40 #include "zypp/thread/Once.h"
41 #include <cstdlib>
42 #include <sys/types.h>
43 #include <sys/stat.h>
44 #include <sys/mount.h>
45 #include <errno.h>
46 #include <dirent.h>
47 #include <unistd.h>
48 #include <boost/format.hpp>
49
50 #define  DETECT_DIR_INDEX       0
51 #define  CONNECT_TIMEOUT        60
52 #define  TRANSFER_TIMEOUT       60 * 3
53 #define  TRANSFER_TIMEOUT_MAX   60 * 60
54
55 #define  ARIA_BINARY "aria2c"
56
57 using namespace std;
58 using namespace zypp::base;
59
60 namespace zypp
61 {
62 namespace media
63 {
64
65 Pathname MediaAria2c::_cookieFile = "/var/lib/YaST2/cookies";
66 std::string MediaAria2c::_aria2cVersion = "WE DON'T KNOW ARIA2C VERSION";
67
68 //check if aria2c is present in the system
69 bool
70 MediaAria2c::existsAria2cmd()
71 {
72   static const char* argv[] =
73   {
74     ARIA_BINARY,
75     "--version",
76     NULL
77   };
78   ExternalProgram aria( argv, ExternalProgram::Stderr_To_Stdout );
79   return( aria.close() == 0 );
80 }
81
82 /**
83  * comannd line for aria.
84  * The argument list gets passed as reference
85  * and it is filled.
86  */
87 void fillAriaCmdLine( const string &ariaver,
88                       const TransferSettings &s,
89                       filesystem::TmpPath &credentials,
90                       const Url &url,
91                       const Pathname &destination,
92                       ExternalProgram::Arguments &args )
93 {
94     
95     // options that are not passed in the command line
96     // like credentials, every string is in the
97     // opt=val format
98     list<string> file_options;
99     
100     args.push_back(ARIA_BINARY);
101     args.push_back(str::form("--user-agent=%s", s.userAgentString().c_str()));
102     args.push_back("--summary-interval=1");
103     args.push_back("--follow-metalink=mem");
104     args.push_back("--check-integrity=true");
105     args.push_back("--file-allocation=none");
106
107     // save the stats of the mirrors and use them as input later
108     Pathname statsFile = ZConfig::instance().repoCachePath() / "aria2.stats";
109     args.push_back(str::form("--server-stat-of=%s", statsFile.c_str()));
110     args.push_back(str::form("--server-stat-if=%s", statsFile.c_str()));
111     args.push_back("--uri-selector=adaptive");
112
113     // only present in recent aria lets find out the aria version
114     vector<string> fields;
115     // "aria2c version x.x"
116     str::split( ariaver, std::back_inserter(fields));
117     if ( fields.size() == 3 )
118     {
119         if ( Edition(fields[2]) >= Edition("1.1.2") )
120             args.push_back( "--use-head=false");
121     }
122
123     if ( s.maxDownloadSpeed() > 0 )
124         args.push_back(str::form("--max-download-limit=%ld", s.maxDownloadSpeed()));
125     if ( s.minDownloadSpeed() > 0 )
126         args.push_back(str::form("--lowest-speed-limit=%ld", s.minDownloadSpeed()));
127
128     args.push_back(str::form("--max-tries=%ld", s.maxSilentTries()));
129
130     if ( Edition(fields[2]) < Edition("1.2.0") )
131         WAR << "aria2c is older than 1.2.0, some features may be disabled" << endl;
132
133     // TODO make this one configurable
134     args.push_back(str::form("--max-concurrent-downloads=%ld", s.maxConcurrentConnections()));
135
136     // add the anonymous id.
137     for ( TransferSettings::Headers::const_iterator it = s.headersBegin();
138           it != s.headersEnd();
139           ++it )
140         args.push_back(str::form("--header=%s", it->c_str() ));
141
142     args.push_back( str::form("--connect-timeout=%ld", s.timeout()));
143
144     if ( s.username().empty() )
145     {
146         if ( url.getScheme() == "ftp" )
147         {
148             // set anonymous ftp
149             args.push_back(str::form("--ftp-user=%s", "suseuser" ));
150             args.push_back(str::form("--ftp-passwd=%s", VERSION ));
151
152             string id = "yast2";
153             id += VERSION;
154             DBG << "Anonymous FTP identification: '" << id << "'" << endl;
155         }
156     }
157     else
158     {
159         if ( url.getScheme() == "ftp" )
160             file_options.push_back(str::form("ftp-user=%s", s.username().c_str() ));
161         else if ( url.getScheme() == "http" ||
162                   url.getScheme() == "https" )
163             file_options.push_back(str::form("http-user=%s", s.username().c_str() ));
164
165         if ( s.password().size() )
166         {
167             if ( url.getScheme() == "ftp" )
168                 file_options.push_back(str::form("ftp-passwd=%s", s.password().c_str() ));
169             else if ( url.getScheme() == "http" ||
170                       url.getScheme() == "https" )
171                 file_options.push_back(str::form("http-passwd=%s", s.password().c_str() ));
172         }
173     }
174
175     if ( s.proxyEnabled() )
176     {
177         args.push_back(str::form("--http-proxy=%s", s.proxy().c_str() ));
178         if ( ! s.proxyUsername().empty() )
179         {
180             file_options.push_back(str::form("http-proxy-user=%s", s.proxyUsername().c_str() ));
181             if ( ! s.proxyPassword().empty() )
182                 file_options.push_back(str::form("http-proxy-passwd=%s", s.proxyPassword().c_str() ));
183         }
184     }
185
186     if ( ! destination.empty() )
187         args.push_back(str::form("--dir=%s", destination.c_str()));
188
189     // now append the file if there are hidden options
190     if ( ! file_options.empty() )
191     {
192         filesystem::TmpFile tmp;
193         ofstream outs( tmp.path().c_str() );
194         for_( it, file_options.begin(), file_options.end() )
195             outs << *it << endl;
196         outs.close();
197
198         credentials = tmp;
199         args.push_back(str::form("--conf-path=%s", credentials.path().c_str()));
200     }
201     
202     args.push_back(url.asString().c_str());
203 }
204
205 const char *const MediaAria2c::agentString()
206 {
207   // we need to add the release and identifier to the
208   // agent string.
209   // The target could be not initialized, and then this information
210   // is not available.
211   Target_Ptr target = zypp::getZYpp()->getTarget();
212
213   static const std::string _value(
214     str::form(
215        "ZYpp %s (%s) %s"
216        , VERSION
217        , MediaAria2c::_aria2cVersion.c_str()
218        , Target::targetDistribution( Pathname()/*guess root*/ ).c_str()
219     )
220   );
221   return _value.c_str();
222 }
223
224
225
226 MediaAria2c::MediaAria2c( const Url &      url_r,
227                       const Pathname & attach_point_hint_r )
228     : MediaCurl( url_r, attach_point_hint_r )
229 {
230   MIL << "MediaAria2c::MediaAria2c(" << url_r << ", " << attach_point_hint_r << ")" << endl;
231   //Get aria2c version
232   _aria2cVersion = getAria2cVersion();
233 }
234
235 void MediaAria2c::attachTo (bool next)
236 {
237   MediaCurl::attachTo(next);
238   _settings.setUserAgentString(agentString());
239 }
240
241 bool
242 MediaAria2c::checkAttachPoint(const Pathname &apoint) const
243 {
244     return MediaCurl::checkAttachPoint( apoint );
245 }
246
247 void MediaAria2c::disconnectFrom()
248 {
249     MediaCurl::disconnectFrom();
250 }
251
252 void MediaAria2c::releaseFrom( const std::string & ejectDev )
253 {
254   MediaCurl::releaseFrom(ejectDev);
255 }
256
257 static Url getFileUrl(const Url & url, const Pathname & filename)
258 {
259   Url newurl(url);
260   string path = url.getPathName();
261   if ( !path.empty() && path != "/" && *path.rbegin() == '/' &&
262        filename.absolute() )
263   {
264     // If url has a path with trailing slash, remove the leading slash from
265     // the absolute file name
266     path += filename.asString().substr( 1, filename.asString().size() - 1 );
267   }
268   else if ( filename.relative() )
269   {
270     // Add trailing slash to path, if not already there
271     if (path.empty()) path = "/";
272     else if (*path.rbegin() != '/' ) path += "/";
273     // Remove "./" from begin of relative file name
274     path += filename.asString().substr( 2, filename.asString().size() - 2 );
275   }
276   else
277   {
278     path += filename.asString();
279   }
280
281   newurl.setPathName(path);
282   return newurl;
283 }
284
285 void MediaAria2c::getFile( const Pathname & filename ) const
286 {
287     // Use absolute file name to prevent access of files outside of the
288     // hierarchy below the attach point.
289     getFileCopy(filename, localPath(filename).absolutename());
290 }
291
292 void MediaAria2c::getFileCopy( const Pathname & filename , const Pathname & target) const
293 {
294   callback::SendReport<DownloadProgressReport> report;
295
296   Url fileurl(getFileUrl(_url, filename));
297
298   bool retry = false;
299
300   ExternalProgram::Arguments args;
301
302   filesystem::TmpPath credentials;
303   fillAriaCmdLine(_aria2cVersion, _settings, credentials, fileurl, target.dirname(), args);
304
305   do
306   {
307     try
308     {
309       report->start(fileurl, target.asString() );
310
311       ExternalProgram aria(args, ExternalProgram::Stderr_To_Stdout);
312
313       // progress line like: [#1 SIZE:8.3MiB/10.1MiB(82%) CN:5 SPD:6899.88KiB/s]
314       // but since 1.4.0:    [#1 SIZE:8.3MiB/10.1MiB(82%) CN:5 SPD:899.8KiBs]
315       //       (bnc #513944) [#1 SIZE:8.3MiB/10.1MiB(82%) CN:5 SPD:3.8MiBs]
316       // we save it until we find a string with FILE: later
317       string progressLine;
318       int progress = 0;
319       // file line, which tell which file is the previous progress
320       // ie: FILE: ./packages.FL.gz
321       double average_speed = 0;
322       long average_speed_count = 0;
323
324       // here we capture aria output exceptions
325       vector<string> ariaExceptions;
326
327       // TODO: Detect partial downloads!
328       bool partialDownload = false; // Whether it makes sense to retry with --continue!
329
330       //Process response
331       for(std::string ariaResponse( aria.receiveLine());
332           ariaResponse.length();
333           ariaResponse = aria.receiveLine())
334       {
335         //cout << ariaResponse;
336         string line = str::trim(ariaResponse);
337
338         // look for the progress line and save it for later
339         if ( str::hasPrefix(line, "[#") )
340         {
341           progressLine = line;
342         }
343         // save error messages for later
344         else if ( str::hasPrefix(line, "Exception: ") )
345         {
346           // for auth exception, we throw
347           if (!line.substr(0,31).compare("Exception: Authorization failed") )
348           {
349             ZYPP_THROW(MediaUnauthorizedException(
350                        _url, "Login failed.", "Login failed", "auth hint"
351             ));
352           }
353           // otherwise, remember the error
354           string excpMsg = line.substr(10, line.size());
355           DBG << "aria2c reported: '" << excpMsg << "'" << endl;
356           ariaExceptions.push_back(excpMsg);
357         }
358         else if ( str::hasPrefix(line, "FILE: ") )
359         {
360           // get the FILE name
361           string theFile(line.substr(6, line.size()));
362           // is the report about the filename we are downloading?
363           // aria may report progress about metalinks, torrent and
364           // other stuff which is not the main transfer
365           // the reported file is the url before the server emits a response
366           // and then is reported as the target file
367           if ( Pathname(theFile) == target || theFile == fileurl.asCompleteString() )
368           {
369             // once we find the FILE: line, progress has to be
370             // non empty
371             if ( ! progressLine.empty() )
372             {
373               // get the percentage (progress) data
374               progress = 0;
375               size_t left_bound = progressLine.find('(',0) + 1;
376               size_t count = progressLine.find('%',left_bound) - left_bound;
377               string progressStr = progressLine.substr(left_bound, count);
378
379               if ( count != string::npos )
380                 progress = std::atoi(progressStr.c_str());
381               else
382                   ERR << "Can't parse progress from '" << progressStr << "'" << endl;
383
384               // get the speed
385               double current_speed = 0;
386               left_bound = progressLine.find("SPD:",0) + 4;
387               count = progressLine.find("KiB",left_bound);
388               bool kibs = true; // KiBs ? (MiBs if false)
389               if ( count == string::npos ) // try MiBs
390               {
391                 count = progressLine.find("MiBs",left_bound);
392                 kibs = false;
393               }
394               if ( count != string::npos )
395               { // convert the string to a double
396                 count -= left_bound;
397                 string speedStr = progressLine.substr(left_bound, count);
398                 try {
399                   current_speed = boost::lexical_cast<double>(speedStr);
400                 }
401                 catch (const std::exception&) {
402                   ERR << "Can't parse speed from '" << speedStr << "'" << endl;
403                   current_speed = 0;
404                 }
405               }
406
407               // we have a new average speed
408               average_speed_count++;
409
410               // this is basically A: average
411               // ((n-1)A(n-1) + Xn)/n = A(n)
412               average_speed =
413                 (((average_speed_count - 1 )*average_speed) + current_speed)
414                 / average_speed_count;
415
416               // note that aria report speed in kBps or MBps, while the report takes Bps
417               report->progress ( progress, fileurl,
418                   average_speed * (kibs ? 0x400 : 0x10000),
419                   current_speed * (kibs ? 0x400 : 0x10000));
420               // clear the progress line to detect mismatches between
421               // [# and FILE: lines
422               progressLine.clear();
423             }
424             else
425             {
426               WAR << "aria2c reported a file, but no progress data available" << endl;
427             }
428
429           }
430           else
431           {
432             DBG << "Progress is not about '" << target << "' but '" << theFile << "'" << endl;
433           }
434         }
435         else
436         {
437             // other line type, just ignore it.
438         }
439       }
440
441       int code = aria.close();
442
443       switch ( code )
444       {
445         // success
446         case 0: // success
447             break;
448         case 2: // timeout
449         {
450           MediaTimeoutException e(_url);
451           for_(it, ariaExceptions.begin(), ariaExceptions.end())
452               e.addHistory(*it);
453           ZYPP_THROW(e);
454         }
455         case 3: // not found
456         case 4: // max notfound reached
457         {
458           MediaFileNotFoundException e(_url, filename);
459           for_(it, ariaExceptions.begin(), ariaExceptions.end())
460               e.addHistory(*it);
461           ZYPP_THROW(e);
462         }
463         case 5: // too slow
464         case 6: // network problem
465         case 7: // unfinished downloads (ctr-c)
466         case 1: // unknown
467         default:
468         {
469           if ( partialDownload )
470           {
471             // Ask for retry on partial downloads, when it makes sense to retry with --continue!
472             // Other errors are handled by the layers above.
473             MediaException e(str::form(_("Download interrupted at %d%%"), progress ));
474             for_(it, ariaExceptions.begin(), ariaExceptions.end())
475               e.addHistory(*it);
476
477             DownloadProgressReport::Action action = report->problem( _url, DownloadProgressReport::ERROR, e.asUserHistory() );
478             if ( action == DownloadProgressReport::RETRY )
479             {
480               retry = true;
481               continue;
482             }
483           }
484
485           // TranslatorExplanation: Failed to download <FILENAME> from <SERVERURL>.
486           MediaException e(str::form(_("Failed to download %s from %s"), filename.c_str(), _url.asString().c_str()));
487           for_(it, ariaExceptions.begin(), ariaExceptions.end())
488               e.addHistory(*it);
489
490           ZYPP_THROW(e);
491         }
492       }
493
494       retry = false;
495     }
496     // retry with proper authentication data
497     catch (MediaUnauthorizedException & ex_r)
498     {
499       if(authenticate(ex_r.hint(), !retry))
500         retry = true;
501       else
502       {
503         report->finish(fileurl, zypp::media::DownloadProgressReport::ACCESS_DENIED, ex_r.asUserHistory());
504         ZYPP_RETHROW(ex_r);
505       }
506
507     }
508     // unexpected exception
509     catch (MediaException & excpt_r)
510     {
511       // FIXME: error number fix
512       report->finish(fileurl, zypp::media::DownloadProgressReport::ERROR, excpt_r.asUserHistory());
513       ZYPP_RETHROW(excpt_r);
514     }
515   }
516   while (retry);
517
518   report->finish(fileurl, zypp::media::DownloadProgressReport::NO_ERROR, "");
519 }
520
521 bool MediaAria2c::getDoesFileExist( const Pathname & filename ) const
522 {
523     return MediaCurl::getDoesFileExist(filename);
524 }
525
526 bool MediaAria2c::doGetDoesFileExist( const Pathname & filename ) const
527 {
528     return MediaCurl::doGetDoesFileExist(filename);
529 }
530
531 void MediaAria2c::getDir( const Pathname & dirname, bool recurse_r ) const
532 {
533     MediaCurl::getDir(dirname, recurse_r);
534 }
535
536 bool MediaAria2c::authenticate(const std::string & availAuthTypes, bool firstTry) const
537 {
538     return false;
539 }
540
541 void MediaAria2c::getDirInfo( std::list<std::string> & retlist,
542                                const Pathname & dirname, bool dots ) const
543 {
544   getDirectoryYast( retlist, dirname, dots );
545 }
546
547 void MediaAria2c::getDirInfo( filesystem::DirContent & retlist,
548                             const Pathname & dirname, bool dots ) const
549 {
550   getDirectoryYast( retlist, dirname, dots );
551 }
552
553 std::string MediaAria2c::getAria2cVersion()
554 {
555     static const char* argv[] =
556     {
557       ARIA_BINARY,
558       "--version",
559       NULL
560     };
561     ExternalProgram aria(argv, ExternalProgram::Stderr_To_Stdout);
562     std::string vResponse( str::trim( aria.receiveLine() ) );
563     aria.close();
564     return vResponse;
565 }
566 } // namespace media
567 } // namespace zypp
568 //