613dd439fa7ce38f7c55486415cf5a1a8cc8d15a
[grits] / src / data / gis-http.c
1 /*
2  * Copyright (C) 2009-2010 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:gis-http
20  * @short_description: Hyper Text Transfer Protocol
21  *
22  * #GisHttp is a small wrapper around libsoup to provide data access using the
23  * Hyper Text Transfer Protocol. Each #GisHttp should be associated with a
24  * particular server or dataset, all the files downloaded for this dataset will
25  * be cached together in $HOME.cache/libgis/
26  */
27
28 #include <config.h>
29 #include <glib.h>
30 #include <glib/gstdio.h>
31 #include <libsoup/soup.h>
32
33 #include "gis-http.h"
34
35 gchar *_get_cache_path(GisHttp *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  * gis_http_new:
43  * @prefix: The prefix in the cache to store the downloaded files.
44  *          For example: * "/nexrad/level2/".
45  *
46  * Create a new #GisHttp for the given prefix
47  *
48  * Returns: the new #GisHttp
49  */
50 GisHttp *gis_http_new(const gchar *prefix)
51 {
52         g_debug("GisHttp: new - %s", prefix);
53         GisHttp *http = g_new0(GisHttp, 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         return http;
58 }
59
60 /**
61  * gis_http_free:
62  * @http: the #GisHttp to free
63  *
64  * Frees resources used by @http and cancels any pending requests.
65  */
66 void gis_http_free(GisHttp *http)
67 {
68         g_debug("GisHttp: free - %s", http->prefix);
69         soup_session_abort(http->soup);
70         g_object_unref(http->soup);
71         g_free(http->prefix);
72         g_free(http);
73 }
74
75 /* For passing data to the chunck callback */
76 struct _CacheInfo {
77         FILE  *fp;
78         gchar *path;
79         GisChunkCallback callback;
80         gpointer user_data;
81 };
82
83 /**
84  * Append data to the file and call the users callback if they supplied one.
85  */
86 static void _chunk_cb(SoupMessage *message, SoupBuffer *chunk, gpointer _info)
87 {
88         struct _CacheInfo *info = _info;
89
90         if (!SOUP_STATUS_IS_SUCCESSFUL(message->status_code)) {
91                 g_warning("GisHttp: _chunk_cb - soup failed with %d",
92                                 message->status_code);
93                 return;
94         }
95
96         if (!fwrite(chunk->data, chunk->length, 1, info->fp))
97                 g_error("GisHttp: _chunk_cb - Unable to write data");
98
99         if (info->callback) {
100                 goffset cur = ftell(info->fp);
101                 goffset st=0, end=0, total=0;
102                 soup_message_headers_get_content_range(message->response_headers,
103                                 &st, &end, &total);
104                 info->callback(info->path, cur, total, info->user_data);
105         }
106 }
107
108 /**
109  * gis_http_fetch:
110  * @http:      the #GisHttp connection to use
111  * @uri:       the URI to fetch
112  * @local:     the local name to give to the file
113  * @mode:      the update type to use when fetching data
114  * @callback:  callback to call when a chunk of data is received
115  * @user_data: user data to pass to the callback
116  *
117  * Fetch a file from the cache. Whether the file is actually loaded from the
118  * remote server depends on the value of @mode.
119  *
120  * Returns: The local path to the complete file
121  */
122 /* TODO: use .part extentions and continue even when using GIS_ONCE */
123 gchar *gis_http_fetch(GisHttp *http, const gchar *uri, const char *local,
124                 GisCacheType mode, GisChunkCallback callback, gpointer user_data)
125 {
126         g_debug("GisHttp: fetch - %s... >> %s/%s  mode=%d",
127                         uri, http->prefix, local, mode);
128
129         gchar *path = _get_cache_path(http, local);
130
131         /* Unlink the file if we're refreshing it */
132         if (mode == GIS_REFRESH)
133                 g_remove(path);
134
135         /* Do the cache if necessasairy */
136         if (!(mode == GIS_ONCE && g_file_test(path, G_FILE_TEST_EXISTS)) &&
137                         mode != GIS_LOCAL) {
138                 g_debug("GisHttp: fetch - Caching file %s", local);
139
140                 /* Open the file for writting */
141                 gchar *part = path;
142                 if (!g_file_test(path, G_FILE_TEST_EXISTS))
143                         part = g_strdup_printf("%s.part", path);
144                 FILE *fp = fopen_p(part, "a");
145
146                 /* Make temp data */
147                 struct _CacheInfo info = {
148                         .fp        = fp,
149                         .path      = path,
150                         .callback  = callback,
151                         .user_data = user_data,
152                 };
153
154                 /* Download the file */
155                 SoupMessage *message = soup_message_new("GET", uri);
156                 if (message == NULL)
157                         g_error("message is null, cannot parse uri");
158                 g_signal_connect(message, "got-chunk", G_CALLBACK(_chunk_cb), &info);
159                 soup_message_headers_set_range(message->request_headers, ftell(fp), -1);
160                 soup_session_send_message(http->soup, message);
161
162                 /* Close file */
163                 fclose(fp);
164                 if (path != part && SOUP_STATUS_IS_SUCCESSFUL(message->status_code)) {
165                         g_rename(part, path);
166                         g_free(part);
167                 }
168
169                 /* Finished */
170                 if (message->status_code == 416) {
171                         /* Range unsatisfiable, file already complete */
172                 } else if (!SOUP_STATUS_IS_SUCCESSFUL(message->status_code)) {
173                         g_warning("GisHttp: done_cb - error copying file, status=%d\n"
174                                         "\tsrc=%s\n"
175                                         "\tdst=%s",
176                                         message->status_code, uri, path);
177                         return NULL;
178                 }
179         }
180
181         /* TODO: free everything.. */
182         return path;
183 }
184
185 /**
186  * gis_http_available:
187  * @http:    the #GisHttp connection to use
188  * @filter:  filter used to extract files from the index, or NULL
189  *           For example: "href=\"([^"]*)\""
190  * @cache:   path to the local cache, or NULL to not search the cache
191  * @extract: regex used to extract filenames from the page, should match the
192  *           filename as $1, or NULL to use /http="([^"])"/
193  * @index:   path to the index page, or NULL to not search online
194  *
195  * Look through the cache and an HTTP index page for a list of available files.
196  * The name of each file that matches the filter is added to the returned list.
197  *
198  * The list as well as the strings contained in it should be freed afterwards.
199  *
200  * Returns the list of matching filenames
201  */
202 GList *gis_http_available(GisHttp *http,
203                 gchar *filter, gchar *cache,
204                 gchar *extract, gchar *index)
205 {
206         g_debug("GisHttp: available - %s~=%s %s~=%s",
207                         filter, cache, extract, index);
208         GRegex *filter_re = g_regex_new(filter, 0, 0, NULL);
209         GList  *files = NULL;
210
211         /* Add cached files */
212         if (cache) {
213                 const gchar *file;
214                 gchar *path = _get_cache_path(http, cache);
215                 GDir  *dir  = g_dir_open(path, 0, NULL);
216                 while ((file = g_dir_read_name(dir)))
217                         if (g_regex_match(filter_re, file, 0, NULL))
218                                 files = g_list_prepend(files, g_strdup(file));
219                 g_free(path);
220         }
221
222
223         /* Add online files if online */
224         if (index) {
225                 gchar tmp[16];
226                 g_snprintf(tmp, sizeof(tmp), ".index.%x", g_random_int());
227                 gchar *path = gis_http_fetch(http, index, tmp,
228                                 GIS_REFRESH, NULL, NULL);
229                 gchar *html;
230                 g_file_get_contents(path, &html, NULL, NULL);
231
232                 /* Match hrefs by default, this regex is not very accurate */
233                 GRegex *extract_re = g_regex_new(
234                                 extract ?: "href=\"([^\"]*)\"", 0, 0, NULL);
235                 GMatchInfo *info;
236                 g_regex_match(extract_re, html, 0, &info);
237                 while (g_match_info_matches(info)) {
238                         gchar *file = g_match_info_fetch(info, 1);
239                         if (g_regex_match(filter_re, file, 0, NULL))
240                                 files = g_list_prepend(files, file);
241                         else
242                                 g_free(file);
243                         g_match_info_next(info, NULL);
244                 }
245
246                 g_match_info_free(info);
247                 g_unlink(path);
248                 g_free(path);
249                 g_free(html);
250         }
251
252         return files;
253 }