Post-release version bump
[platform/upstream/folks.git] / folks / avatar-cache.vala
1 /*
2  * Copyright (C) 2011 Collabora Ltd.
3  *
4  * This library is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 2.1 of the License, or
7  * (at your option) any later version.
8  *
9  * This library 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 Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public License
15  * along with this library.  If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Authors:
18  *       Philip Withnall <philip.withnall@collabora.co.uk>
19  */
20
21 using GLib;
22
23 /**
24  * A singleton persistent cache object for avatars used across backends in
25  * folks. Avatars may be added to the cache, and referred to by a persistent
26  * URI from that point onwards.
27  *
28  * @since 0.6.0
29  */
30 public class Folks.AvatarCache : Object
31 {
32   private static weak AvatarCache? _instance = null; /* needs to be locked */
33   private File _cache_directory;
34
35   /**
36    * Private constructor for an instance of the avatar cache. The singleton
37    * instance should be retrieved by calling {@link AvatarCache.dup()} instead.
38    *
39    * @since 0.6.0
40    */
41   private AvatarCache ()
42     {
43       Object ();
44     }
45
46   construct
47     {
48       this._cache_directory =
49           File.new_for_path (Environment.get_user_cache_dir ())
50               .get_child ("folks")
51               .get_child ("avatars");
52     }
53
54   /**
55    * Create or return the singleton {@link AvatarCache} class instance.
56    * If the instance doesn't exist already, it will be created.
57    *
58    * This function is thread-safe.
59    *
60    * @return Singleton {@link AvatarCache} instance
61    * @since 0.6.0
62    */
63   public static AvatarCache dup ()
64     {
65       lock (AvatarCache._instance)
66         {
67           var _retval = AvatarCache._instance;
68           AvatarCache retval;
69
70           if (_retval == null)
71             {
72               /* use an intermediate variable to force a strong reference */
73               retval = new AvatarCache ();
74               AvatarCache._instance = retval;
75             }
76           else
77             {
78               retval = (!) _retval;
79             }
80
81           return retval;
82         }
83     }
84
85   ~AvatarCache ()
86     {
87       /* Manually clear the singleton _instance */
88       lock (AvatarCache._instance)
89         {
90           AvatarCache._instance = null;
91         }
92     }
93
94   /**
95    * Fetch an avatar from the cache by its globally unique ID.
96    *
97    * @param id the globally unique ID for the avatar
98    * @return Avatar from the cache, or `null` if it doesn't exist in the cache
99    * @throws GLib.Error if checking for existence of the cache file failed
100    * @since 0.6.0
101    */
102   public async LoadableIcon? load_avatar (string id) throws GLib.Error
103     {
104       var avatar_file = this._get_avatar_file (id);
105
106       debug ("Loading avatar '%s' from file '%s'.", id, avatar_file.get_uri ());
107
108       // Return null if the avatar doesn't exist
109       if (avatar_file.query_exists () == false)
110         {
111           return null;
112         }
113
114       return new FileIcon (avatar_file);
115     }
116
117   /**
118    * Store an avatar in the cache, assigning the given globally unique ID to it,
119    * which can later be used to load and remove the avatar from the cache. For
120    * example, this ID could be the UID of a persona. The URI of the cached
121    * avatar file will be returned.
122    *
123    * @param id the globally unique ID for the avatar
124    * @param avatar the avatar data to cache
125    * @return a URI for the file storing the cached avatar
126    * @throws GLib.Error if the avatar data couldn't be loaded, or if creating
127    * the avatar directory or cache file failed
128    * @since 0.6.0
129    */
130   public async string store_avatar (string id, LoadableIcon avatar)
131       throws GLib.Error
132     {
133       var dest_avatar_file = this._get_avatar_file (id);
134
135       debug ("Storing avatar '%s' in file '%s'.", id,
136           dest_avatar_file.get_uri ());
137
138       InputStream src_avatar_stream =
139           yield avatar.load_async (-1, null, null);
140
141       // Copy the icon data into a file
142       while (true)
143         {
144           OutputStream? dest_avatar_stream = null;
145
146           try
147             {
148               dest_avatar_stream =
149                   yield dest_avatar_file.replace_async (null, false,
150                       FileCreateFlags.PRIVATE);
151               yield ((!) dest_avatar_stream).splice_async (src_avatar_stream,
152                   OutputStreamSpliceFlags.NONE);
153               yield ((!) dest_avatar_stream).close_async ();
154
155               break;
156             }
157           catch (GLib.Error e)
158             {
159               /* If the parent directory wasn't found, create it and loop
160                * round to try again. */
161               if (e is IOError.NOT_FOUND)
162                 {
163                   this._create_cache_directory ();
164                   continue;
165                 }
166
167               if (dest_avatar_stream != null)
168                 {
169                   yield ((!) dest_avatar_stream).close_async ();
170                 }
171
172               throw e;
173             }
174         }
175
176       yield src_avatar_stream.close_async ();
177
178       return this.build_uri_for_avatar (id);
179     }
180
181   /**
182    * Remove an avatar from the cache, if it exists in the cache. If the avatar
183    * exists in the cache but there is a problem in removing it, a
184    * {@link GLib.Error} will be thrown.
185    *
186    * @param id the globally unique ID for the avatar
187    * @throws GLib.Error if deleting the cache file failed
188    * @since 0.6.0
189    */
190   public async void remove_avatar (string id) throws GLib.Error
191     {
192       var avatar_file = this._get_avatar_file (id);
193
194       debug ("Removing avatar '%s' in file '%s'.", id, avatar_file.get_uri ());
195
196       try
197         {
198           avatar_file.delete (null);
199         }
200       catch (GLib.Error e)
201         {
202           // Ignore file not found errors
203           if (!(e is IOError.NOT_FOUND))
204             {
205               throw e;
206             }
207         }
208     }
209
210   /**
211    * Build the URI of an avatar file in the cache from a globally unique ID.
212    * This will always succeed, even if the avatar doesn't exist in the cache.
213    *
214    * @param id the globally unique ID for the avatar
215    * @return URI of the avatar file with the given globally unique ID
216    * @since 0.6.0
217    */
218   public string build_uri_for_avatar (string id)
219     {
220       return this._get_avatar_file (id).get_uri ();
221     }
222
223   private File _get_avatar_file (string id)
224     {
225       var escaped_uri = Uri.escape_string (id, "", false);
226       var file = this._cache_directory.get_child (escaped_uri);
227
228       assert (file.has_parent (this._cache_directory) == true);
229
230       return file;
231     }
232
233   private void _create_cache_directory () throws GLib.Error
234     {
235       try
236         {
237           this._cache_directory.make_directory_with_parents ();
238         }
239       catch (GLib.Error e)
240         {
241           // Ignore errors caused by the directory existing already
242           if (!(e is IOError.EXISTS))
243             {
244               throw e;
245             }
246         }
247     }
248 }