/* * Copyright (C) 2012 - Juan Ferrer Toribio * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "db-file-loader.h" #include "gio/gio.h" #include #include #include #define CACHE_CLEAN_PERIOD 600 #define CACHE_DEFAULT_SIZE 100000000 #define CACHE_CLEAN_MARGIN (CACHE_DEFAULT_SIZE*20)/100 /** * SECTION: db-file-loader * @Short_description: utility to download/upload resources from an URL * @Title: DbFileLoader * * #DbFileLoader can download and upload files from a remote or local location by * its URL. Both operations will allways be made asynchronously, to retrieve the * results you must pass a #DbFileLoaderCallbackFunc. * * By default, #DbFileLoader uses a local cache on disk to store the downloaded * files. To avoid the use of this feature, create the #DbFileLoader using * db_file_loader_new_simple(). The default cache directory is 'hedera', under * g_get_user_cache_dir(). To use another directory as cache or set its size, * use g_object_set(). */ struct _DbFileLoaderPrivate { gchar * dir; gchar * cache; goffset size; gchar * host; gchar * path; GInetSocketAddress * addr; GThreadPool * pool; GMutex * mutex; GMutex * cache_mutex; GHashTable * downloading; guint src; }; G_DEFINE_TYPE (DbFileLoader, db_file_loader, G_TYPE_OBJECT) typedef struct { DbFileLoader * obj; gchar * name; GBytes * data; GError * error; DbFileLoaderCallbackFunc func; gpointer user_data; GCancellable * cancel; } File; typedef struct { guint64 atime; goffset size; gchar * path; } CacheFile; static const gchar * WDAY[] = {"Err", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; static const gchar * MONTH[] = {"Err", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; static const gchar * format_request = { "GET /%s/%s HTTP/1.1\r\n" // Path to the file "Host: %s\r\n" // Hostname "%s" // If-modified-since: date\r\n "\r\n" }; /** * db_file_loader_new: * @host: the host where the files are stored * @path: the base directory to find the loaded resources * * Creates a new #DbFileLoader pointing to @path in @host. The cache is set * to default. * * Return value: a #DbFileLoader **/ DbFileLoader * db_file_loader_new (const gchar * host, const gchar * path) { g_return_val_if_fail (host, NULL); g_return_val_if_fail (path, NULL); return g_object_new (DB_TYPE_FILE_LOADER, "host", host, "path", path, "cache", NULL, NULL); } /** * db_file_loader_new_simple: * @host: the host where the files are stored * @path: the base directory to find the loaded resources * * Creates a new #DbFileLoader pointing to @path in @host. To use a cache, * call db_file_loader_new(). * * Return value: a #DbFileLoader **/ DbFileLoader * db_file_loader_new_simple (const gchar * host, const gchar * path) { DbFileLoader * fl; g_return_val_if_fail (host, NULL); g_return_val_if_fail (path, NULL); fl = g_object_new (DB_TYPE_FILE_LOADER, "host", host, "path", path, NULL); if (fl->priv->cache) { g_free (fl->priv->cache); fl->priv->cache = NULL; } return fl; } /* * db_file_loader_new_full: * @host: the host where the files are stored * @path: the base directory to find the loaded resources * @cache: (allow-none): the base directory for the local cache or %NULL * @size: maximal size for the cache * * Creates a new #DbFileLoader pointing to @path in @host. Note that the user * needs permission to read and write in @cache. If @cache is %NULL, it will be * set to default, if @max_cache is 0, the cache has no maximal size. * * Return value: a #DbFileLoader or %NULL if @cache is an invalid directory **/ DbFileLoader * db_file_loader_new_full (const gchar * host, const gchar * path, const gchar * cache, goffset size) { g_return_val_if_fail (host, NULL); g_return_val_if_fail (path, NULL); // Comprobar que cache existe y se puede escribir, comprobar tamaƱo disponible return g_object_new (DB_TYPE_FILE_LOADER, "host", host, "path", path, "cache", cache, "size", size, NULL); } //+++++++++++++++++++++++++++++++++++++++++++++++++++ Methods static File * file_new (DbFileLoader * obj, const gchar * path, const GBytes * data, DbFileLoaderCallbackFunc func, gpointer user_data, gboolean async) { File * file = g_new (File, 1); file->obj = g_object_ref (obj); file->name = g_strdup (path); file->data = data ? g_boxed_copy (G_TYPE_BYTES, data) : NULL; file->error = NULL; file->func = func; file-> user_data = user_data; file->cancel = async ? g_cancellable_new () : NULL; return file; } static void file_free (File * file) { if (file) { g_free (file->name); if (file->error) g_error_free (file->error); if (file->data) g_bytes_unref (file->data); g_object_unref (file->obj); g_free (file); } } /* * Connection management and usage */ static GFile * db_file_loader_get_cache_file (DbFileLoader * obj, const gchar * subpath) { gchar * path = g_strconcat (obj->priv->cache, "/", subpath, NULL); GFile * file = g_file_new_for_path (path); g_free (path); return file; } static gboolean db_file_loader_callback (File * file) { if (file->cancel && !g_cancellable_is_cancelled (file->cancel)) file->func (file->obj, file->data, file->error, file->user_data); g_hash_table_remove (file->obj->priv->downloading, file); return FALSE; } static gboolean db_file_loader_resolve_host (DbFileLoader * obj, GCancellable * cancel, GError ** error) { gboolean ret = FALSE; GResolver * r = g_resolver_get_default (); GList * ads = g_resolver_lookup_by_name (r, obj->priv->host, cancel, error); if (ads) { GInetAddress * a = g_list_nth_data (ads, 0);// TODO? Use the entire list // TODO Add the option to set the connection port!! obj->priv->addr = G_INET_SOCKET_ADDRESS (g_inet_socket_address_new (a, 80)); g_resolver_free_addresses (ads); ret = TRUE; } g_object_unref (r); return ret; } static GIOStream * db_file_loader_connect (DbFileLoader * obj, GCancellable * cancel, GError ** error) { GSocketClient * client = g_socket_client_new (); GIOStream * connection = G_IO_STREAM (g_socket_client_connect (client, G_SOCKET_CONNECTABLE (obj->priv->addr), cancel, error)); g_object_unref (client); return connection; } static gchar * db_file_loader_create_request (DbFileLoader * obj, const gchar * subpath) { gchar * ifmod; gchar * request; gchar * date = NULL; if (obj->priv->cache) { GFileInfo * info; GFile * file = db_file_loader_get_cache_file (obj, subpath); info = g_file_query_info (file ,G_FILE_ATTRIBUTE_TIME_CHANGED ,G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS ,NULL, NULL); g_object_unref (file); if (info) { guint64 date_attr = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_CHANGED); GDateTime * dt = g_date_time_new_from_unix_utc (date_attr); gchar * f_year_time = g_date_time_format (dt, "%Y %T GMT"); gchar * f_month_day = g_date_time_format (dt, "%d"); date = g_strdup_printf ("%s, %s %s %s" ,WDAY[g_date_time_get_day_of_week (dt)] ,f_month_day ,MONTH[g_date_time_get_month (dt)] ,f_year_time ); g_free (f_month_day); g_free (f_year_time); g_date_time_unref (dt); g_object_unref (info); } } ifmod = date ? g_strconcat ("If-modified-since: ", date, "\r\n", NULL) : ""; request = g_strdup_printf (format_request, obj->priv->path, subpath, obj->priv->host, ifmod); if (date) { g_free (date); g_free (ifmod); } return request; } static gboolean db_file_loader_store (DbFileLoader * obj, const gchar * subpath, const gchar * data, gsize len, GError ** error) { gsize dir_len; gboolean ret = FALSE; gchar * path = g_strconcat (obj->priv->cache, "/",subpath, NULL); GFile * file = g_file_new_for_path (path); gchar * name = g_file_get_basename (file); dir_len = strlen (path) - strlen (name); gchar dir[dir_len]; g_strlcpy (dir, path, dir_len); g_free (name); g_mutex_lock (obj->priv->cache_mutex); if (g_mkdir_with_parents (dir, 00700) >= 0) { if (!g_file_query_exists (file, NULL) && g_file_set_contents (path, data, len, error)) ret = TRUE; } else g_set_error (error, DB_FILE_LOADER_LOG_DOMAIN, g_file_error_from_errno (errno), _("%s not cached"), subpath); g_mutex_unlock (obj->priv->cache_mutex); g_free (path); g_object_unref (file); return ret; } static gchar * db_file_loader_load_from_cache (DbFileLoader * obj, const gchar * subpath, gsize * len, GError ** error) { gchar * data; GFile * file = db_file_loader_get_cache_file (obj, subpath); g_file_load_contents (file, NULL, &data, len, NULL, error); g_object_unref (file); return data; } static void db_file_loader_thread_download (File * file, DbFileLoader * obj) { gsize len; gchar ** split; gchar * status_line = NULL; gchar * request = NULL; gchar * data = NULL; GError * error = NULL; GIOStream * connection = NULL; GDataInputStream * receive_stream = NULL; GOutputStream * send_stream; GCancellable * cancel = file->cancel; g_mutex_lock (obj->priv->mutex); if (!obj->priv->addr && !db_file_loader_resolve_host (obj, cancel, &error)) { g_mutex_unlock (obj->priv->mutex); goto final; } g_mutex_unlock (obj->priv->mutex); if (!(connection = db_file_loader_connect (obj, cancel, &error))) goto final; request = db_file_loader_create_request (obj, file->name); send_stream = g_io_stream_get_output_stream (connection); if (0 > g_output_stream_write (send_stream, request, strlen (request), cancel, &error) || !g_output_stream_close (send_stream, cancel, &error)) goto final; receive_stream = g_data_input_stream_new (g_io_stream_get_input_stream (connection)); g_data_input_stream_set_newline_type (receive_stream, G_DATA_STREAM_NEWLINE_TYPE_CR_LF); status_line = g_data_input_stream_read_line (receive_stream, &len, cancel, &error); if (!status_line) goto final; split = g_strsplit (status_line, " ", -1); guint status = g_strv_length (split) >= 2 ? atoi (split[1]) : 0; g_strfreev (split); switch (status) { gchar * line; case 200: { gboolean content_known; guint i, nbytes = 0; line = NULL; do {// Read header fields g_free (line); line = g_data_input_stream_read_line (receive_stream, &len, cancel, &error); if (g_str_has_prefix (line, "Content-Length: ")) { nbytes = atoi (line + 16); content_known = TRUE; } } while (line && len && !error); g_free (line); if (!error) { if (!content_known) g_set_error (&error, DB_FILE_LOADER_LOG_DOMAIN, status, _("Unknown content length of file %s"), file->name); else { len = nbytes; data = g_new (gchar, len); for (i = 0; !error && i < len; i++) data[i] = g_data_input_stream_read_byte (receive_stream, cancel, &error); if (!error && data) db_file_loader_store (obj, file->name, data, len, &error); } } break; } case 304: { data = db_file_loader_load_from_cache (obj, file->name, &len, &error); break; } default: if (status_line && !error) g_set_error (&error, DB_FILE_LOADER_LOG_DOMAIN, status, "%s: '%s'", status_line, file->name); } file->data = g_bytes_new_take (data, len); final: g_free (request); g_free (status_line); if (receive_stream) { g_input_stream_close (G_INPUT_STREAM (receive_stream), NULL, error ? NULL : &error); g_object_unref (receive_stream); } if (connection) { g_io_stream_close (connection, NULL, error ? NULL : &error); g_object_unref (connection); } file->error = error; g_idle_add_full (G_PRIORITY_DEFAULT_IDLE ,(GSourceFunc) db_file_loader_callback, file ,(GDestroyNotify) file_free ); } /** * db_file_loader_download: * @obj: a #DbFileLoader * @path: the path to the file from @obj's path * @func: (scope async): the #DbFileLoaderCallbackFunc to call after downloading * or in case of error * @user_data: (closure): data to pass to @func * * Downloads a file from @file, which is a relative path to the file from the * base URL of the #DbFileLoader. The result will be availble in @func. **/ void db_file_loader_download (DbFileLoader * obj, const gchar * path, DbFileLoaderCallbackFunc func, gpointer user_data) { File * file; g_return_if_fail (DB_IS_FILE_LOADER (obj)); g_return_if_fail (path); if (!obj->priv->pool) obj->priv->pool = g_thread_pool_new ((GFunc) db_file_loader_thread_download, obj, 2, FALSE, NULL); file = file_new (obj, path, NULL, func, user_data, TRUE); g_hash_table_add (obj->priv->downloading, file); g_thread_pool_push (obj->priv->pool, file, NULL); } static void db_file_loader_thread_upload (DbFileLoader * obj, File * file) { g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, (GSourceFunc) db_file_loader_callback, file, (GDestroyNotify) file_free); } /** * db_file_loader_upload: * @obj: a #DbFileLoader * @data: a #GBytes with the data to upload * @path: a string with the path to the file * @func: (scope async): the #DbFileLoaderCallbackFunc to call after uploading * or in case of error * @user_data: (closure): data to pass to @func * * Uploads @data to @file, which is a relative path to the file from the base * URL of the #DbFileLoader. To retrieve the result, connect to the * "downloaded" signal and to the "error" signal to manage errors. **/ void db_file_loader_upload (DbFileLoader * obj, GBytes * data, const gchar * path, DbFileLoaderCallbackFunc func, gpointer user_data) { File * file; g_return_if_fail (DB_IS_FILE_LOADER (obj)); g_return_if_fail (data); g_return_if_fail (path); //TODO? create a pool only for uploads if (!obj->priv->pool) obj->priv->pool = g_thread_pool_new ((GFunc) db_file_loader_thread_upload, obj, 2, TRUE, NULL); file = file_new (obj, path, data, func, user_data, TRUE); g_thread_pool_push (obj->priv->pool, file, NULL); } static void db_file_loader_cancel (DbFileLoader * obj, const gchar * name) { GHashTableIter iter; File * file; g_return_if_fail (DB_IS_FILE_LOADER (obj)); if (!obj->priv->downloading) return; g_hash_table_iter_init (&iter, obj->priv->downloading); while (g_hash_table_iter_next (&iter, (gpointer *) &file, NULL)) if (!name || !g_strcmp0 (file->name, name)) { g_cancellable_cancel (file->cancel); g_hash_table_iter_steal (&iter); } } /** * db_file_loader_cancel_all: * @obj: a #DbFileLoader * * Cancels all downloads pending or in progress. Note that after cancelling a * task it may delay until it returns to the main loop. For the cause of this, * see g_cancellable_cancel(). **/ void db_file_loader_cancel_all (DbFileLoader * obj) { db_file_loader_cancel (obj, NULL); } /** * db_file_loader_cancel_by_name: * @obj: a #DbFileLoader * @name: an string with a filename * * Cancels the download of the file with name @name, that may be pending or in * progress. See db_file_loader_cancel_all() for more information. **/ void db_file_loader_cancel_by_name (DbFileLoader * obj, const gchar * name) { if (name) db_file_loader_cancel (obj, name); } //+++++++++++++++++++++++++++++++++++++++++++++++++++ Properties enum { HOST_PROP = 1 ,PATH_PROP ,CACHE_PROP ,SIZE_PROP }; static void db_file_loader_set_property (DbFileLoader * obj, guint id, const GValue * value, GParamSpec * pspec) { switch (id) { case HOST_PROP: { g_free (obj->priv->host); obj->priv->host = g_value_dup_string (value); break; } case PATH_PROP: { g_free (obj->priv->path); obj->priv->path = g_value_dup_string (value); break; } case CACHE_PROP: { const gchar * cache = g_value_get_string (value); g_free (obj->priv->cache); obj->priv->cache = g_build_filename ( (cache ? cache : g_get_user_cache_dir ()) ,"hedera" ,obj->priv->host ,obj->priv->path ,NULL); break; } case SIZE_PROP: { obj->priv->size = g_value_get_int64 (value); obj->priv->size = obj->priv->size <= 0 ? 0 : obj->priv->size; break; } default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, id, pspec); } } static void db_file_loader_get_property (DbFileLoader * obj, guint id, GValue * value, GParamSpec * pspec) { switch (id) { case HOST_PROP: g_value_set_string (value, obj->priv->host); break; case PATH_PROP: g_value_set_string (value, obj->priv->path); break; case CACHE_PROP: g_value_set_string (value, obj->priv->cache); case SIZE_PROP: g_value_set_int64 (value, obj->priv->size); default: G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, id, pspec); } } //+++++++++++++++++++++++++++++++++++++++++++++++++++ Class static void db_file_loader_init (DbFileLoader * obj) { obj->priv = G_TYPE_INSTANCE_GET_PRIVATE (obj, DB_TYPE_FILE_LOADER, DbFileLoaderPrivate); obj->priv->host = NULL; obj->priv->path = NULL; obj->priv->cache = NULL; obj->priv->size = CACHE_DEFAULT_SIZE; obj->priv->addr = NULL; obj->priv->pool = NULL; obj->priv->mutex = g_new (GMutex, 1); g_mutex_init (obj->priv->mutex); obj->priv->cache_mutex = g_new (GMutex, 1); g_mutex_init (obj->priv->cache_mutex); obj->priv->downloading = g_hash_table_new_full ((GHashFunc) g_direct_hash, (GEqualFunc) g_direct_equal, NULL, NULL); } static void db_file_loader_finalize (DbFileLoader * obj) { G_OBJECT_CLASS (db_file_loader_parent_class)->finalize (G_OBJECT (obj)); g_free (obj->priv->host); g_free (obj->priv->path); g_free (obj->priv->cache); g_clear_object (&obj->priv->addr); if (obj->priv->pool) g_thread_pool_free (obj->priv->pool, TRUE, TRUE); g_mutex_clear (obj->priv->cache_mutex); g_free (obj->priv->cache_mutex); g_mutex_clear (obj->priv->mutex); g_free (obj->priv->mutex); g_hash_table_destroy (obj->priv->downloading); } static void db_file_loader_class_init (DbFileLoaderClass * klass) { GObjectClass * k = G_OBJECT_CLASS (klass); k->finalize = (GObjectFinalizeFunc) db_file_loader_finalize; k->set_property = (GObjectSetPropertyFunc) db_file_loader_set_property; k->get_property = (GObjectGetPropertyFunc) db_file_loader_get_property; g_type_class_add_private (klass, sizeof (DbFileLoaderPrivate)); g_object_class_install_property (k, HOST_PROP, g_param_spec_string ("host" ,_("Host") ,_("The host web server name to get the images") ,NULL ,G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY )); g_object_class_install_property (k, PATH_PROP, g_param_spec_string ("path" ,_("Path") ,_("The path of the directory to interact with") ,NULL ,G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY )); g_object_class_install_property (k, CACHE_PROP, g_param_spec_string ("cache" ,_("Cache directory") ,_("The local directory where the downloaded files will be stored. " "The default cache directory is 'hedera', under g_get_user_cache_dir().") ,NULL ,G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY )); g_object_class_install_property (k, SIZE_PROP, g_param_spec_int64 ("size" ,_("Maximal cache size") ,_("The maximal size for the contents of the cache directory") ,0 ,G_MAXOFFSET ,CACHE_DEFAULT_SIZE ,G_PARAM_READWRITE )); }