[folks] Add a key-file backend



commit 39cddd06a71c50d42518fbbf7fbc4dc5247e4833
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Fri Jul 16 13:41:34 2010 +0100

    Add a key-file backend
    
    This backend stores Personas in a key file which is completely under the
    user's control, making it the only fully trusted Backend. Currently, it only
    stores IM addresses, which can then be used as linkable properties by the
    IndividualAggregator to link together Personas from multiple backends to
    automagically produce Individuals.

 backends/Makefile.am                      |    1 +
 backends/key-file/Makefile.am             |   84 +++++++++++
 backends/key-file/folks-key-file.deps     |    2 +
 backends/key-file/kf-backend-factory.vala |   67 +++++++++
 backends/key-file/kf-backend.vala         |   73 ++++++++++
 backends/key-file/kf-persona-store.vala   |  222 +++++++++++++++++++++++++++++
 backends/key-file/kf-persona.vala         |  124 ++++++++++++++++
 backends/telepathy/tp-backend.vala        |    3 -
 configure.ac                              |    1 +
 folks/backend.vala                        |    6 +
 folks/debug.vala                          |    9 +-
 11 files changed, 586 insertions(+), 6 deletions(-)
---
diff --git a/backends/Makefile.am b/backends/Makefile.am
index ba7cb9a..cd023e8 100644
--- a/backends/Makefile.am
+++ b/backends/Makefile.am
@@ -1,5 +1,6 @@
 SUBDIRS = \
 	telepathy \
+	key-file \
 	$(NULL)
 
 -include $(top_srcdir)/git.mk
