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