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