implement failover correctly
[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
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/media/MediaAria2c.h"
34 #include "zypp/media/proxyinfo/ProxyInfos.h"
35 #include "zypp/media/ProxyInfo.h"
36 #include "zypp/media/MediaUserAuth.h"
37 #include "zypp/media/MediaCurl.h"
38 #include "zypp/thread/Once.h"
39 #include <cstdlib>
40 #include <sys/types.h>
41 #include <sys/stat.h>
42 #include <sys/mount.h>
43 #include <errno.h>
44 #include <dirent.h>
45 #include <unistd.h>
46 #include <boost/format.hpp>
47
48 #define  DETECT_DIR_INDEX       0
49 #define  CONNECT_TIMEOUT        60
50 #define  TRANSFER_TIMEOUT       60 * 3
51 #define  TRANSFER_TIMEOUT_MAX   60 * 60
52
53
54 using namespace std;
55 using namespace zypp::base;
56
57 namespace zypp
58 {
59 namespace media
60 {
61
62 Pathname MediaAria2c::_cookieFile = "/var/lib/YaST2/cookies";
63 Pathname MediaAria2c::_aria2cPath = "/usr/local/bin/aria2c";
64 std::string MediaAria2c::_aria2cVersion = "WE DON'T KNOW ARIA2C VERSION";
65
66 //check if aria2c is present in the system
67 bool
68 MediaAria2c::existsAria2cmd()
69 {
70     const char* argv[] =
71     {
72       "which",
73       "aria2c",
74       NULL
75     };
76
77     ExternalProgram aria(argv, ExternalProgram::Stderr_To_Stdout);
78     
79     for(std::string ariaResponse( aria.receiveLine());
80         ariaResponse.length();
81         ariaResponse = aria.receiveLine())
82     {
83         // nothing
84     }
85     
86     return ( aria.close() == 0 );
87 }
88
89 /**
90  * comannd line for aria.
91  * The argument list gets passed as reference
92  * and it is filled.
93  */
94 void fillAriaCmdLine( const Pathname &ariapath,
95                       const string &ariaver,
96                       const TransferSettings &s,
97                       const Url &url,
98                       const Pathname &destination,
99                       ExternalProgram::Arguments &args )
100 {
101     args.push_back(ariapath.c_str());
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         if ( url.getScheme() == "ftp" )
161             args.push_back(str::form("--ftp-user=%s", s.username().c_str() ));
162         else if ( url.getScheme() == "http" ||
163                   url.getScheme() == "https" )
164             args.push_back(str::form("--http-user=%s", s.username().c_str() ));
165         
166         if ( s.password().size() )
167         {
168             if ( url.getScheme() == "ftp" )
169                 args.push_back(str::form("--ftp-passwd=%s", s.password().c_str() ));
170             else if ( url.getScheme() == "http" ||
171                       url.getScheme() == "https" )
172                 args.push_back(str::form("--http-passwd=%s", s.password().c_str() ));
173         }
174     }
175     
176     if ( s.proxyEnabled() )
177     {
178         args.push_back(str::form("--http-proxy=%s", s.proxy().c_str() ));
179         if ( ! s.proxyUsername().empty() )
180         {
181             args.push_back(str::form("--http-proxy-user=%s", s.proxyUsername().c_str() ));
182             if ( ! s.proxyPassword().empty() )
183                 args.push_back(str::form("--http-proxy-passwd=%s", s.proxyPassword().c_str() ));
184         }
185     }
186
187     if ( ! destination.empty() )
188         args.push_back(str::form("--dir=%s", destination.c_str()));
189
190     args.push_back(url.asString().c_str());
191 }
192
193 const char *const MediaAria2c::agentString()
194 {
195   // we need to add the release and identifier to the
196   // agent string.
197   // The target could be not initialized, and then this information
198   // is not available.
199   Target_Ptr target = zypp::getZYpp()->getTarget();
200
201   static const std::string _value(
202     str::form(
203        "ZYpp %s (%s) %s"
204        , VERSION
205        , MediaAria2c::_aria2cVersion.c_str()
206        , target ? target->targetDistribution().c_str() : ""
207     )
208   );
209   return _value.c_str();
210 }
211
212
213
214 MediaAria2c::MediaAria2c( const Url &      url_r,
215                       const Pathname & attach_point_hint_r )
216     : MediaCurl( url_r, attach_point_hint_r )
217 {
218   MIL << "MediaAria2c::MediaAria2c(" << url_r << ", " << attach_point_hint_r << ")" << endl;
219
220    //At this point, we initialize aria2c path
221    _aria2cPath = Pathname( whereisAria2c().asString() );
222
223    //Get aria2c version
224    _aria2cVersion = getAria2cVersion();
225 }
226
227 void MediaAria2c::attachTo (bool next)
228 {
229   MediaCurl::attachTo(next);
230   _settings.setUserAgentString(agentString());
231 }
232
233 bool
234 MediaAria2c::checkAttachPoint(const Pathname &apoint) const
235 {
236     return MediaCurl::checkAttachPoint( apoint );
237 }
238
239 void MediaAria2c::disconnectFrom()
240 {
241     MediaCurl::disconnectFrom();
242 }
243
244 void MediaAria2c::releaseFrom( const std::string & ejectDev )
245 {
246   MediaCurl::releaseFrom(ejectDev);
247 }
248
249 static Url getFileUrl(const Url & url, const Pathname & filename)
250 {
251   Url newurl(url);
252   string path = url.getPathName();
253   if ( !path.empty() && path != "/" && *path.rbegin() == '/' &&
254        filename.absolute() )
255   {
256     // If url has a path with trailing slash, remove the leading slash from
257     // the absolute file name
258     path += filename.asString().substr( 1, filename.asString().size() - 1 );
259   }
260   else if ( filename.relative() )
261   {
262     // Add trailing slash to path, if not already there
263     if (path.empty()) path = "/";
264     else if (*path.rbegin() != '/' ) path += "/";
265     // Remove "./" from begin of relative file name
266     path += filename.asString().substr( 2, filename.asString().size() - 2 );
267   }
268   else
269   {
270     path += filename.asString();
271   }
272
273   newurl.setPathName(path);
274   return newurl;
275 }
276
277 void MediaAria2c::getFile( const Pathname & filename ) const
278 {
279     // Use absolute file name to prevent access of files outside of the
280     // hierarchy below the attach point.
281     getFileCopy(filename, localPath(filename).absolutename());
282 }
283
284 void MediaAria2c::getFileCopy( const Pathname & filename , const Pathname & target) const
285 {
286   callback::SendReport<DownloadProgressReport> report;
287
288   Url fileurl(getFileUrl(_url, filename));
289
290   bool retry = false;
291
292   ExternalProgram::Arguments args;
293
294   fillAriaCmdLine(_aria2cPath, _aria2cVersion, _settings, fileurl, target.dirname(), args);
295   
296   do
297   {
298     try
299     {
300       report->start(_url, target.asString() );
301
302       ExternalProgram aria(args, ExternalProgram::Stderr_To_Stdout);
303
304       // progress line like: [#1 SIZE:8.3MiB/10.1MiB(82%) CN:5 SPD:6899.88KiB/s]
305       // we save it until we find a string with FILE: later
306       string progressLine;
307       // file line, which tell which file is the previous progress
308       // ie: FILE: ./packages.FL.gz
309       double average_speed = 0;
310       long average_speed_count = 0;
311
312       // here we capture aria output exceptions
313       vector<string> ariaExceptions;
314       
315       //Process response
316       for(std::string ariaResponse( aria.receiveLine());
317           ariaResponse.length();
318           ariaResponse = aria.receiveLine())
319       {
320         //cout << ariaResponse;
321         string line = str::trim(ariaResponse);
322           
323         // look for the progress line and save it for later
324         if ( str::hasPrefix(line, "[#") )
325         {
326           progressLine = line;
327         }
328         // save error messages for later
329         else if ( str::hasPrefix(line, "Exception: ") )
330         {
331           // for auth exception, we throw
332           if (!line.substr(0,31).compare("Exception: Authorization failed") )
333           {
334             ZYPP_THROW(MediaUnauthorizedException(
335                        _url, "Login failed.", "Login failed", "auth hint"
336             ));
337           }
338           // otherwise, remember the error
339           string excpMsg = line.substr(10, line.size());
340           DBG << "aria2c reported: '" << excpMsg << "'" << endl;            
341           ariaExceptions.push_back(excpMsg);
342         }
343         else if ( str::hasPrefix(line, "FILE: ") )
344         {
345           // get the FILE name
346           Pathname theFile(line.substr(6, line.size()));
347           // is the report about the filename we are downloading?
348           // aria may report progress about metalinks, torrent and
349           // other stuff which is not the main transfer
350           if ( theFile == target )
351           {
352             // once we find the FILE: line, progress has to be
353             // non empty
354             if ( ! progressLine.empty() )
355             {
356               // get the percentage (progress) data
357               int progress = 0;
358               size_t left_bound = progressLine.find('(',0) + 1;
359               size_t count = progressLine.find('%',left_bound) - left_bound;
360               string progressStr = progressLine.substr(left_bound, count);
361  
362               if ( count != string::npos )
363                 progress = std::atoi(progressStr.c_str());
364               else
365                   ERR << "Can't parse progress from '" << progressStr << "'" << endl;
366               // get the speed
367               double current_speed = 0;
368               left_bound = progressLine.find("SPD:",0) + 4;
369               count = progressLine.find("KiB/s",left_bound) - left_bound;
370               if ( count != string::npos )
371               { // convert the string to a double
372                 string speedStr = progressLine.substr(left_bound, count);
373                 try {
374                   current_speed = boost::lexical_cast<double>(speedStr);
375                 }
376                 catch (const std::exception&) {
377                   ERR << "Can't parse speed from '" << speedStr << "'" << endl;
378                   current_speed = 0;
379                 }
380               }
381                     
382               // we have a new average speed
383               average_speed_count++;
384               
385               // this is basically A: average
386               // ((n-1)A(n-1) + Xn)/n = A(n)
387               average_speed = (((average_speed_count - 1 )*average_speed) + current_speed)/average_speed_count;
388                     
389               report->progress ( progress, _url, average_speed, current_speed );
390               // clear the progress line to detect mismatches between
391               // [# and FILE: lines
392               progressLine.clear();
393             }
394             else
395             {
396               WAR << "aria2c reported a file, but no progress data available" << endl;
397             }
398
399           }
400           else
401           {
402             DBG << "Progress is not about '" << target << "' but '" << theFile << "'" << endl;
403           }            
404         }
405         else
406         {
407             // other line type, just ignore it.
408         }
409       }
410
411       int code = aria.close();
412
413       switch ( code )
414       {
415         // success
416         case 0: // success
417             break;
418         case 2: // timeout
419         {
420           MediaTimeoutException e(_url);
421           for_(it, ariaExceptions.begin(), ariaExceptions.end())
422               e.addHistory(*it);
423           ZYPP_THROW(e);
424         }
425         case 3: // not found
426         case 4: // max notfound reached
427         {
428           MediaFileNotFoundException e(_url, filename);
429           for_(it, ariaExceptions.begin(), ariaExceptions.end())
430               e.addHistory(*it);
431           ZYPP_THROW(e);
432         }
433         case 5: // too slow
434         case 6: // network problem
435         case 7: // unfinished downloads (ctr-c)
436         case 1: // unknown
437         default:
438         {
439           MediaException e(str::form("Failed to download %s from %s", filename.c_str(), _url.asString().c_str()));
440           for_(it, ariaExceptions.begin(), ariaExceptions.end())
441               e.addHistory(*it);
442               
443           ZYPP_THROW(e);
444         }
445       }
446       
447       report->finish( _url ,  zypp::media::DownloadProgressReport::NO_ERROR, "");
448       retry = false;
449     }
450     // retry with proper authentication data
451     catch (MediaUnauthorizedException & ex_r)
452     {
453       if(authenticate(ex_r.hint(), !retry))
454         retry = true;
455       else
456       {
457         report->finish(fileurl, zypp::media::DownloadProgressReport::ACCESS_DENIED, ex_r.asUserHistory());
458         ZYPP_RETHROW(ex_r);
459       }
460
461     }
462     // unexpected exception
463     catch (MediaException & excpt_r)
464     {
465       // FIXME: error number fix
466       report->finish(fileurl, zypp::media::DownloadProgressReport::ERROR, excpt_r.asUserHistory());
467       ZYPP_RETHROW(excpt_r);
468     }
469   }
470   while (retry);
471
472   report->finish(fileurl, zypp::media::DownloadProgressReport::NO_ERROR, "");
473 }
474
475 bool MediaAria2c::getDoesFileExist( const Pathname & filename ) const
476 {
477     return MediaCurl::getDoesFileExist(filename);
478 }
479
480 bool MediaAria2c::doGetDoesFileExist( const Pathname & filename ) const
481 {
482     return MediaCurl::doGetDoesFileExist(filename);
483 }
484     
485 void MediaAria2c::getDir( const Pathname & dirname, bool recurse_r ) const
486 {
487     MediaCurl::getDir(dirname, recurse_r);
488 }
489
490 bool MediaAria2c::authenticate(const std::string & availAuthTypes, bool firstTry) const
491 {
492     return false;
493 }
494
495
496 void MediaAria2c::getDirInfo( std::list<std::string> & retlist,
497                                const Pathname & dirname, bool dots ) const
498 {
499   getDirectoryYast( retlist, dirname, dots );
500 }
501
502 void MediaAria2c::getDirInfo( filesystem::DirContent & retlist,
503                             const Pathname & dirname, bool dots ) const
504 {
505   getDirectoryYast( retlist, dirname, dots );
506 }
507
508 std::string MediaAria2c::getAria2cVersion()
509 {
510     const char* argv[] =
511     {
512         _aria2cPath.c_str(),
513       "--version",
514       NULL
515     };
516
517     ExternalProgram aria(argv, ExternalProgram::Stderr_To_Stdout);
518
519     std::string vResponse = aria.receiveLine();
520     aria.close();
521     return str::trim(vResponse);
522 }
523
524 #define ARIA_DEFAULT_BINARY "/usr/bin/aria2c"
525
526 Pathname MediaAria2c::whereisAria2c()
527 {
528     Pathname aria2cPathr(ARIA_DEFAULT_BINARY);
529
530     const char* argv[] =
531     {
532       "which",
533       "aria2c",
534       NULL
535     };
536
537     ExternalProgram aria(argv, ExternalProgram::Stderr_To_Stdout);
538
539     std::string ariaResponse( aria.receiveLine());
540     int code = aria.close();
541
542     if( code == 0 )
543     {
544         aria2cPathr = str::trim(ariaResponse);
545         MIL << "We will use aria2c located here:  " << aria2cPathr << endl;
546     }
547     else
548     {
549         MIL << "We don't know were is ari2ac binary. We will use aria2c located here:  " << aria2cPathr << endl;
550     }
551
552     return aria2cPathr;
553 }
554
555 } // namespace media
556 } // namespace zypp
557 //