diff options
author | Cosimo Cecchi <cosimoc@gnome.org> | 2013-09-26 14:34:08 -0700 |
---|---|---|
committer | Cosimo Cecchi <cosimoc@gnome.org> | 2013-09-26 14:34:08 -0700 |
commit | 81ee21249d9a0ee48442cd161fc04e0b32fdd2a6 (patch) | |
tree | aa817b9b4fd182fe00d5f00415199c04064e5e71 | |
parent | 8ceb8ac14c85c812308b9d93e29f76e985e7b688 (diff) | |
parent | a1129813f76b08515522af2be3e93a9994d47dfc (diff) |
Merge pull request #305 from endlessm/issues/303
API for per-user application config directory
-rw-r--r-- | docs/reference/endless/endless-sections.txt | 1 | ||||
-rw-r--r-- | endless/eosapplication.c | 148 | ||||
-rw-r--r-- | endless/eosapplication.h | 9 | ||||
-rw-r--r-- | test/run-tests.c | 16 | ||||
-rw-r--r-- | test/run-tests.h | 3 | ||||
-rw-r--r-- | test/test-application.c | 129 |
6 files changed, 301 insertions, 5 deletions
diff --git a/docs/reference/endless/endless-sections.txt b/docs/reference/endless/endless-sections.txt index de69f31..3857e8f 100644 --- a/docs/reference/endless/endless-sections.txt +++ b/docs/reference/endless/endless-sections.txt @@ -11,6 +11,7 @@ EOS_ENUM_VALUE <FILE>application</FILE> EosApplication eos_application_new +eos_application_get_config_dir <SUBSECTION Standard> EosApplicationClass EOS_APPLICATION diff --git a/endless/eosapplication.c b/endless/eosapplication.c index b7a9163..5429669 100644 --- a/endless/eosapplication.c +++ b/endless/eosapplication.c @@ -55,9 +55,49 @@ G_DEFINE_TYPE (EosApplication, eos_application, GTK_TYPE_APPLICATION) struct _EosApplicationPrivate { + GOnce init_config_dir_once; + GFile *config_dir; + EosWindow *main_application_window; }; +enum +{ + PROP_0, + PROP_CONFIG_DIR, + NPROPS +}; + +static GParamSpec *eos_application_props[NPROPS] = { NULL, }; + +static void +eos_application_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + EosApplication *self = EOS_APPLICATION (object); + + switch (property_id) + { + case PROP_CONFIG_DIR: + g_value_set_object (value, eos_application_get_config_dir (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +eos_application_finalize (GObject *object) +{ + EosApplication *self = EOS_APPLICATION (object); + g_clear_object (&self->priv->config_dir); + + G_OBJECT_CLASS (eos_application_parent_class)->finalize (object); +} + static void eos_application_activate (GApplication *application) { @@ -76,6 +116,58 @@ eos_application_activate (GApplication *application) /* TODO: Should it be required to override activate() as in GApplication? */ } +static gpointer +ensure_config_dir_exists_and_is_writable (EosApplication *self) +{ + const gchar *xdg_path = g_get_user_config_dir (); + const gchar *app_id = g_application_get_application_id (G_APPLICATION (self)); + GFile *xdg_dir = g_file_new_for_path (xdg_path); + GFile *config_dir = g_file_get_child (xdg_dir, app_id); + gchar *config_path = g_file_get_path (config_dir); /* For error reporting */ + + g_object_unref (xdg_dir); + + GError *error = NULL; + if (!g_file_make_directory_with_parents (config_dir, NULL, &error)) + { + if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_EXISTS) + { + g_clear_error (&error); /* Ignore G_IO_ERROR_EXISTS */ + } + else + { + g_error ("There was an error creating the user config directory %s: " + "%s", + config_path, error->message); + } + } + + GFileInfo *info = g_file_query_info (config_dir, + G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, + G_FILE_QUERY_INFO_NONE, + NULL, + &error); + if (info == NULL) + { + g_error ("Checking the user config directory %s failed. This means " + "something strange is going on in your home directory: %s", + config_path, error->message); + } + if (!g_file_info_get_attribute_boolean(info, + G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE)) + { + g_error ("Your user config directory %s is not writable. This means " + "something strange is going on in your home directory.", + config_path); + } + + g_object_unref (info); + g_free (config_path); + + self->priv->config_dir = config_dir; + return NULL; +} + static void eos_application_startup (GApplication *application) { @@ -96,6 +188,10 @@ eos_application_startup (GApplication *application) g_debug ("Initialized theme\n"); g_object_unref (provider); + + EosApplication *self = EOS_APPLICATION (application); + g_once (&self->priv->init_config_dir_once, + (GThreadFunc)ensure_config_dir_exists_and_is_writable, self); } static void @@ -162,21 +258,43 @@ on_app_id_set (EosApplication *self) static void eos_application_class_init (EosApplicationClass *klass) { + GObjectClass *object_class = G_OBJECT_CLASS (klass); GApplicationClass *g_application_class = G_APPLICATION_CLASS (klass); GtkApplicationClass *gtk_application_class = GTK_APPLICATION_CLASS (klass); g_type_class_add_private (klass, sizeof (EosApplicationPrivate)); + object_class->get_property = eos_application_get_property; + object_class->finalize = eos_application_finalize; g_application_class->activate = eos_application_activate; g_application_class->startup = eos_application_startup; gtk_application_class->window_added = eos_application_window_added; gtk_application_class->window_removed = eos_application_window_removed; + + /** + * EosApplication:config-dir: + * + * A directory appropriate for storing per-user configuration information for + * this application. + * Accessing this property guarantees that the directory exists and is + * writable. + * See also eos_application_get_config_dir() for more information. + */ + eos_application_props[PROP_CONFIG_DIR] = + g_param_spec_object ("config-dir", "Config dir", + "User configuration directory for this application", + G_TYPE_FILE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, NPROPS, + eos_application_props); } static void eos_application_init (EosApplication *self) { self->priv = APPLICATION_PRIVATE (self); + self->priv->init_config_dir_once = (GOnce)G_ONCE_INIT; g_signal_connect (self, "notify::application-id", G_CALLBACK (on_app_id_set), self); } @@ -204,3 +322,33 @@ eos_application_new (const gchar *application_id, "flags", flags, NULL); } + +/** + * eos_application_get_config_dir: + * @self: the application + * + * Gets a #GFile pointing to the application-specific user configuration + * directory. + * This directory is located in <code>XDG_USER_CONFIG_DIR</code>, which usually + * expands to <filename class="directory">~/.config</filename>. + * The directory name is the same as the application's unique ID (see + * #GApplication:application-id.) + * + * You should use this directory to store configuration data specific to your + * application and specific to one user, such as cookies. + * + * Calling this function will also ensure that the directory exists and is + * writable. + * If it does not exist, it will be created. + * If it cannot be created, or it exists but is not writable, the program will + * abort. + * + * Returns: (transfer none): A #GFile pointing to the user config directory. + */ +GFile * +eos_application_get_config_dir (EosApplication *self) +{ + g_once (&self->priv->init_config_dir_once, + (GThreadFunc)ensure_config_dir_exists_and_is_writable, self); + return self->priv->config_dir; +} diff --git a/endless/eosapplication.h b/endless/eosapplication.h index f809522..0ce6553 100644 --- a/endless/eosapplication.h +++ b/endless/eosapplication.h @@ -59,11 +59,14 @@ struct _EosApplicationClass }; EOS_SDK_ALL_API_VERSIONS -GType eos_application_get_type (void) G_GNUC_CONST; +GType eos_application_get_type (void) G_GNUC_CONST; EOS_SDK_ALL_API_VERSIONS -EosApplication *eos_application_new (const gchar *application_id, - GApplicationFlags flags); +EosApplication *eos_application_new (const gchar *application_id, + GApplicationFlags flags); + +EOS_SDK_ALL_API_VERSIONS +GFile *eos_application_get_config_dir (EosApplication *self); G_END_DECLS diff --git a/test/run-tests.c b/test/run-tests.c index b73d520..016744d 100644 --- a/test/run-tests.c +++ b/test/run-tests.c @@ -1,5 +1,6 @@ /* Copyright 2013 Endless Mobile, Inc. */ +#include <inttypes.h> /* For PRIi64 */ #include <glib-object.h> #include <glib.h> #include <gtk/gtk.h> @@ -7,12 +8,25 @@ #include "run-tests.h" +#define APPLICATION_TEST_ID_BASE "com.endlessm.eosapplication.test" + +/* App ID based on timestamp so that test applications don't collide */ +gchar * +generate_unique_app_id (void) +{ + return g_strdup_printf ("%s%" PRIi64, + APPLICATION_TEST_ID_BASE, + g_get_real_time ()); +} + /* Test fixture for running a test from an EosApplication's "startup" handler */ void app_window_test_fixture_setup (AppWindowTestFixture *fixture, gconstpointer callback) { - fixture->app = eos_application_new (TEST_APPLICATION_ID, 0); + gchar *app_id = generate_unique_app_id (); + fixture->app = eos_application_new (app_id, 0); + g_free (app_id); g_signal_connect(fixture->app, "startup", G_CALLBACK (callback), NULL); } diff --git a/test/run-tests.h b/test/run-tests.h index 88a57d5..8947a5a 100644 --- a/test/run-tests.h +++ b/test/run-tests.h @@ -4,7 +4,6 @@ #define RUN_TESTS_H #define TEST_LOG_DOMAIN "EndlessSDK" -#define TEST_APPLICATION_ID "com.endlessm.example.test" #define ADD_APP_WINDOW_TEST(path, test_func) \ g_test_add ((path), AppWindowTestFixture, (test_func), \ @@ -17,6 +16,8 @@ typedef struct EosApplication *app; } AppWindowTestFixture; +gchar *generate_unique_app_id (void); + void app_window_test_fixture_setup (AppWindowTestFixture *fixture, gconstpointer callback); diff --git a/test/test-application.c b/test/test-application.c index 7f291b1..74b9c49 100644 --- a/test/test-application.c +++ b/test/test-application.c @@ -1,12 +1,20 @@ /* Copyright 2013 Endless Mobile, Inc. */ #include <stdlib.h> +#include <sys/stat.h> /* For file mode constants */ #include <gtk/gtk.h> #include <endless/endless.h> #include "run-tests.h" #define EXPECTED_TWO_WINDOW_ERRMSG "*You should not add more than one application window*" +#define EXPECTED_CONFIG_NOT_WRITABLE_ERRMSG "*Your user config directory*is not writable*" + +typedef struct +{ + gchar *unique_id; + EosApplication *app; +} ConfigDirFixture; static void test_two_windows (EosApplication *app) @@ -27,8 +35,129 @@ test_two_windows (EosApplication *app) gtk_widget_destroy (win1); } +static void +config_dir_setup (ConfigDirFixture *fixture, + gconstpointer unused) +{ + fixture->unique_id = generate_unique_app_id (); + fixture->app = eos_application_new (fixture->unique_id, + G_APPLICATION_FLAGS_NONE); +} + +static void +config_dir_teardown (ConfigDirFixture *fixture, + gconstpointer unused) +{ + /* Clean up the temporary config directory */ + GFile *config_dir = eos_application_get_config_dir (fixture->app); + g_assert (g_file_delete (config_dir, NULL, NULL)); + + g_free (fixture->unique_id); + g_object_unref (fixture->app); +} + +static void +test_config_dir_get (ConfigDirFixture *fixture, + gconstpointer unused) +{ + GFile *dir1 = eos_application_get_config_dir (fixture->app); + GFile *dir2; + g_object_get (fixture->app, "config-dir", &dir2, NULL); + + g_assert (dir1 != NULL); + g_assert (G_IS_FILE (dir1)); + g_assert (dir1 == dir2); + + g_object_unref (dir2); +} + +static void +test_config_dir_returns_expected_path (ConfigDirFixture *fixture, + gconstpointer unused) +{ + GFile *config_dir = eos_application_get_config_dir (fixture->app); + + char *basename = g_file_get_basename (config_dir); + g_assert_cmpstr (basename, ==, fixture->unique_id); + g_free (basename); + + GFile *parent = g_file_get_parent (config_dir); + char *dirname = g_file_get_path (parent); + g_object_unref (parent); + g_assert_cmpstr (dirname, ==, g_get_user_config_dir ()); + g_free (dirname); +} + +static void +test_config_dir_exists (ConfigDirFixture *fixture, + gconstpointer unused) +{ + GFile *config_dir = eos_application_get_config_dir (fixture->app); + g_assert (g_file_query_exists (config_dir, NULL)); +} + +/* Helper function */ +static void +set_writable (GFile *file, + gboolean writable) +{ + guint32 unwritable_mode = S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP; + guint32 writable_mode = unwritable_mode | S_IWUSR | S_IWGRP; + + g_assert (g_file_set_attribute_uint32 (file, + G_FILE_ATTRIBUTE_UNIX_MODE, + writable? writable_mode : unwritable_mode, + G_FILE_QUERY_INFO_NONE, + NULL, NULL)); +} + +static void +test_config_dir_fails_if_not_writable (ConfigDirFixture *fixture, + gconstpointer unused) +{ + /* Pre-create the config dir and make it non-writable */ + char *config_path = g_build_filename (g_get_user_config_dir (), + fixture->unique_id, + NULL); + GFile *precreated_config_dir = g_file_new_for_path (config_path); + g_free (config_path); + g_assert (g_file_make_directory (precreated_config_dir, NULL, NULL)); + + set_writable (precreated_config_dir, FALSE); + + /* Unix-only test */ + if (g_test_trap_fork(0 /* timeout */, G_TEST_TRAP_SILENCE_STDERR)) + { + GFile *config_dir = eos_application_get_config_dir (fixture->app); + } + + g_test_trap_assert_failed (); + g_test_trap_assert_stderr (EXPECTED_CONFIG_NOT_WRITABLE_ERRMSG); + + set_writable (precreated_config_dir, TRUE); + + g_object_unref (precreated_config_dir); +} + void add_application_tests (void) { ADD_APP_WINDOW_TEST ("/application/two-windows", test_two_windows); + g_test_add ("/application/config-dir-get", ConfigDirFixture, NULL, + config_dir_setup, + test_config_dir_get, + config_dir_teardown); + g_test_add ("/application/config-dir-expected-path", ConfigDirFixture, NULL, + config_dir_setup, + test_config_dir_returns_expected_path, + config_dir_teardown); + g_test_add ("/application/config-dir-exists", ConfigDirFixture, NULL, + config_dir_setup, + test_config_dir_exists, + config_dir_teardown); + g_test_add ("/application/config-dir-fails-if-not-writable", ConfigDirFixture, + NULL, + config_dir_setup, + test_config_dir_fails_if_not_writable, + config_dir_teardown); } |