/*
* 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;
/**
* A singleton persistent cache object for avatars used across backends in
* folks. Avatars may be added to the cache, and referred to by a persistent
* URI from that point onwards.
*
* @since 0.6.0
*/
public class Folks.AvatarCache : Object
{
private static weak AvatarCache? _instance = null; /* needs to be locked */
private File _cache_directory;
/**
* Private constructor for an instance of the avatar cache. The singleton
* instance should be retrieved by calling {@link AvatarCache.dup()} instead.
*
* @since 0.6.0
*/
private AvatarCache ()
{
Object ();
}
construct
{
this._cache_directory =
File.new_for_path (Environment.get_user_cache_dir ())
.get_child ("folks")
.get_child ("avatars");
}
/**
* Create or return the singleton {@link AvatarCache} class instance.
* If the instance doesn't exist already, it will be created.
*
* This function is thread-safe.
*
* @return Singleton {@link AvatarCache} instance
* @since 0.6.0
*/
public static AvatarCache dup ()
{
lock (AvatarCache._instance)
{
var _retval = AvatarCache._instance;
AvatarCache retval;
if (_retval == null)
{
/* use an intermediate variable to force a strong reference */
retval = new AvatarCache ();
AvatarCache._instance = retval;
}
else
{
retval = (!) _retval;
}
return retval;
}
}
~AvatarCache ()
{
/* Manually clear the singleton _instance */
lock (AvatarCache._instance)
{
AvatarCache._instance = null;
}
}
/**
* Fetch an avatar from the cache by its globally unique ID.
*
* @param id the globally unique ID for the avatar
* @return Avatar from the cache, or `null` if it doesn't exist in the cache
* @throws GLib.Error if checking for existence of the cache file failed
* @since 0.6.0
*/
public async LoadableIcon? load_avatar (string id) throws GLib.Error
{
var avatar_file = this._get_avatar_file (id);
debug ("Loading avatar '%s' from file '%s'.", id, avatar_file.get_uri ());
// Return null if the avatar doesn't exist
if (avatar_file.query_exists () == false)
{
return null;
}
return new FileIcon (avatar_file);
}
/**
* Store an avatar in the cache, assigning the given globally unique ID to it,
* which can later be used to load and remove the avatar from the cache. For
* example, this ID could be the UID of a persona. The URI of the cached
* avatar file will be returned.
*
* @param id the globally unique ID for the avatar
* @param avatar the avatar data to cache
* @return a URI for the file storing the cached avatar
* @throws GLib.Error if the avatar data couldn't be loaded, or if creating
* the avatar directory or cache file failed
* @since 0.6.0
*/
public async string store_avatar (string id, LoadableIcon avatar)
throws GLib.Error
{
var dest_avatar_file = this._get_avatar_file (id);
debug ("Storing avatar '%s' in file '%s'.", id,
dest_avatar_file.get_uri ());
InputStream src_avatar_stream =
yield avatar.load_async (-1, null, null);
// Copy the icon data into a file
while (true)
{
OutputStream? dest_avatar_stream = null;
try
{
dest_avatar_stream =
yield dest_avatar_file.replace_async (null, false,
FileCreateFlags.PRIVATE);
yield ((!) dest_avatar_stream).splice_async (src_avatar_stream,
OutputStreamSpliceFlags.NONE);
yield ((!) dest_avatar_stream).close_async ();
break;
}
catch (GLib.Error e)
{
/* If the parent directory wasn't found, create it and loop
* round to try again. */
if (e is IOError.NOT_FOUND)
{
this._create_cache_directory ();
continue;
}
if (dest_avatar_stream != null)
{
yield ((!) dest_avatar_stream).close_async ();
}
throw e;
}
}
yield src_avatar_stream.close_async ();
return this.build_uri_for_avatar (id);
}
/**
* Remove an avatar from the cache, if it exists in the cache. If the avatar
* exists in the cache but there is a problem in removing it, a
* {@link GLib.Error} will be thrown.
*
* @param id the globally unique ID for the avatar
* @throws GLib.Error if deleting the cache file failed
* @since 0.6.0
*/
public async void remove_avatar (string id) throws GLib.Error
{
var avatar_file = this._get_avatar_file (id);
debug ("Removing avatar '%s' in file '%s'.", id, avatar_file.get_uri ());
try
{
avatar_file.delete (null);
}
catch (GLib.Error e)
{
// Ignore file not found errors
if (!(e is IOError.NOT_FOUND))
{
throw e;
}
}
}
/**
* Build the URI of an avatar file in the cache from a globally unique ID.
* This will always succeed, even if the avatar doesn't exist in the cache.
*
* @param id the globally unique ID for the avatar
* @return URI of the avatar file with the given globally unique ID
* @since 0.6.0
*/
public string build_uri_for_avatar (string id)
{
return this._get_avatar_file (id).get_uri ();
}
private File _get_avatar_file (string id)
{
var escaped_uri = Uri.escape_string (id, "", false);
var file = this._cache_directory.get_child (escaped_uri);
assert (file.has_parent (this._cache_directory) == true);
return file;
}
private void _create_cache_directory () throws GLib.Error
{
try
{
this._cache_directory.make_directory_with_parents ();
}
catch (GLib.Error e)
{
// Ignore errors caused by the directory existing already
if (!(e is IOError.EXISTS))
{
throw e;
}
}
}
}