diff --git a/backends/key-file/Makefile.am b/backends/key-file/Makefile.am
new file mode 100644
index 0000000..7021c38
--- /dev/null
+++ b/backends/key-file/Makefile.am
@@ -0,0 +1,84 @@
+AM_CPPFLAGS = \
+	-I$(top_srcdir)/folks \
+	-include $(CONFIG_HEADER) \
+	-DPACKAGE_DATADIR=\"$(pkgdatadir)\" \
+	-DG_LOG_DOMAIN=\"KeyFileBackend\" \
+	$(NULL)
+
+VALAFLAGS += --vapidir=. --vapidir=$(top_srcdir)/folks
+
+backenddir = $(BACKEND_DIR)/key-file
+backend_LTLIBRARIES = libfolks-backend-key-file.la
+
+folks_backend_key_file_valasources = \
+	kf-backend.vala \
+	kf-backend-factory.vala \
+	kf-persona.vala \
+	kf-persona-store.vala \
+	$(NULL)
+
+folks_backend_key_file_deps = \
+	folks \
+	gee-1.0 \
+	gio-2.0 \
+	gobject-2.0 \
+	$(NULL)
+
+libfolks_backend_key_file_la_SOURCES = \
+	$(folks_backend_key_file_valasources:.vala=.c)
+
+libfolks_backend_key_file_la_CPPFLAGS = \
+	$(GIO_CFLAGS) \
+	$(GLIB_CFLAGS) \
+	$(GEE_CFLAGS) \
+	$(AM_CPPFLAGS) \
+	$(NULL)
+
+libfolks_backend_key_file_la_LIBADD = \
+	$(GIO_LIBS) \
+	$(GLIB_LIBS) \
+	$(GEE_LIBS) \
+	$(NULL)
+
+libfolks_backend_key_file_la_LDFLAGS = -shared -fPIC -module -avoid-version
+
+folks-backend-key-file.h $(libfolks_backend_key_file_la_SOURCES): \
+		folks-backend-key-file.vala.stamp
+
+folks-backend-key-file.vapi folks-backend-key-file.vala.stamp: $(folks_backend_key_file_valasources)
+	$(VALA_V)$(VALAC) $(VALACFLAGS) $(VALAFLAGS) \
+		-H folks-backend-key-file.h -C \
+		--library folks-backend-key-file \
+		$(addprefix --pkg ,$(folks_backend_key_file_deps)) \
+		$(addprefix $(srcdir)/,$(folks_backend_key_file_valasources))
+	touch $@
+
+BUILT_SOURCES = \
+	folks-backend-key-file.h \
+	folks-backend-key-file.vala.stamp \
+	folks-backend-key-file.vapi \
+	$(libfolks_backend_key_file_la_SOURCES) \
+	$(NULL)
+
+EXTRA_DIST = \
+	$(folks_backend_key_file_valasources) \
+	folks-backend-key-file.vala.stamp \
+	folks-backend-key-file.vapi \
+	$(NULL)
+
+CLEANFILES = \
+	$(BUILT_SOURCES) \
+	$(NULL)
+
+MAINTAINERCLEANFILES = \
+	$(libfolks_backend_key_file_la_SOURCES) \
+	$(NULL)
+
+# set up the verbosity rules to avoid some build noise
+# XXX: once automake >= 1.11 is common, remove these, push valasources files
+# into SOURCES and add AM_PROG_VALAC to configure.ac
+VALA_V = $(VALA_V_$(V))
+VALA_V_ = $(VALA_V_$(AM_DEFAULT_VERBOSITY))
+VALA_V_0 = @echo "  VALAC " $^;
+
+-include $(top_srcdir)/git.mk
diff --git a/backends/key-file/folks-key-file.deps b/backends/key-file/folks-key-file.deps
new file mode 100644
index 0000000..430a2e8
--- /dev/null
+++ b/backends/key-file/folks-key-file.deps
@@ -0,0 +1,2 @@
+folks
+gobject-2.0
diff --git a/backends/key-file/kf-backend-factory.vala b/backends/key-file/kf-backend-factory.vala
new file mode 100644
index 0000000..8343c28
--- /dev/null
+++ b/backends/key-file/kf-backend-factory.vala
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2009 Zeeshan Ali (Khattak) <zeeshanak gnome org>.
+ * Copyright (C) 2009 Nokia Corporation.
+ * Copyright (C) 2010 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Zeeshan Ali (Khattak) <zeeshanak gnome org>
+ *          Travis Reitter <travis reitter collabora co uk>
+ *          Philip Withnall <philip withnall collabora co uk>
+ *
+ * This file was originally part of Rygel.
+ */
+
+using Folks;
+using Folks.Backends.Kf;
+
+private BackendFactory backend_factory = null;
+
+/**
+ * The backend module entry point.
+ */
+public void module_init (BackendStore backend_store)
+{
+  backend_factory = new BackendFactory (backend_store);
+}
+
+/**
+ * The backend module exit point.
+ */
+public void module_finalize (BackendStore backend_store)
+{
+  backend_factory = null;
+}
+
+/**
+ * A backend factory to create a single { link Backend}.
+ */
+public class Folks.Backends.Kf.BackendFactory : Object
+{
+  /**
+   * { inheritDoc}
+   */
+  public BackendFactory (BackendStore backend_store)
+    {
+      try
+        {
+          backend_store.add_backend (new Backend ());
+        }
+      catch (GLib.Error e)
+        {
+          warning ("Failed to add key file backend to libfolks: %s",
+              e.message);
+        }
+    }
+}
diff --git a/backends/key-file/kf-backend.vala b/backends/key-file/kf-backend.vala
new file mode 100644
index 0000000..b5bf475
--- /dev/null
+++ b/backends/key-file/kf-backend.vala
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using GLib;
+using Folks;
+using Folks.Backends.Kf;
+
+/**
+ * A backend which loads { link Persona}s from a simple key file in
+ * (XDG_DATA_HOME/folks/) and presents them through a single
+ * { link PersonaStore}.
+ */
+public class Folks.Backends.Kf.Backend : Folks.Backend
+{
+  /**
+   * { inheritDoc}
+   */
+  public override string name { get; private set; }
+
+  /**
+   * { inheritDoc}
+   */
+  public override HashTable<string, PersonaStore> persona_stores
+    {
+      get; private set;
+    }
+
+  /**
+   * { inheritDoc}
+   */
+  public Backend () throws GLib.Error
+    {
+      Object (name: "key-file");
+    }
+
+  public override async void prepare ()
+    {
+      File file = File.new_for_path (Environment.get_user_data_dir ());
+      file = file.get_child ("folks");
+      file = file.get_child ("relationships.ini");
+
+      /* Create the PersonaStore for the key file */
+      PersonaStore store = new Kf.PersonaStore (file);
+
+      this.persona_stores.insert (store.id, store);
+      store.removed.connect (this.store_removed_cb);
+
+      this.persona_store_added (store);
+    }
+
+  private void store_removed_cb (Folks.PersonaStore store)
+    {
+      this.persona_store_removed (store);
+      this.persona_stores.remove (store.id);
+    }
+}
diff --git a/backends/key-file/kf-persona-store.vala b/backends/key-file/kf-persona-store.vala
new file mode 100644
index 0000000..d57b592
--- /dev/null
+++ b/backends/key-file/kf-persona-store.vala
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2010 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Travis Reitter <travis reitter collabora co uk>
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using GLib;
+using Folks;
+using Folks.Backends.Kf;
+
+/**
+ * A persona store which is associated with a single simple key file. It will
+ * create a { link Persona} for each of the groups in the key file.
+ */
+public class Folks.Backends.Kf.PersonaStore : Folks.PersonaStore
+{
+  private HashTable<string, Persona> _personas;
+  private File file;
+  private GLib.KeyFile key_file;
+
+  /**
+   * { inheritDoc}
+   */
+  public override string type_id { get; private set; }
+
+  /**
+   * { inheritDoc}
+   */
+  public override string id { get; private set; }
+
+  /**
+   * { inheritDoc}
+   */
+  public override HashTable<string, Persona> personas
+    {
+      get { return this._personas; }
+    }
+
+  /**
+   * Create a new PersonaStore.
+   *
+   * Create a new persona store to expose the { link Persona}s provided by the
+   * different groups in the key file given by `key_file`.
+   */
+  public PersonaStore (File key_file)
+    {
+      this.type_id = "key-file";
+      this.id = key_file.get_basename ();
+      this.trust_level = PersonaStoreTrust.FULL;
+      this.file = key_file;
+      this._personas = new HashTable<string, Persona> (str_hash, str_equal);
+    }
+
+  public override async void prepare ()
+    {
+      string filename = this.file.get_path ();
+      this.key_file = new GLib.KeyFile ();
+
+      /* Load or create the file */
+      while (true)
+        {
+          /* Load the file; if this fails due to the file not existing or having
+           * been deleted in the meantime, we can continue below and try to
+           * create it instead. */
+          try
+            {
+              string contents = null;
+              size_t length = 0;
+
+              yield this.file.load_contents_async (null, out contents,
+                  out length);
+              if (length > 0)
+                {
+                  this.key_file.load_from_data (contents, length,
+                      KeyFileFlags.KEEP_COMMENTS);
+                }
+              break;
+            }
+          catch (Error e1)
+            {
+              if (!(e1 is IOError.NOT_FOUND))
+                {
+                  warning ("The relationship key file '%s' could not be " +
+                      "loaded: %s", filename, e1.message);
+                  this.removed ();
+                  return;
+                }
+            }
+
+          /* Create a new file; if this fails due to the file having been
+           * created in the meantime, we can loop back round and try and load
+           * it. */
+          try
+            {
+              /* Recursively create the directory */
+              File parent_dir = this.file.get_parent ();
+              parent_dir.make_directory_with_parents ();
+
+              /* Create the file */
+              FileOutputStream stream = yield this.file.create_async (
+                  FileCreateFlags.PRIVATE, Priority.DEFAULT);
+              yield stream.close_async (Priority.DEFAULT);
+            }
+          catch (Error e2)
+            {
+              if (!(e2 is IOError.EXISTS))
+                {
+                  warning ("The relationship key file '%s' could not be " +
+                      "created: %s", filename, e2.message);
+                  this.removed ();
+                  return;
+                }
+            }
+        }
+
+      /* We've loaded or created a key file by now, so cycle through the groups:
+       * each group is a persona which we have to create and emit */
+      string[] groups = this.key_file.get_groups ();
+      foreach (string persona_uid in groups)
+        {
+          Persona persona = new Kf.Persona (this.key_file, persona_uid, this);
+          this._personas.insert (persona.iid, persona);
+        }
+
+      if (this._personas.size () > 0)
+        {
+          /* FIXME: Groups.ChangeReason is not the right enum to use here */
+          this.personas_changed (this._personas.get_values (), null, null, null,
+              Groups.ChangeReason.NONE);
+        }
+    }
+
+  /**
+   * { inheritDoc}
+   */
+  public override async void remove_persona (Folks.Persona persona)
+    {
+      try
+        {
+          this.key_file.remove_group (persona.uid);
+          yield this.save_key_file ();
+        }
+      catch (KeyFileError e)
+        {
+          /* Ignore the error, since it's only about a missing group */
+        }
+    }
+
+  /**
+   * { inheritDoc}
+   */
+  public override async Folks.Persona? add_persona_from_details (
+      HashTable<string, Value?> details) throws Folks.PersonaStoreError
+    {
+      Value val = details.lookup ("im-address");
+      unowned string im_address = val.get_string ();
+      val = details.lookup ("protocol");
+      unowned string protocol = val.get_string ();
+
+      if (im_address == null || protocol == null)
+        {
+          throw new PersonaStoreError.INVALID_ARGUMENT (
+              "persona store (%s, %s) requires the following details:\n" +
+              "    im-address (provided: '%s')\n",
+              "    protocol (provided: '%s')\n",
+              this.type_id, this.id, im_address, protocol);
+        }
+
+      /* Take the persona ID as the concatenation of the protocol and IM
+       * address, fairly arbitrarily */
+      string persona_id = protocol + ":" + im_address;
+
+      /* Insert the new IM details into the key file and create a Persona from
+       * them */
+      string[] im_addresses = new string[1];
+      im_addresses[0] = im_address;
+      this.key_file.set_string_list (persona_id, protocol, im_addresses);
+      yield this.save_key_file ();
+
+      Persona persona = new Kf.Persona (this.key_file, persona_id, this);
+      this._personas.insert (persona.iid, persona);
+
+      /* FIXME: Groups.ChangeReason is not the right enum to use here */
+      GLib.List<Persona> personas = new GLib.List<Persona> ();
+      personas.prepend (persona);
+      this.personas_changed (personas, null, null, null,
+          Groups.ChangeReason.NONE);
+
+      return persona;
+    }
+
+  internal async void save_key_file ()
+    {
+      string key_file_data = this.key_file.to_data ();
+
+      try
+        {
+          yield this.file.replace_contents_async (key_file_data,
+              key_file_data.length, null, false, FileCreateFlags.PRIVATE);
+        }
+      catch (Error e)
+        {
+          warning ("Could not write updated key file '%s': %s",
+              this.file.get_path (), e.message);
+        }
+    }
+}
diff --git a/backends/key-file/kf-persona.vala b/backends/key-file/kf-persona.vala
new file mode 100644
index 0000000..54b0a7e
--- /dev/null
+++ b/backends/key-file/kf-persona.vala
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using GLib;
+using Folks;
+using Folks.Backends.Kf;
+
+/**
+ * A persona subclass which represents a single persona from a simple key file.
+ */
+public class Folks.Backends.Kf.Persona : Folks.Persona,
+    IMable
+{
+  private unowned GLib.KeyFile key_file;
+  /* FIXME: As described in the IMable interface, we have to use
+   * GenericArray<string> here rather than just string[], as null-terminated
+   * arrays aren't supported as generic types. */
+  private HashTable<string, GenericArray<string>> _im_addresses;
+
+  /**
+   * { inheritDoc}
+   */
+  public HashTable<string, GenericArray<string>> im_addresses
+    {
+      get
+        { return this._im_addresses; }
+
+      set
+        {
+          /* Remove the current IM addresses from the key file */
+          this._im_addresses.foreach ((k, v) =>
+            {
+              try
+                {
+                  unowned string protocol = (string) k;
+                  this.key_file.remove_key (this.uid, protocol);
+                }
+              catch (KeyFileError e)
+                {
+                  /* Ignore the error, since it's just a group or key not found
+                   * error. */
+                }
+            });
+
+          this._im_addresses = value;
+
+          /* Add the new IM addresses to the key file */
+          this._im_addresses.foreach ((k, v) =>
+            {
+              unowned string protocol = (string) k;
+              unowned string[] addresses = (string[]) v;
+              this.key_file.set_string_list (this.uid, protocol, addresses);
+            });
+
+          /* Get the PersonaStore to save the key file */
+          ((Kf.PersonaStore) this.store).save_key_file.begin ();
+        }
+    }
+
+  /**
+   * Create a new persona.
+   *
+   * Create a new persona for the { link PersonaStore} `store`, representing
+   * the Persona given by the group `uid` in the key file `key_file`.
+   */
+  public Persona (KeyFile key_file, string uid, Folks.PersonaStore store)
+    {
+      string iid = "key-file:" + uid;
+      string[] linkable_properties = { "im-addresses" };
+
+      Object (iid: iid,
+              uid: uid,
+              store: store,
+              linkable_properties: linkable_properties);
+
+      this.key_file = key_file;
+      this._im_addresses = new HashTable<string, GenericArray<string>> (
+          str_hash, str_equal);
+
+      /* Load the IM addresses from the key file */
+      try
+        {
+          string[] keys = this.key_file.get_keys (uid);
+          foreach (string protocol in keys)
+            {
+              string[] im_addresses = this.key_file.get_string_list (uid,
+                  protocol);
+
+              /* FIXME: We have to convert our nice efficient string[] to a
+               * GenericArray<string> because Vala doesn't like null-terminated
+               * arrays as generic types. */
+              GenericArray<string> im_address_array =
+                  new GenericArray<string> ();
+              foreach (string address in im_addresses)
+                im_address_array.add (address);
+
+              this._im_addresses.insert (protocol, im_address_array);
+            }
+        }
+      catch (KeyFileError e)
+        {
+          /* This should never be reached, as we're listing the keys then
+           * iterating through the list. */
+          GLib.assert_not_reached ();
+        }
+    }
+}
diff --git a/backends/telepathy/tp-backend.vala b/backends/telepathy/tp-backend.vala
index 016ec2b..c5dc516 100644
--- a/backends/telepathy/tp-backend.vala
+++ b/backends/telepathy/tp-backend.vala
@@ -50,9 +50,6 @@ public class Folks.Backends.Tp.Backend : Folks.Backend
   public Backend ()
     {
       Object (name: "telepathy");
-
-      this.persona_stores = new HashTable<string, PersonaStore> (str_hash,
-          str_equal);
     }
 
   public override async void prepare () throws GLib.Error
