e17679f0eea1d6ec49f9ca1adf80757e087d4666
[assman] / src / mod_url.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <errno.h>
5 #include "assman_impl.h"
6 #include "tpool.h"
7 #include "md4.h"
8
9 #ifdef BUILD_MOD_URL
10 #include <pthread.h>
11 #include <curl/curl.h>
12
13 enum {
14         DL_UNKNOWN,
15         DL_STARTED,
16         DL_ERROR,
17         DL_DONE
18 };
19
20 struct file_info {
21         char *url;
22         char *cache_fname;
23
24         FILE *cache_file;
25
26         /* fopen-thread waits until the state becomes known (request starts transmitting or fails) */
27         int state;
28         pthread_cond_t state_cond;
29         pthread_mutex_t state_mutex;
30 };
31
32 static void *fop_open(const char *fname, void *udata);
33 static void fop_close(void *fp, void *udata);
34 static long fop_seek(void *fp, long offs, int whence, void *udata);
35 static long fop_read(void *fp, void *buf, long size, void *udata);
36
37 static void exit_cleanup(void);
38 static void download(void *data);
39 static size_t recv_callback(char *ptr, size_t size, size_t nmemb, void *udata);
40 static const char *get_temp_dir(void);
41
42 static char *tmpdir, *cachedir;
43 static struct thread_pool *tpool;
44 static CURL **curl;
45
46 struct ass_fileops *ass_alloc_url(const char *url)
47 {
48         static int done_init;
49         struct ass_fileops *fop;
50         int i, len;
51         char *ptr;
52
53         if(!done_init) {
54                 curl_global_init(CURL_GLOBAL_ALL);
55                 atexit(exit_cleanup);
56
57                 if(!*ass_mod_url_cachedir) {
58                         strcpy(ass_mod_url_cachedir, "assman_cache");
59                 }
60                 tmpdir = (char*)get_temp_dir();
61                 if(!(cachedir = malloc(strlen(ass_mod_url_cachedir) + strlen(tmpdir) + 2))) {
62                         fprintf(stderr, "assman: failed to allocate cachedir path buffer\n");
63                         goto init_failed;
64                 }
65                 sprintf(cachedir, "%s/%s", tmpdir, ass_mod_url_cachedir);
66
67                 if(ass_mod_url_max_threads <= 0) {
68                         ass_mod_url_max_threads = 8;
69                 }
70
71                 if(!(curl = calloc(ass_mod_url_max_threads, sizeof *curl))) {
72                         perror("assman: failed to allocate curl context table");
73                         goto init_failed;
74                 }
75                 for(i=0; i<ass_mod_url_max_threads; i++) {
76                         if(!(curl[i] = curl_easy_init())) {
77                                 goto init_failed;
78                         }
79                         curl_easy_setopt(curl[i], CURLOPT_WRITEFUNCTION, recv_callback);
80                 }
81
82                 if(!(tpool = ass_tpool_create(ass_mod_url_max_threads))) {
83                         fprintf(stderr, "assman: failed to create thread pool\n");
84                         goto init_failed;
85                 }
86
87                 done_init = 1;
88         }
89
90         if(!(fop = malloc(sizeof *fop))) {
91                 return 0;
92         }
93         len = strlen(url);
94         if(!(fop->udata = malloc(len + 1))) {
95                 free(fop);
96                 return 0;
97         }
98         memcpy(fop->udata, url, len + 1);
99         if(len) {
100                 ptr = (char*)fop->udata + len - 1;
101                 while(*ptr == '/') *ptr-- = 0;
102         }
103
104         fop->open = fop_open;
105         fop->close = fop_close;
106         fop->seek = fop_seek;
107         fop->read = fop_read;
108         return fop;
109
110 init_failed:
111         free(cachedir);
112         if(curl) {
113                 for(i=0; i<ass_mod_url_max_threads; i++) {
114                         if(curl[i]) {
115                                 curl_easy_cleanup(curl[i]);
116                         }
117                 }
118                 free(curl);
119         }
120         return 0;
121 }
122
123 static void exit_cleanup(void)
124 {
125         int i;
126
127         if(tpool) {
128                 ass_tpool_destroy(tpool);
129         }
130         if(curl) {
131                 for(i=0; i<ass_mod_url_max_threads; i++) {
132                         if(curl[i]) {
133                                 curl_easy_cleanup(curl[i]);
134                         }
135                 }
136                 free(curl);
137         }
138         curl_global_cleanup();
139 }
140
141
142 void ass_free_url(struct ass_fileops *fop)
143 {
144 }
145
146 static char *cache_filename(const char *fname, const char *url_prefix)
147 {
148         MD4_CTX md4ctx;
149         unsigned char sum[16];
150         char sumstr[33];
151         char *resfname;
152         int i;
153         int fname_len = strlen(fname);
154         int prefix_len = strlen(url_prefix);
155         int url_len = fname_len + prefix_len + 1;
156
157         char *url = alloca(url_len + 1);
158         sprintf(url, "%s/%s", url_prefix, fname);
159
160         MD4Init(&md4ctx);
161         MD4Update(&md4ctx, (unsigned char*)url, url_len);
162         MD4Final((unsigned char*)sum, &md4ctx);
163
164         for(i=0; i<16; i++) {
165                 sprintf(sumstr + i * 2, "%x", (unsigned int)sum[i]);
166         }
167         sumstr[32] = 0;
168
169         prefix_len = strlen(cachedir);
170         if(!(resfname = malloc(prefix_len + fname_len + 64))) {
171                 return 0;
172         }
173         sprintf(resfname, "%s/%s-%s", cachedir, fname, sumstr);
174         return resfname;
175 }
176
177 static void *fop_open(const char *fname, void *udata)
178 {
179         struct file_info *file;
180         int state;
181         char *prefix = udata;
182
183         if(!fname || !*fname) {
184                 ass_errno = ENOENT;
185                 return 0;
186         }
187
188         if(!(file = malloc(sizeof *file))) {
189                 ass_errno = ENOMEM;
190                 return 0;
191         }
192         if(!(file->cache_fname = cache_filename(fname, udata))) {
193                 free(file);
194                 ass_errno = ENOMEM;
195                 return 0;
196         }
197         printf("assman: mod_url cache file: %s\n", file->cache_fname);
198         if(!(file->cache_file = fopen(file->cache_fname, "wb"))) {
199                 fprintf(stderr, "assman: mod_url: failed to open cache file (%s) for writing: %s\n",
200                                 file->cache_fname, strerror(errno));
201                 ass_errno = errno;
202                 free(file->cache_fname);
203                 free(file);
204                 return 0;
205         }
206
207         if(!(file->url = malloc(strlen(prefix) + strlen(fname) + 2))) {
208                 perror("assman: mod_url: failed to allocate url buffer");
209                 ass_errno = errno;
210                 fclose(file->cache_file);
211                 remove(file->cache_fname);
212                 free(file->cache_fname);
213                 free(file);
214                 return 0;
215         }
216         if(prefix && *prefix) {
217                 sprintf(file->url, "%s/%s", prefix, fname);
218         } else {
219                 strcpy(file->url, fname);
220         }
221
222         file->state = DL_UNKNOWN;
223         pthread_mutex_init(&file->state_mutex, 0);
224         pthread_cond_init(&file->state_cond, 0);
225
226         ass_tpool_enqueue(tpool, file, download, 0);
227
228         /* wait until the file changes state */
229         pthread_mutex_lock(&file->state_mutex);
230         while(file->state == DL_UNKNOWN) {
231                 pthread_cond_wait(&file->state_cond, &file->state_mutex);
232         }
233         state = file->state;
234         pthread_mutex_unlock(&file->state_mutex);
235
236         if(state == DL_ERROR) {
237                 /* the worker stopped, so we can safely cleanup and return error */
238                 fclose(file->cache_file);
239                 remove(file->cache_fname);
240                 free(file->cache_fname);
241                 pthread_cond_destroy(&file->state_cond);
242                 pthread_mutex_destroy(&file->state_mutex);
243                 free(file);
244                 ass_errno = ENOENT;     /* TODO: differentiate between 403 and 404 */
245                 return 0;
246         }
247         return file;
248 }
249
250 static void wait_done(struct file_info *file)
251 {
252         pthread_mutex_lock(&file->state_mutex);
253         while(file->state != DL_DONE && file->state != DL_ERROR) {
254                 pthread_cond_wait(&file->state_cond, &file->state_mutex);
255         }
256         pthread_mutex_unlock(&file->state_mutex);
257 }
258
259 static void fop_close(void *fp, void *udata)
260 {
261         struct file_info *file = fp;
262
263         wait_done(file);        /* TODO: stop download instead of waiting to finish */
264
265         fclose(file->cache_file);
266         if(file->state == DL_ERROR) remove(file->cache_fname);
267         free(file->cache_fname);
268         pthread_cond_destroy(&file->state_cond);
269         pthread_mutex_destroy(&file->state_mutex);
270         free(file);
271 }
272
273 static long fop_seek(void *fp, long offs, int whence, void *udata)
274 {
275         struct file_info *file = fp;
276         wait_done(file);
277
278         fseek(file->cache_file, offs, whence);
279         return ftell(file->cache_file);
280 }
281
282 static long fop_read(void *fp, void *buf, long size, void *udata)
283 {
284         struct file_info *file = fp;
285         wait_done(file);
286
287         return fread(buf, 1, size, file->cache_file);
288 }
289
290 /* this is the function called by the worker threads to perform the download
291  * signal state changes, and prepare the cache file for reading
292  */
293 static void download(void *data)
294 {
295         int tid, res;
296         struct file_info *file = data;
297
298         tid = ass_tpool_thread_id(tpool);
299
300         curl_easy_setopt(curl[tid], CURLOPT_URL, file->url);
301         curl_easy_setopt(curl[tid], CURLOPT_WRITEDATA, file);
302         res = curl_easy_perform(curl[tid]);
303
304         pthread_mutex_lock(&file->state_mutex);
305         if(res == CURLE_OK) {
306                 file->state = DL_DONE;
307                 fclose(file->cache_file);
308                 if(!(file->cache_file = fopen(file->cache_fname, "rb"))) {
309                         fprintf(stderr, "assman: failed to reopen cache file (%s) for reading: %s\n",
310                                         file->cache_fname, strerror(errno));
311                         file->state = DL_ERROR;
312                 }
313         } else {
314                 file->state = DL_ERROR;
315         }
316         pthread_cond_broadcast(&file->state_cond);
317         pthread_mutex_unlock(&file->state_mutex);
318 }
319
320 /* this function is called by curl to pass along downloaded data chunks */
321 static size_t recv_callback(char *ptr, size_t size, size_t count, void *udata)
322 {
323         struct file_info *file = udata;
324
325         pthread_mutex_lock(&file->state_mutex);
326         if(file->state == DL_UNKNOWN) {
327                 file->state = DL_STARTED;
328                 pthread_cond_broadcast(&file->state_cond);
329         }
330         pthread_mutex_unlock(&file->state_mutex);
331
332         return fwrite(ptr, size, count, file->cache_file);
333 }
334
335 #ifdef WIN32
336 #include <windows.h>
337
338 static const char *get_temp_dir(void)
339 {
340         static char buf[MAX_PATH + 1];
341         GetTempPathA(MAX_PATH + 1, buf);
342         return buf;
343 }
344 #else   /* UNIX */
345 static const char *get_temp_dir(void)
346 {
347         char *env = getenv("TMPDIR");
348         return env ? env : "/tmp";
349 }
350 #endif
351
352
353
354 #else   /* don't build mod_url */
355 struct ass_fileops *ass_alloc_url(const char *url)
356 {
357         fprintf(stderr, "assman: compiled without URL asset source support\n");
358         return 0;
359 }
360
361 void ass_free_url(struct ass_fileops *fop)
362 {
363 }
364 #endif