1 /*
2     libgutenfetch - a small library to help developers of utilities to list
3 	and fetch books available through Project Gutenberg.
4 
5     Copyright (C) 2001, 2002, 2003, 2004 Russell Francis
6 
7     This program is free software; you can redistribute it and/or modify
8     it under the terms of the GNU General Public License as published by
9     the Free Software Foundation; either version 2 of the License, or
10     (at your option) any later version.
11 
12     This program is distributed in the hope that it will be useful,
13     but WITHOUT ANY WARRANTY; without even the implied warranty of
14     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15     GNU General Public License for more details.
16 
17     You should have received a copy of the GNU General Public License
18     along with this program; if not, write to the
19 
20 	Free Software Foundation, Inc.
21 	59 Temple Place, Suite 330
22 	Boston, MA  02111-1307  USA
23 
24 Last updated on
25 $Date: 2004/07/07 02:41:22 $
26 $Author: johntabularasa $.
27 
28 */
29 
30 #include "stddefs.h"
31 #include "gutenfetch.h"
32 #include "libgutenfetch_cache.h"
33 #include "libgutenfetch_init.h"
34 #include "libgutenfetch_utility.h"
35 #include <curl/curl.h>
36 #ifdef HAVE_FCNTL_H
37 #	include <fcntl.h>
38 #endif /* HAVE_FCNTL_H */
39 #ifdef HAVE_SYS_TYPES_H
40 #	include <sys/types.h>
41 #endif /* HAVE_SYS_TYPES_H */
42 #ifdef HAVE_SYS_STAT_H
43 #	include <sys/stat.h>
44 #endif /* HAVE_SYS_STAT_H */
45 #ifdef HAVE_ERRNO_H
46 #	include <errno.h>
47 #endif /* HAVE_ERRNO_H */
48 #ifdef HAVE_ASSERT_H
49 #	include <assert.h>
50 #endif /* HAVE_ASSERT_H */
51 #ifdef HAVE_STRING_H
52 #	include <string.h>
53 #endif /* HAVE_STRING_H */
54 #ifdef HAVE_STRINGS_H
55 #	include <strings.h>
56 #endif /* HAVE_STRINGS_H */
57 
58 static int
59 	gutenfetch_curl_progress_func(void *, double, double, double, double);
60 
61 typedef struct {
62 	void *data;
63 	int (*func)(void *, double, double, double, const char *);
64 	char msg[4096];
65 } progress_struct_t;
66 
67 extern int errno;
68 static int cache_enabled = FALSE;
69 static char *cache_base_dir = NULL;
70 static time_t expires = 0;
71 
72 
73 /**
74  * gutenfetch_curl_write
75  *
76  * This is a private function which is called by
77  * CURL periodically to write the downloaded data
78  * to the file descriptor passed in through data.
79  *
80  * @param ptr The buffer of data which CURL downloaded.
81  * @param size The size of each element in the buffer.
82  * @param nelem The number of elements in the buffer.
83  * @param data User supplied pointer to file descriptor where
84  *			buffer should be written to.
85  * @return Same as write, number of bytes written on success,
86  *			-1 and errno is set on failure.  CURL will abort
87  *			a transfer on failure.
88  */
89 size_t
gutenfetch_curl_write(void * ptr,size_t size,size_t nelem,void * data)90 gutenfetch_curl_write(
91 	void *ptr,
92 	size_t size,
93 	size_t nelem,
94 	void *data)
95 {
96 	int fd;
97 
98 	if (data == NULL) {
99 		return -1;
100 	}
101 
102 	fd = *(int*)data;
103 
104 	if (fd < 0) {
105 		return -1;
106 	}
107 
108 	return write(fd, ptr, size * nelem);
109 }
110 
111 
112 /**
113  * gutenfetch_curl_progress_func
114  *
115  * Called periodically by CURL to display the progress of
116  * a current download.
117  *
118  * @param data User specified data to pass to func.
119  * @param dltotal The total filesize of the download.
120  * @param dlnow The amount we have downloaded.
121  * @param ultotal The total filesize of the upload.
122  * @param ulnow The amount we have uploaded.
123  * @return An integer from the user supplied function.
124  *		returning anything but 0 will cause CURL to
125  *		abort the transfer.
126  */
127 int
gutenfetch_curl_progress_func(void * data,double dltotal,double dlnow,double ultotal,double ulnow)128 gutenfetch_curl_progress_func(
129 	void *data, double dltotal, double dlnow, double ultotal, double ulnow)
130 {
131 	progress_struct_t *pstruct = data;
132 	double total_progress = 0.0;
133 
134 	assert(pstruct != NULL);
135 	assert(pstruct->func != NULL);
136 
137 	if (dltotal != 0) {
138 		total_progress = dlnow / dltotal;
139 	}
140 
141 	return pstruct->func(
142 		pstruct->data, total_progress,
143 		dltotal, dlnow, pstruct->msg);
144 }
145 
146 
147 /**
148  * gutenfetch_cache_init()
149  *
150  * Initialize resources used by the cacheing module
151  * of libgutenfetch.
152  *
153  * @param should_enable TRUE if we should enable caching.
154  * @return
155  */
156 gutenfetch_error_t
gutenfetch_cache_init(int should_enable)157 gutenfetch_cache_init(int should_enable)
158 {
159 	return gutenfetch_cache_enable(should_enable);
160 }
161 
162 
163 /**
164  * gutenfetch_cache_shutdown
165  *
166  * Release all resources held by the cache.
167  *
168  */
169 void
gutenfetch_cache_shutdown(void)170 gutenfetch_cache_shutdown(void)
171 {
172 	FREE_NULL(cache_base_dir);
173 }
174 
175 gutenfetch_error_t
gutenfetch_cache_enable(int e)176 gutenfetch_cache_enable(int e)
177 {
178 	char *home_dir;
179 	int retval;
180 
181 	if (e == TRUE) {
182 		home_dir = gutenfetch_util_get_home_directory();
183 		if (home_dir != NULL) {
184 			FREE_NULL(cache_base_dir);
185 			cache_base_dir = gutenfetch_util_strcat(home_dir,
186 				"/.libgutenfetch-cache", NULL);
187 			if (cache_base_dir != NULL) {
188 				retval = mkdir(cache_base_dir, S_IRUSR | S_IWUSR | S_IXUSR );
189 				if ((retval == 0) ||
190 				((retval == -1) && (errno == EEXIST))){  /* Success */
191 					home_dir = gutenfetch_util_strcat(cache_base_dir, DIR_SEPARATOR, NULL);
192 					FREE_NULL(cache_base_dir);
193 					cache_base_dir = home_dir;
194 					cache_enabled = TRUE;
195 					expires = (60 * 60 * 24 * 7); // 1 week
196 				} else {
197 					cache_enabled = FALSE;
198 					FREE_NULL(cache_base_dir);
199 					return GUTENFETCH_UNABLE_TO_INIT_CACHE;
200 				}
201 			}
202 		}
203 	} else {
204 		cache_enabled = FALSE;
205 		FREE_NULL(cache_base_dir);
206 	}
207 	return GUTENFETCH_OK;
208 }
209 
210 int
gutenfetch_cache_is_enabled(void)211 gutenfetch_cache_is_enabled(void)
212 {
213 	return cache_enabled;
214 }
215 
216 void
gutenfetch_cache_set_expires(time_t t)217 gutenfetch_cache_set_expires(time_t t)
218 {
219 	expires = t;
220 }
221 
222 time_t
gutenfetch_cache_get_expires(void)223 gutenfetch_cache_get_expires(void)
224 {
225 	return expires;
226 }
227 
228 /**
229  * gutenfetch_cache_flush
230  *
231  * Remove all files in the cache.
232  */
233 gutenfetch_error_t
gutenfetch_cache_flush(void)234 gutenfetch_cache_flush(void)
235 {
236 	if ((cache_enabled == TRUE) && (cache_base_dir != NULL)) {
237 		gutenfetch_util_rm_below_dir(cache_base_dir);
238 	}
239 	return GUTENFETCH_OK;
240 }
241 
242 /**
243  * gutenfetch_cache_fetch
244  *
245  * This function looks for a file in the cache, if it
246  * finds it, it returns a file descriptor for reading
247  * if it doesn't it fetchs the file, stores it in the
248  * cache and returns a file descriptor for reading.
249  * it it can't do that it returns -1.
250  *
251  * @param loc Whether we should look in australian or non-australian server.
252  * @param file The file we are looking for.
253  * @param progress_func NULL or a pointer of a function to display progress
254  * 	to.
255  * @param progress_func_data Optional user supplied data to progress function.
256  * @return A valid file descriptor or -1.
257  */
258 int
gutenfetch_cache_fetch(server_location_t loc,const char * file,int (* progress_func)(void *,double,double,double,const char *),void * progress_func_data)259 gutenfetch_cache_fetch(
260 	server_location_t loc,
261 	const char *file,
262 	int (*progress_func)(void *, double, double, double, const char *),
263 	void *progress_func_data)
264 {
265 	CURL *handle;
266 	progress_struct_t progress;
267 	gutenfetch_server_t *server;
268 	char *cache_filename = NULL;
269 	char *temp_filename = NULL;
270 	struct stat sb;
271 	time_t now;
272 	int fd = -1;
273 	char *url;
274 
275 	if (file == NULL)
276 		return -1;
277 
278 	/* Look for the file in the cache */
279 	if ((cache_enabled == TRUE) && (cache_base_dir != NULL)) {
280 		if ((strlen(file) >= 2) && (file[0] == '/')) {
281 			cache_filename = gutenfetch_util_strcat(cache_base_dir, &file[1], NULL);
282 		} else {
283 			cache_filename = gutenfetch_util_strcat(cache_base_dir, file, NULL);
284 		}
285 		if (cache_filename != NULL) {
286 			if(stat(cache_filename, &sb) != -1) {
287 				now = time(NULL);
288 				if ((now - sb.st_mtime) < expires) { /* in cache and valid. */
289 					fd = open(cache_filename, O_RDONLY);
290 					FREE_NULL(cache_filename);
291 					return fd;
292 				} else {
293 					unlink(cache_filename);
294 				}
295 			}
296 		}
297 	}
298 
299 
300 	if (fd == -1) { 	/* The file doesn't exist in the cache */
301 		// lets try to open a temp file to write this to.
302 		fd = gutenfetch_util_get_temp_file(&temp_filename);
303 	}
304 
305 	/* if we still don't have something, fail! */
306 	if ((fd == -1) || (temp_filename == NULL)) {
307 		FREE_NULL(temp_filename);
308 		FREE_NULL(cache_filename);
309 		close(fd);
310 		return -1;
311 	}
312 
313 	/* Get the server. */
314 	/* XXX The australian server is not accessible from the US,
315 	   if there is anyone who can get to it and wants to test
316  	   this code please feel free.
317 	 */
318 //	if (loc == NON_AUSTRALIAN) {
319 		server = gutenfetch_get_active_server();
320 //	} else {
321 //		server = gutenfetch_get_aussie_server();
322 //	}
323 
324 	if (server == NULL) {
325 		if (fd != -1) {
326 			close(fd);
327 		}
328 		FREE_NULL(cache_filename);
329 		return -1;
330 	}
331 
332 	/* Build the URL */
333 	url = gutenfetch_util_build_URL(server, file);
334 	gutenfetch_free_server(server);
335 
336 	handle = gutenfetch_init_curl_handle();
337 	if (handle == NULL) {
338 		if (fd != -1) {
339 			close (fd);
340 		}
341 		FREE_NULL(cache_filename);
342 		FREE_NULL(url);
343 		return -1;
344 	}
345 
346 	// Assign the URL to curl.
347 	curl_easy_setopt(handle, CURLOPT_URL, url);
348 
349 	// Setup our progress display function.
350 	if (progress_func != NULL) {
351 		progress.data = progress_func_data;
352 		progress.func = progress_func;
353 		snprintf(progress.msg, 4096, "Downloading %s", file);
354 		curl_easy_setopt(handle, CURLOPT_NOPROGRESS, FALSE);
355 		curl_easy_setopt(handle, CURLOPT_PROGRESSFUNCTION,
356 			gutenfetch_curl_progress_func);
357 		curl_easy_setopt(handle, CURLOPT_PROGRESSDATA,
358 			&progress);
359 	} else {
360 		curl_easy_setopt(handle, CURLOPT_NOPROGRESS, TRUE);
361 	}
362 
363 	// setup our write function
364 	curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION,
365 		gutenfetch_curl_write);
366 
367 	// fd is guaranteed to be valid here!
368 	curl_easy_setopt(handle, CURLOPT_WRITEDATA, &fd);
369 	if (curl_easy_perform(handle) != 0) {
370 		FREE_NULL(cache_filename);
371 		close(fd);
372 	} else {
373 		lseek(fd, 0, SEEK_SET);
374 	}
375 	FREE_NULL(url);
376 
377 	if ((cache_enabled == TRUE) && (cache_base_dir != NULL)) {
378 		if(gutenfetch_util_move(temp_filename, cache_filename) == 1) {
379 			unlink(temp_filename);
380 			close(fd); /* close the tempfile */
381 			fd = open(cache_filename, O_RDONLY);
382 			if (fd > 0) {
383 				lseek(fd, 0, SEEK_SET);
384 			}
385 		}
386 	}
387 
388 	FREE_NULL(temp_filename);
389 	FREE_NULL(cache_filename);
390 	return fd;
391 }
392 
393 /**
394  * gutenfetch_cache_flush_old
395  *
396  * Flush all old entries from the cache.
397  */
398 void
gutenfetch_cache_flush_old(void)399 gutenfetch_cache_flush_old(void)
400 {
401 	if ((cache_enabled == TRUE) && (cache_base_dir != NULL)) {
402 		gutenfetch_util_rm_old_below_dir(expires, cache_base_dir);
403 	}
404 }
405