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