diff --git a/configure.ac b/configure.ac
index a6f4f7e..92cadf0 100644
--- a/configure.ac
+++ b/configure.ac
@@ -202,6 +202,7 @@ AC_CONFIG_FILES([
 	folks/folks-uninstalled.pc
 	Makefile
 	backends/Makefile
+	backends/key-file/Makefile
 	backends/telepathy/Makefile
 	folks/Makefile
 	docs/Makefile
diff --git a/folks/backend.vala b/folks/backend.vala
index 933c773..0bdb419 100644
--- a/folks/backend.vala
+++ b/folks/backend.vala
@@ -93,4 +93,10 @@ public abstract class Folks.Backend : Object
    * If this function throws an error, the Backend will not be functional.
    */
   public abstract async void prepare () throws GLib.Error;
+
+  construct
+    {
+      this.persona_stores = new HashTable<string, PersonaStore> (str_hash,
+          str_equal);
+    }
 }
diff --git a/folks/debug.vala b/folks/debug.vala
index f56de98..27ced0c 100644
--- a/folks/debug.vala
+++ b/folks/debug.vala
@@ -25,16 +25,19 @@ namespace Folks.Debug
   private enum Domains {
     /* Zero is used for "no debug spew" */
     CORE = 1 << 0,
-    TELEPATHY_BACKEND = 1 << 1
+    TELEPATHY_BACKEND = 1 << 1,
+    KEY_FILE_BACKEND = 1 << 2
   }
 
   internal static void set_flags (string? debug_flags)
     {
-      GLib.DebugKey keys[2] =
+      GLib.DebugKey keys[3] =
         {
           DebugKey () { key = "Core", value = Domains.CORE },
           DebugKey () { key = "TelepathyBackend",
-              value = Domains.TELEPATHY_BACKEND }
+              value = Domains.TELEPATHY_BACKEND },
+          DebugKey () { key = "KeyFileBackend",
+              value = Domains.KEY_FILE_BACKEND }
         };
 
       uint flags = GLib.parse_debug_string (debug_flags, keys);



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]