]> Pileus Git - grits/blob - src/data/grits-http.c
Add grits_http_abort function
[grits] / src / data / grits-http.c
1 /*
2  * Copyright (C) 2009-2011 Andy Spencer <andy753421@gmail.com>
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 /**
19  * SECTION:grits-http
20  * @short_description: Hyper Text Transfer Protocol
21  *
22  * #GritsHttp is a small wrapper around libsoup to provide data access using
23  * the Hyper Text Transfer Protocol. Each #GritsHttp should be associated with
24  * a particular server or dataset, all the files downloaded for this dataset
25  * will be cached together in $HOME/.cache/grits/
26  */
27
28 #include <config.h>
29 #include <glib.h>
30 #include <glib/gstdio.h>
31 #include <libsoup/soup.h>
32
33 #include "grits-http.h"
34
35 gchar *_get_cache_path(GritsHttp *http, const gchar *local)
36 {
37         return g_build_filename(g_get_user_cache_dir(), PACKAGE,
38                         http->prefix, local, NULL);
39 }
40
41 /**
42  * grits_http_new:
43  * @prefix: The prefix in the cache to store the downloaded files.
44  *          For example: * "/nexrad/level2/".
45  *
46  * Create a new #GritsHttp for the given prefix
47  *
48  * Returns: the new #GritsHttp
49  */
50 GritsHttp *grits_http_new(const gchar *prefix)
51 {
52         g_debug("GritsHttp: new - %s", prefix);
53         GritsHttp *http = g_new0(GritsHttp, 1);
54         http->soup = soup_session_sync_new();
55         http->prefix = g_strdup(prefix);
56         g_object_set(http->soup, "user-agent", PACKAGE_STRING, NULL);
57         g_object_set(http->soup, "timeout",    10,             NULL);
58         return http;
59 }
60
61 /**
62  * grits_http_abort:
63  * @http: the #GritsHttp to abort
64  *
65  * Cancels any pending requests and prevents new requests.
66  */
67 void grits_http_abort(GritsHttp *http)
68 {
69         g_debug("GritsHttp: abort - %s", http->prefix);
70         http->aborted = TRUE;
71         soup_session_abort(http->soup);
72 }
73
74 /**
75  * grits_http_free:
76  * @http: the #GritsHttp to free
77  *
78  * Frees resources used by @http and cancels any pending requests.
79  */
80 void grits_http_free(GritsHttp *http)
81 {
82         g_debug("GritsHttp: free - %s", http->prefix);
83         g_object_unref(http->soup);
84         g_free(http->prefix);
85         g_free(http);
86 }
87
88 /* For passing data to the chunck callback */
89 struct _CacheInfo {
90         FILE  *fp;
91         gchar *path;
92         GritsChunkCallback callback;
93         gpointer user_data;
94 };
95 struct _CacheInfoMain {
96         gchar *path;
97         GritsChunkCallback callback;
98         gpointer user_data;
99         goffset cur, total;
100 };
101
102 /* call the user callback from the main thread,
103  * since it's usually UI updates */
104 static gboolean _chunk_main_cb(gpointer _infomain)
105 {
106         struct _CacheInfoMain *infomain = _infomain;
107         infomain->callback(infomain->path,
108                         infomain->cur, infomain->total,
109                         infomain->user_data);
110         g_free(infomain);
111         return FALSE;
112 }
113
114 /**
115  * Append data to the file and call the users callback if they supplied one.
116  */
117 static void _chunk_cb(SoupMessage *message, SoupBuffer *chunk, gpointer _info)
118 {
119         struct _CacheInfo *info = _info;
120
121         if (!SOUP_STATUS_IS_SUCCESSFUL(message->status_code)) {
122                 g_warning("GritsHttp: _chunk_cb - soup failed with %d",
123                                 message->status_code);
124                 return;
125         }
126
127         if (!fwrite(chunk->data, chunk->length, 1, info->fp))
128                 g_error("GritsHttp: _chunk_cb - Unable to write data");
129
130         if (info->callback) {
131                 struct _CacheInfoMain *infomain = g_new0(struct _CacheInfoMain, 1);
132                 infomain->path      = info->path;
133                 infomain->callback  = info->callback;
134                 infomain->user_data = info->user_data;
135                 infomain->cur       = ftell(info->fp);
136                 goffset st=0, end=0;
137                 soup_message_headers_get_content_range(message->response_headers,
138                                 &st, &end, &infomain->total);
139                 g_idle_add(_chunk_main_cb, infomain);
140         }
141
142 }
143
144 /**
145  * grits_http_fetch:
146  * @http:      the #GritsHttp connection to use
147  * @uri:       the URI to fetch
148  * @local:     the local name to give to the file
149  * @mode:      the update type to use when fetching data
150  * @callback:  callback to call when a chunk of data is received
151  * @user_data: user data to pass to the callback
152  *
153  * Fetch a file from the cache. Whether the file is actually loaded from the
154  * remote server depends on the value of @mode.
155  *
156  * Returns: The local path to the complete file
157  */
158 /* TODO: use .part extentions and continue even when using GRITS_ONCE */
159 gchar *grits_http_fetch(GritsHttp *http, const gchar *uri, const char *local,
160                 GritsCacheType mode, GritsChunkCallback callback, gpointer user_data)
161 {
162         g_debug("GritsHttp: fetch - %s mode=%d", local, mode);
163         if (http->aborted) {
164                 g_debug("GritsPluginSat: _load_tile_thread - aborted");
165                 return NULL;
166         }
167         gchar *path = _get_cache_path(http, local);
168
169         /* Unlink the file if we're refreshing it */
170         if (mode == GRITS_REFRESH)
171                 g_remove(path);
172
173         /* Do the cache if necessasairy */
174         if (!(mode == GRITS_ONCE && g_file_test(path, G_FILE_TEST_EXISTS)) &&
175                         mode != GRITS_LOCAL) {
176                 g_debug("GritsHttp: fetch - Caching file %s", local);
177
178                 /* Open the file for writting */
179                 gchar *part = path;
180                 if (!g_file_test(path, G_FILE_TEST_EXISTS))
181                         part = g_strdup_printf("%s.part", path);
182                 FILE *fp = fopen_p(part, "ab");
183                 if (!fp) {
184                         g_warning("GritsHttp: fetch - error opening %s", path);
185                         return NULL;
186                 }
187                 fseek(fp, 0, SEEK_END); // "a" is broken on Windows, twice
188
189                 /* Make temp data */
190                 struct _CacheInfo info = {
191                         .fp        = fp,
192                         .path      = path,
193                         .callback  = callback,
194                         .user_data = user_data,
195                 };
196
197                 /* Download the file */
198                 SoupMessage *message = soup_message_new("GET", uri);
199                 if (message == NULL)
200                         g_error("message is null, cannot parse uri");
201                 g_signal_connect(message, "got-chunk", G_CALLBACK(_chunk_cb), &info);
202                 //if (ftell(fp) > 0)
203                         soup_message_headers_set_range(message->request_headers, ftell(fp), -1);
204                 if (mode == GRITS_REFRESH)
205                         soup_message_headers_replace(message->request_headers,
206                                         "Cache-Control", "max-age=0");
207                 soup_session_send_message(http->soup, message);
208
209                 /* Close file */
210                 fclose(fp);
211                 if (path != part) {
212                         if (SOUP_STATUS_IS_SUCCESSFUL(message->status_code))
213                                 g_rename(part, path);
214                         g_free(part);
215                 }
216
217                 /* Finished */
218                 guint status = message->status_code;
219                 g_object_unref(message);
220                 if (status == SOUP_STATUS_CANCELLED) {
221                         return NULL;
222                 } else if (status == SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE) {
223                         /* Range unsatisfiable, file already complete */
224                 } else if (!SOUP_STATUS_IS_SUCCESSFUL(status)) {
225                         g_warning("GritsHttp: done_cb - error copying file, status=%d\n"
226                                         "\tsrc=%s\n"
227                                         "\tdst=%s",
228                                         status, uri, path);
229                         return NULL;
230                 }
231         }
232
233         /* TODO: free everything.. */
234         return path;
235 }
236
237 /**
238  * grits_http_available:
239  * @http:    the #GritsHttp connection to use
240  * @filter:  filter used to extract files from the index, or NULL
241  *           For example: "href=\"([^"]*)\""
242  * @cache:   path to the local cache, or NULL to not search the cache
243  * @extract: regex used to extract filenames from the page, should match the
244  *           filename as $1, or NULL to use /http="([^"])"/
245  * @index:   path to the index page, or NULL to not search online
246  *
247  * Look through the cache and an HTTP index page for a list of available files.
248  * The name of each file that matches the filter is added to the returned list.
249  *
250  * The list as well as the strings contained in it should be freed afterwards.
251  *
252  * Returns the list of matching filenames
253  */
254 GList *grits_http_available(GritsHttp *http,
255                 gchar *filter, gchar *cache,
256                 gchar *extract, gchar *index)
257 {
258         g_debug("GritsHttp: available - %s~=%s %s~=%s",
259                         filter, cache, extract, index);
260         GRegex *filter_re = g_regex_new(filter, 0, 0, NULL);
261         GList  *files = NULL;
262
263         /* Add cached files */
264         if (cache) {
265                 const gchar *file;
266                 gchar *path = _get_cache_path(http, cache);
267                 GDir  *dir  = g_dir_open(path, 0, NULL);
268                 while (dir && (file = g_dir_read_name(dir)))
269                         if (g_regex_match(filter_re, file, 0, NULL))
270                                 files = g_list_prepend(files, g_strdup(file));
271                 g_free(path);
272                 if (dir)
273                         g_dir_close(dir);
274         }
275
276         /* Add online files if online */
277         if (index) {
278                 gchar tmp[32];
279                 g_snprintf(tmp, sizeof(tmp), ".index.%x", g_random_int());
280                 gchar *path = grits_http_fetch(http, index, tmp,
281                                 GRITS_REFRESH, NULL, NULL);
282                 if (!path)
283                         return files;
284                 gchar *html;
285                 g_file_get_contents(path, &html, NULL, NULL);
286                 if (!html)
287                         return files;
288
289                 /* Match hrefs by default, this regex is not very accurate */
290                 GRegex *extract_re = g_regex_new(
291                                 extract ?: "href=\"([^\"]*)\"", 0, 0, NULL);
292                 GMatchInfo *info;
293                 g_regex_match(extract_re, html, 0, &info);
294                 while (g_match_info_matches(info)) {
295                         gchar *file = g_match_info_fetch(info, 1);
296                         if (file) {
297                                 if (g_regex_match(filter_re, file, 0, NULL))
298                                         files = g_list_prepend(files, file);
299                                 else
300                                         g_free(file);
301                         }
302                         g_match_info_next(info, NULL);
303                 }
304
305                 g_regex_unref(extract_re);
306                 g_match_info_free(info);
307                 g_unlink(path);
308                 g_free(path);
309                 g_free(html);
310         }
311
312         g_regex_unref(filter_re);
313
314         return files;
315 }