/* * Copyright (C) 2011 Collabora Ltd. * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * This library 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . * * Authors: * Philip Withnall */ using GLib; using Gee; /** * A generic abstract cache for sets of objects. This can be used by subclasses * to implement caching of homogeneous sets of objects. Subclasses simply have * to implement serialisation and deserialisation of the objects to and from * {@link GLib.Variant}s. * * It's intended that this class be used for providing caching layers for * {@link PersonaStore}s, for example. * * @since 0.6.0 */ public abstract class Folks.ObjectCache : Object { /* The version number of the header/wrapper for a cache file. When accompanied * by a version number for the serialised object type, this unambiguously * keys the variant type describing an entire cache file. * * The wrapper and object version numbers are stored as the first two bytes * of a cache file. They can't be stored as part of the Variant which forms * the rest of the file, as to interpret the Variant its entire type has to * be known — which depends on the version numbers. */ private static const uint8 _FILE_FORMAT_VERSION = 1; /* The length of the version header at the beginning of the file. This has * to be a multiple of 8 to keep Variant's alignment code happy. * As documented above, currently only the first two bytes of this header * are used (for version numbers). */ private static const size_t _HEADER_WIDTH = 8; /* bytes */ private File _cache_directory; private File _cache_file; private string _cache_file_path; /* save calls to _cache_file.get_path() */ /** * Get the {@link GLib.VariantType} of the serialised form of an object stored * in this cache. * * If a smooth upgrade path is needed in future due to cache file format * changes, this may be modified to take a version parameter. * * @param object_version the version of the object format to use, or * `uint8.MAX` for the latest version * @return variant type for that object version, or `null` if the version is * unsupported * @since 0.6.0 */ protected abstract VariantType? get_serialised_object_type ( uint8 object_version); /** * Get the version of the variant type returned by * {@link ObjectCache.get_serialised_object_type}. This must be incremented * every time the variant type changes so that old cache files aren't * misinterpreted. * * @since 0.6.0 */ protected abstract uint8 get_serialised_object_version (); /** * Serialise the given `object` to a {@link GLib.Variant} and return the * variant. The variant must be of the type returned by * {@link ObjectCache.get_serialised_object_type}. * * @param object the object to serialise * @return serialised form of `object` * * @since 0.6.0 */ protected abstract Variant serialise_object (T object); /** * Deserialise the given `variant` to a new instance of an object. The variant * is guaranteed to have the type returned by * {@link ObjectCache.get_serialised_object_type}. * * @param variant the serialised form to deserialise * @param object_version the version of the object format to deserialise from * @return the deserialised object * * @since 0.6.0 */ protected abstract T deserialise_object (Variant variant, uint8 object_version); /** * A string identifying the type of object being cached. * * This has to be suitable for use as a directory name; i.e. lower case, * hyphen-separated tokens. * * @since 0.6.6 */ public string type_id { get; construct; } /** * A string identifying the particular cache instance. * * This will form the file name of the cache file, but will be escaped * beforehand, so can be an arbitrary non-empty string. * * @since 0.6.6 */ public string id { get { return this._id; } construct { assert (value != ""); this._id = value; } } private string _id; /** * Create a new cache instance using the given type ID and ID. This is * protected as the `type_id` will typically be set statically by subclasses. * * @param type_id A string identifying the type of object being cached. This * has to be suitable for use as a directory name; i.e. lower case, * hyphen-separated. * @param id A string identifying the particular cache instance. This will * form the file name of the cache file, but will be escaped beforehand, so * can be an arbitrary non-empty string. * @return A new cache instance * * @since 0.6.0 */ protected ObjectCache (string type_id, string id) { Object (type_id: type_id, id: id); } construct { debug ("Creating object cache for type ID '%s' with ID '%s'.", this.type_id, this.id); this._cache_directory = File.new_for_path (Environment.get_user_cache_dir ()) .get_child ("folks") .get_child (this.type_id); this._cache_file = this._cache_directory.get_child (Uri.escape_string (this.id, "", false)); var path = this._cache_file.get_path (); this._cache_file_path = (path != null) ? (!) path : "(null)"; } /** * Load a set of objects from the cache and return them as a new set. If the * cache file doesn't exist, `null` will be returned. An empty set will be * returned if the cache file existed but was empty (i.e. was stored with * an empty set originally). * * Loading the objects can be cancelled using `cancellable`. If it is, `null` * will be returned. * * If any errors are encountered while loading the objects, warnings will be * logged as appropriate and `null` will be returned. * * @param cancellable A {@link GLib.Cancellable} for the operation, or `null`. * @return A set of objects from the cache, or `null`. * * @since 0.6.0 */ public async Set? load_objects (Cancellable? cancellable = null) { debug ("Loading cache (type ID '%s', ID '%s') from file '%s'.", this.type_id, this._id, this._cache_file_path); // Read in the file uint8[] data; try { yield this._cache_file.load_contents_async (cancellable, out data, null); } catch (Error e) { if (e is IOError.CANCELLED) { /* not a true error */ } else if (e is IOError.NOT_FOUND) { debug ("Couldn't load cache file '%s': %s", this._cache_file_path, e.message); } else { warning ("Couldn't load cache file '%s': %s", this._cache_file_path, e.message); } return null; } // Check the length if (data.length < this._HEADER_WIDTH) { warning ("Cache file '%s' was too small. The file was deleted.", this._cache_file_path); yield this.clear_cache (); return null; } // Check the version var wrapper_version = data[0]; var object_version = data[1]; if (wrapper_version != this._FILE_FORMAT_VERSION) { warning ("Cache file '%s' was version %u of the file format, " + "but only version %u is supported. The file was deleted.", this._cache_file_path, wrapper_version, this._FILE_FORMAT_VERSION); yield this.clear_cache (); return null; } unowned uint8[] variant_data = data[this._HEADER_WIDTH:data.length]; // Deserialise the variant according to the given version numbers var _variant_type = this._get_cache_file_variant_type (wrapper_version, object_version); if (_variant_type == null) { warning ("Cache file '%s' was version %u of the object file " + "format, which is not supported. The file was deleted.", this._cache_file_path, object_version, this._FILE_FORMAT_VERSION); yield this.clear_cache (); return null; } var variant_type = (!) _variant_type; var variant = Variant.new_from_data (variant_type, variant_data, false, data); // Check the variant was deserialised correctly if (variant.is_normal_form () == false) { warning ("Cache file '%s' was corrupt and was deleted.", this._cache_file_path); yield this.clear_cache (); return null; } // Unpack the stored data var type_id = variant.get_child_value (0).get_string (); if (type_id != this.type_id) { warning ("Cache file '%s' had type ID '%s', but '%s' was expected." + "The file was deleted.", this._cache_file_path, type_id, this.type_id); yield this.clear_cache (); return null; } var id = variant.get_child_value (1).get_string (); if (id != this._id) { warning ("Cache file '%s' had ID '%s', but '%s' was expected." + "The file was deleted.", this._cache_file_path, id, this._id); yield this.clear_cache (); return null; } var objects_variant = variant.get_child_value (2); var objects = new HashSet (); for (uint i = 0; i < objects_variant.n_children (); i++) { var object_variant = objects_variant.get_child_value (i); var object = this.deserialise_object (object_variant, object_version); objects.add (object); } return objects; } /** * Store a set of objects to the cache file, overwriting any existing set of * objects in the cache, or creating the cache file from scratch if it didn't * previously exist. * * Storing the objects can be cancelled using `cancellable`. If it is, the * cache will be left in a consistent state, but may be storing the old set * of objects or the new set. * * @param objects A set of objects to store. This may be empty, but may not * be `null`. * @param cancellable A {@link GLib.Cancellable} for the operation, or `null`. * * @since 0.6.0 */ public async void store_objects (Set objects, Cancellable? cancellable = null) { debug ("Storing cache (type ID '%s', ID '%s') to file '%s'.", this.type_id, this._id, this._cache_file_path); var child_type = this.get_serialised_object_type (uint8.MAX); assert (child_type != null); // uint8.MAX should always be supported Variant[] children = new Variant[objects.size]; // Serialise all the objects in the set uint i = 0; foreach (var object in objects) { children[i++] = this.serialise_object (object); } // File format var wrapper_version = this._FILE_FORMAT_VERSION; var object_version = this.get_serialised_object_version (); var variant = new Variant.tuple ({ new Variant.string (this.type_id), // Type ID new Variant.string (this._id), // ID new Variant.array (child_type, children) // Array of objects }); var desired_variant_type = this._get_cache_file_variant_type (wrapper_version, object_version); assert (desired_variant_type != null && variant.get_type ().equal ((!) desired_variant_type)); // Prepend the version numbers to the data uint8[] data = new uint8[this._HEADER_WIDTH + variant.get_size ()]; data[0] = wrapper_version; data[1] = object_version; variant.store (data[this._HEADER_WIDTH:data.length]); // Write the data out to the file while (true) { try { yield this._cache_file.replace_contents_async ( data, null, false, FileCreateFlags.PRIVATE, cancellable, null); break; } catch (Error e) { if (e is IOError.NOT_FOUND) { try { yield this._create_cache_directory (); continue; } catch (Error e2) { warning ("Couldn't create cache directory '%s': %s", this._cache_directory.get_path (), e.message); return; } } else if (e is IOError.CANCELLED) { /* We assume the replace_contents_async() call is atomic, * so cancelling it is atomic as well. */ return; } /* Print a warning and delete the cache file so we don't leave * stale cached objects lying around. */ warning ("Couldn't write to cache file '%s', so deleting it: %s", this._cache_file_path, e.message); yield this.clear_cache (); return; } } } /** * Clear this cache object, deleting its backing file. * * @since 0.6.0 */ public async void clear_cache () { debug ("Clearing cache (type ID '%s', ID '%s'); deleting file '%s'.", this.type_id, this._id, this._cache_file_path); try { this._cache_file.delete (); } catch (Error e) { // Ignore errors } } private VariantType? _get_cache_file_variant_type (uint8 wrapper_version, uint8 object_version) { var _object_type = this.get_serialised_object_type (object_version); if (_object_type == null) { // Unsupported version return null; } var object_type = (!) _object_type; return new VariantType.tuple ({ VariantType.STRING, // Type ID VariantType.STRING, // ID new VariantType.array (object_type) // Objects }); } private async void _create_cache_directory () throws Error { try { this._cache_directory.make_directory_with_parents (); } catch (Error e) { // Ignore errors caused by the directory existing already if (!(e is IOError.EXISTS)) { throw e; } } } } /* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */