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