gget/gget.c

773 lines
20 KiB
C

/*
Copyright 2014-2016 Grégory Soutadé
This file is part of gget.
gget is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
gget is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with gget. If not, see <http://www.gnu.org/licenses/>.
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include <libgen.h>
#define DEFAULT_USER_AGENT "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:30.0) Gecko/20100101 Firefox/30.0"
#define DEFAULT_NB_THREADS 3
#define MAX_NB_THREADS 10
#define DEFAULT_OUT_FILENAME "gget.out"
#ifdef WIN32
#include <windows.h>
#define GNU_ERR ""
typedef HANDLE pthread_t;
typedef void pthread_attr_t;
static int get_console_width()
{
RECT r;
HWND console = GetConsoleWindow();
GetWindowRect(console, &r);
return r.right-r.left;
}
static int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg)
{
*thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)start_routine,
arg, 0, NULL);
return 0;
}
static int pthread_join(pthread_t thread, void **retval)
{
WaitForSingleObject(thread, 0);
GetExitCodeThread(thread,(LPDWORD)retval);
CloseHandle(thread);
return 0;
}
#else
#include <sys/ioctl.h>
#include <pthread.h>
#define GNU_ERR " (%m)"
static int get_console_width()
{
struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
return w.ws_col;
}
#endif
typedef struct {
curl_off_t dltotal;
curl_off_t dlnow;
curl_off_t dllast;
unsigned speed;
} stats_t ;
typedef struct {
pthread_t thread;
char* tmp_filename;
//
CURL *curl;
char* url;
char* user_agent;
int fd;
int id;
unsigned already_downloaded;
unsigned start;
unsigned end;
unsigned max_chunk_size;
curl_off_t max_speed;
stats_t* stats;
} transfert_t;
typedef struct {
unsigned nb_threads;
stats_t* stats;
unsigned end;
} stats_params_t;
static void* display_stats(stats_params_t* p)
{
unsigned speed, percent, time_left, max_time_left;
unsigned total_percent, hours, minutes, seconds;
char* suffix;
stats_t* cur;
int i, nb_chars_printed;
while (!p->end)
{
// If the window has been resized
max_time_left = 0;
suffix = "B";
total_percent = 0;
nb_chars_printed = 0;
printf("\r");
for(i=0; i<p->nb_threads; i++)
{
cur = &p->stats[i];
speed = cur->speed;
if (cur->dltotal)
percent = (cur->dlnow*100)/cur->dltotal;
else
percent = 0;
if (speed)
time_left = (cur->dltotal-cur->dlnow)/speed;
else
time_left = 0;
if (speed < (1024*1024))
{
speed /= 1024;
suffix = "kB";
}
else
{
speed /= 1024*1024;
suffix = "MB";
}
nb_chars_printed +=
printf("T%d: %02u%% %u%s/s ",
i, percent, speed, suffix);
if (time_left > max_time_left)
max_time_left = time_left;
total_percent += percent/p->nb_threads;
}
nb_chars_printed += printf(" Total: %u%% ", total_percent);
if (max_time_left < 60)
{
nb_chars_printed += printf("eta %us", max_time_left);
}
else
{
seconds = max_time_left % 60;
max_time_left /= 60;
if (max_time_left < 60)
{
minutes = max_time_left;
nb_chars_printed += printf("eta %um %us", minutes, seconds);
}
else
{
minutes = max_time_left % 60;
hours = max_time_left / 60;
nb_chars_printed += printf("eta %uh %um %us", hours, minutes, seconds);
}
}
for(i=nb_chars_printed; i<get_console_width(); i++)
printf(" ");
fflush(stdout);
sleep(1);
}
return NULL;
}
static size_t write_cb(void *contents, size_t size, size_t nmemb, void *userp)
{
int ret;
ret = write(((transfert_t*)userp)->fd, contents, size*nmemb);
if (ret < 0)
printf("Error write" GNU_ERR "\n");
return ret;
}
static int progress_cb(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
transfert_t* t = (transfert_t*)clientp;
double speed_d;
curl_easy_getinfo(t->curl, CURLINFO_SPEED_DOWNLOAD, &speed_d);
t->stats[t->id].dlnow += (dlnow - t->stats[t->id].dllast);
t->stats[t->id].dllast = dlnow;
t->stats[t->id].speed = speed_d;
return 0;
}
void* do_transfert(transfert_t* t)
{
CURLcode res;
char range[64];
unsigned start, end, chunk_size;
curl_easy_setopt(t->curl, CURLOPT_USERAGENT, t->user_agent);
curl_easy_setopt(t->curl, CURLOPT_URL, t->url);
curl_easy_setopt(t->curl, CURLOPT_FOLLOWLOCATION, 1L);
if (t->max_speed)
curl_easy_setopt(t->curl, CURLOPT_MAX_RECV_SPEED_LARGE, t->max_speed);
/* send all data to this function */
curl_easy_setopt(t->curl, CURLOPT_WRITEFUNCTION, write_cb);
/* we pass our 'chunk' struct to the callback function */
curl_easy_setopt(t->curl, CURLOPT_WRITEDATA, (void*)t);
curl_easy_setopt(t->curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt(t->curl, CURLOPT_XFERINFOFUNCTION, progress_cb);
curl_easy_setopt(t->curl, CURLOPT_XFERINFODATA, t);
start = t->start;
if (t->max_chunk_size && (t->end - t->start) > t->max_chunk_size)
chunk_size = t->max_chunk_size;
else
chunk_size = t->end - t->start;
end = start + chunk_size;
while (start < t->end)
{
snprintf(range, sizeof(range), "%u-%u", start, end);
curl_easy_setopt(t->curl, CURLOPT_RANGE, range);
/* Perform the request, res will get the return code */
res = curl_easy_perform(t->curl);
/* Check for errors */
if(res != CURLE_OK)
{
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
break;
}
start += chunk_size + 1;
if (t->max_chunk_size && (t->end - start) > t->max_chunk_size)
chunk_size = t->max_chunk_size;
else
chunk_size = t->end - start;
end = start + chunk_size;
t->stats[t->id].dllast = 0;
}
return NULL;
}
static int configure_transfert(int id, transfert_t* t,
char* url, char* filename,
unsigned start, unsigned end,
unsigned max_speed, char* user_agent,
unsigned max_chunk_size, int* exists)
{
// filename + . + number + \0
unsigned filename_size = strlen(filename)+1+3+1;
struct stat s;
t->curl = curl_easy_init();
if(!t->curl)
return -1;
t->url = url;
t->tmp_filename = malloc(filename_size);
snprintf(t->tmp_filename, filename_size, "%s.%d",
filename, id);
t->id = id;
t->start = start;
t->end = end;
t->max_speed = max_speed;
t->user_agent = user_agent;
t->max_chunk_size = max_chunk_size;
if (stat(t->tmp_filename, &s))
{
*exists = 0;
t->fd = open(t->tmp_filename,
O_WRONLY | O_CREAT,
S_IRUSR|S_IWUSR);
t->already_downloaded = 0;
}
else
{
*exists = 1;
t->fd = open(t->tmp_filename,
O_WRONLY | O_APPEND);
t->start += s.st_size;
t->already_downloaded = s.st_size;
}
if (!t->fd)
{
printf("Opening '%s' failed" GNU_ERR "\n",
t->tmp_filename);
return -1;
}
return 0;
}
static int free_transfert(transfert_t* t)
{
if (t)
{
if (t->curl)
curl_easy_cleanup(t->curl);
if (t->tmp_filename)
free(t->tmp_filename);
}
return 0;
}
static void merge_files(transfert_t* transferts, int nb_threads,
char* out_filename)
{
int fd, fd_tmp, i;
struct stat s;
char* buffer;
rename(transferts[0].tmp_filename, out_filename);
fd = open(out_filename, O_WRONLY | O_APPEND);
for(i=1; i<nb_threads; i++)
{
fd_tmp = open(transferts[i].tmp_filename, O_RDONLY);
stat(transferts[i].tmp_filename, &s);
buffer = malloc(s.st_size);
read(fd_tmp, buffer, s.st_size);
write(fd, buffer, s.st_size);
free(buffer);
close(fd_tmp);
unlink(transferts[i].tmp_filename);
}
close(fd);
}
static int get_file_info(char* url, char* user_agent,
char** filename, unsigned* size,
unsigned quiet)
{
double length;
CURLcode res;
int ret = 0;
unsigned response_code;
char* real_name = NULL;
CURL* curl = curl_easy_init();
if(!curl)
return -1;
curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L);
if (!quiet)
curl_easy_setopt(curl, CURLOPT_HEADER, 1L);
res = curl_easy_perform(curl);
/* Check for errors */
if(res != CURLE_OK)
{
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
ret = -1;
goto end;
}
curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &response_code);
if (response_code != 200)
{
ret = -1;
goto end;
}
curl_easy_getinfo( curl,
CURLINFO_CONTENT_LENGTH_DOWNLOAD, &length );
*size = length;
if (*filename == NULL)
{
curl_easy_getinfo( curl, CURLINFO_EFFECTIVE_URL, &real_name );
if (real_name)
{
*filename = strdup(basename(real_name));
if (!strcmp(*filename, "."))
{
free(*filename);
*filename = strdup(DEFAULT_OUT_FILENAME);
printf("Filename not found, output to %s\n", DEFAULT_OUT_FILENAME);
}
}
}
end:
curl_easy_cleanup(curl);
return ret;
}
static void find_free_file(char** filename)
{
struct stat s;
int cur_index;
char* new_filename;
unsigned new_size;
if (stat(*filename, &s)) return ;
new_size = strlen(*filename) + 1 + 2 + 1;
new_filename = malloc(new_size);
for (cur_index = 1; cur_index < 100; cur_index++)
{
snprintf(new_filename, new_size, "%s.%d",
*filename, cur_index);
if (stat(new_filename, &s))
{
free(*filename);
*filename = new_filename;
return ;
}
}
free(*filename);
*filename = NULL;
}
static void usage(char* program_name)
{
printf("%s: Parallel HTTP file download\n", program_name);
printf("usage: %s [-n nb_threads] [-l speed_limit] [-o out_filename] [-u user_agent] [-m max_chunk_size[kKmMgG]] [-q] [-h] url\n",
program_name);
printf("\t-n : Specify number of threads (default : %d)\n", DEFAULT_NB_THREADS);
printf("\t-l : Download speed limit for all threads (not per thread)\n");
printf("\t-o : Out filename, default is retrieved by GET request or '%s' if not found\n",
DEFAULT_OUT_FILENAME);
printf("\t-u : User agent, default is '%s'\n", DEFAULT_USER_AGENT);
printf("\t-m : Max chunk size in bytes\n");
printf("\t-q : Quiet mode\n");
printf("\t-h : Display help\n");
}
int main(int argc, char** argv)
{
pthread_t display_thread = 0;
unsigned nb_threads = DEFAULT_NB_THREADS;
char* user_agent = strdup(DEFAULT_USER_AGENT), *endptr;
stats_t* stats = NULL;
int ret = -1, i;
stats_params_t stats_params;
transfert_t* transferts = NULL;
unsigned total_size, thread_size, multiplier=1;
double displayed_size;
unsigned start, end, max_speed = 0, quiet = 0, max_chunk_size = 0;
char* out_filename = NULL, *url = NULL, c;
char* suffix = "B";
void* res;
int opt;
struct timeval time_start, time_end;
struct tm * full_time;
double average_speed;
int continuous_mode=0;
int exists;
while ((opt = getopt(argc, argv, "hl:m:n:o:qu:")) != -1) {
switch (opt) {
case 'l':
max_speed = strtoul(optarg, &endptr, 0);
if (*endptr)
{
usage(argv[0]);
return 1;
}
break;
case 'm':
if (strlen(optarg) > 1)
{
c = optarg[strlen(optarg)-1];
if (c < '0' || c > '9')
{
switch(c)
{
case 'g':
case 'G':
multiplier *= 1024;
case 'm':
case 'M':
multiplier *= 1024;
case 'k':
case 'K':
multiplier *= 1024;
optarg[strlen(optarg)-1] = 0;
break;
default:
usage(argv[0]);
return 1;
}
}
}
max_chunk_size = strtoul(optarg, &endptr, 0);
if (*endptr)
{
usage(argv[0]);
return 1;
}
max_chunk_size *= multiplier;
break;
case 'n':
nb_threads = strtoul(optarg, &endptr, 0);
if (*endptr)
{
usage(argv[0]);
return 1;
}
if (nb_threads == 0)
nb_threads = DEFAULT_NB_THREADS;
else if (nb_threads > MAX_NB_THREADS)
{
printf("Max numberb of threads is %d\n", MAX_NB_THREADS);
nb_threads = MAX_NB_THREADS;
}
break;
case 'o':
out_filename = strdup(optarg);
break;
case 'q':
quiet = 1;
break;
case 'u':
user_agent = strdup(optarg);
break;
default:
case 'h':
usage(argv[0]);
return 0;
}
}
if (optind != argc-1)
{
usage(argv[0]);
return 0;
}
else
url = argv[optind];
if (get_file_info(url, user_agent,
&out_filename, &total_size,
quiet))
return -1;
if (!out_filename)
out_filename = strdup(DEFAULT_OUT_FILENAME);
find_free_file(&out_filename);
if (out_filename == NULL)
{
printf("Unable to find free file to write to !\n");
goto end;
}
else
{
displayed_size = (double)total_size;
suffix = "B";
if (displayed_size > (1024))
{
displayed_size /= 1024.0;
suffix = "kB";
}
if (displayed_size > (1024))
{
displayed_size /= 1024.0;
suffix = "MB";
}
if (displayed_size > (1024))
{
displayed_size /= 1024.0;
suffix = "GB";
}
printf("Save in '%s' (%.2f%s)\n\n", out_filename, displayed_size, suffix);
}
stats = malloc(sizeof(*stats)*nb_threads);
memset(stats, 0, sizeof(*stats)*nb_threads);
transferts = malloc(sizeof(*transferts)*(nb_threads+1));
memset(transferts, 0, sizeof(*transferts)*(nb_threads+1));
thread_size = total_size/nb_threads;
max_speed /= nb_threads;
for(i=0; i<nb_threads; i++)
{
transferts[i].stats = stats;
start = thread_size*i;
if (i < (nb_threads-1))
end = thread_size*(i+1)-1;
else
end = total_size;
ret = configure_transfert(i, &transferts[i], url, out_filename,
start, end, max_speed, user_agent,
max_chunk_size, &exists);
if (ret)
goto end;
transferts[i].stats[i].dltotal = transferts[i].already_downloaded +
(end - start);
transferts[i].stats[i].dlnow = transferts[i].already_downloaded;
transferts[i].stats[i].dllast = 0;
// First set continuous mode
if (i == 0)
{
continuous_mode = exists;
}
else
{
// Check valid continuous mode or not
if (exists ^ continuous_mode)
{
printf("Error : you already started to download this file with a different number of thread, please clear temporary files or restart with the same number of threads\n");
goto end;
}
}
}
// Check for last temporary file
configure_transfert(i, &transferts[i], url, out_filename,
start, end, max_speed, user_agent, max_chunk_size,
&exists);
unlink(transferts[i].tmp_filename);
if (exists)
{
printf("Error : you already started to download this file with a different number of thread, please clear temporary files or restart with the same number of threads\n");
free_transfert(&transferts[i]);
goto end;
}
stats_params.end = 0;
stats_params.nb_threads = nb_threads;
stats_params.stats = stats;
if (!quiet)
{
pthread_create(&display_thread, NULL,
(void*(*)(void*))display_stats,
(void*) &stats_params);
}
gettimeofday(&time_start, NULL);
for(i=0; i<nb_threads; i++)
{
pthread_create(&transferts[i].thread, NULL,
(void*(*)(void*))do_transfert,
(void*) &transferts[i]);
}
for(i=0; i<nb_threads; i++)
{
pthread_join(transferts[i].thread, &res);
}
gettimeofday(&time_end, NULL);
merge_files(transferts, nb_threads, out_filename);
if (!quiet)
{
full_time = localtime((const time_t*)&time_end.tv_sec);
average_speed = (double)total_size / (double)(time_end.tv_sec - time_start.tv_sec);
if (average_speed < (1024*1024))
{
average_speed /= 1024;
suffix = "kB";
}
else
{
average_speed /= 1024*1024;
suffix = "MB";
}
printf("\n\n%04d-%02d-%02d %02d:%02d:%02d (%.2f%s/s) '%s' saved\n",
full_time->tm_year+1900, full_time->tm_mon, full_time->tm_mday,
full_time->tm_hour, full_time->tm_min, full_time->tm_sec,
average_speed, suffix, out_filename);
}
ret = 0;
end:
if (stats)
free(stats);
free(user_agent);
free(out_filename);
stats_params.end = 1;
for(i=0; i<nb_threads; i++)
free_transfert(&transferts[i]);
if (!quiet)
pthread_join(display_thread, &res);
if (transferts)
free(transferts);
if (!quiet)
printf("\n");
return ret;
}