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