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