1/* 2 * Copyright © 2016 Software Freedom Conservancy Inc. 3 * Copyright © 2020 Michael Gratton <mike@vee.net> 4 * 5 * This software is licensed under the GNU Lesser General Public License 6 * (version 2.1 or later). See the COPYING file in this distribution. 7n */ 8 9namespace Util.Migrate { 10 private const string GROUP = "AccountInformation"; 11 private const string PRIMARY_EMAIL_KEY = "primary_email"; 12 private const string SETTINGS_FILENAME = Accounts.Manager.SETTINGS_FILENAME; 13 private const string MIGRATED_FILENAME = ".config_migrated"; 14 15 /** 16 * Migrates geary.ini to the XDG configuration directory with the account's email address 17 * 18 * This function iterates through all the account directories in $XDG_DATA_DIR and copies over 19 * geary.ini to ~/.config/geary/<account>/geary.ini. Note that it leaves the 20 * original file untouched. 21 * It also appends a "primary_email" key to the new configuration file to reliaby keep 22 * track of the user's email address. 23 */ 24 public static void xdg_config_dir(GLib.File user_config_dir, 25 GLib.File user_data_dir) 26 throws GLib.Error { 27 File new_config_dir; 28 File old_data_dir; 29 File new_config_file; 30 File old_config_file; 31 32 // Return if Geary has never been run (~/.local/share/geary does not exist). 33 if (!user_data_dir.query_exists()) 34 return; 35 36 FileEnumerator enumerator; 37 enumerator = user_data_dir.enumerate_children ("standard::*", 38 FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null); 39 40 FileInfo? info; 41 string email; 42 File is_migrated; 43 while ((info = enumerator.next_file(null)) != null) { 44 if (info.get_file_type() != FileType.DIRECTORY) 45 continue; 46 47 email = info.get_name(); 48 49 // Skip the directory if its name is not a valid email address. 50 if (!Geary.RFC822.MailboxAddress.is_valid_address(email)) 51 continue; 52 53 old_data_dir = user_data_dir.get_child(email); 54 new_config_dir = user_config_dir.get_child(email); 55 56 // Skip the directory if ~/.local/share/geary/<account>/geary.ini does not exist. 57 old_config_file = old_data_dir.get_child(SETTINGS_FILENAME); 58 if (!old_config_file.query_exists()) 59 continue; 60 61 // Skip the directory if ~/.local/share/geary/<account>/.config_migrated exists. 62 is_migrated = old_data_dir.get_child(MIGRATED_FILENAME); 63 if (is_migrated.query_exists()) 64 continue; 65 66 if (!new_config_dir.query_exists()) { 67 try { 68 new_config_dir.make_directory_with_parents(); 69 } catch (Error e) { 70 debug("Cannot make directory, %s", e.message); 71 continue; 72 } 73 } 74 75 new_config_file = new_config_dir.get_child(SETTINGS_FILENAME); 76 if (new_config_file.query_exists()) 77 continue; 78 79 try { 80 old_config_file.copy(new_config_file, FileCopyFlags.NONE); 81 } catch (Error err) { 82 debug("Error copying over to %s", new_config_dir.get_path()); 83 continue; 84 } 85 KeyFile key_file = new KeyFile(); 86 try { 87 key_file.load_from_file(new_config_file.get_path(), KeyFileFlags.NONE); 88 } catch (Error err) { 89 debug("Error opening %s", new_config_file.get_path()); 90 continue; 91 } 92 93 // Write the primary email key in the new config file. 94 key_file.set_value(GROUP, PRIMARY_EMAIL_KEY, email); 95 string data = key_file.to_data(); 96 try { 97 new_config_file.replace_contents(data.data, null, false, FileCreateFlags.NONE, 98 null); 99 } catch (Error e) { 100 debug("Error writing email %s to config file", email); 101 continue; 102 } 103 is_migrated.create(FileCreateFlags.PRIVATE); 104 } 105 } 106 107 /** 108 * Migrates configuration from release build locations. 109 * 110 * This will migrate configuration from release build locations to 111 * the current config directory, if and only if the current config 112 * directory is empty. For example, from the standard 113 * distro-package config location to the current Flatpak location, 114 * or from either to a development config location. 115 */ 116 public static void release_config(GLib.File[] search_path, 117 GLib.File config_dir) 118 throws GLib.Error { 119 if (is_directory_empty(config_dir)) { 120 GLib.File? most_recent = null; 121 GLib.DateTime most_recent_modified = null; 122 foreach (var source in search_path) { 123 if (!source.equal(config_dir)) { 124 GLib.DateTime? src_modified = null; 125 try { 126 GLib.FileInfo? src_info = source.query_info( 127 GLib.FileAttribute.TIME_MODIFIED, 0 128 ); 129 if (src_info != null) { 130 src_modified = 131 src_info.get_modification_date_time(); 132 } 133 } catch (GLib.IOError.NOT_FOUND err) { 134 // fine 135 } catch (GLib.Error err) { 136 debug( 137 "Error querying release config dir %s: %s", 138 source.get_path(), 139 err.message 140 ); 141 } 142 if (most_recent_modified == null || 143 (src_modified != null && 144 most_recent_modified.compare(src_modified) < 0)) { 145 most_recent = source; 146 most_recent_modified = src_modified; 147 } 148 } 149 } 150 151 if (most_recent != null) { 152 try { 153 debug( 154 "Migrating release config from %s to %s", 155 most_recent.get_path(), 156 config_dir.get_path() 157 ); 158 recursive_copy(most_recent, config_dir); 159 } catch (GLib.Error err) { 160 debug("Error migrating release config: %s", err.message); 161 } 162 } 163 } 164 } 165 166 private bool is_directory_empty(GLib.File dir) { 167 bool is_empty = true; 168 GLib.FileEnumerator? existing = null; 169 try { 170 existing = dir.enumerate_children( 171 GLib.FileAttribute.STANDARD_TYPE, 0 172 ); 173 } catch (GLib.IOError.NOT_FOUND err) { 174 // fine 175 } catch (GLib.Error err) { 176 debug( 177 "Error enumerating directory %s: %s", 178 dir.get_path(), 179 err.message 180 ); 181 } 182 183 if (existing != null) { 184 try { 185 is_empty = existing.next_file() == null; 186 } catch (GLib.Error err) { 187 debug( 188 "Error getting next child in directory %s: %s", 189 dir.get_path(), 190 err.message 191 ); 192 } 193 194 try { 195 existing.close(); 196 } catch (GLib.Error err) { 197 debug( 198 "Error closing directory enumeration %s: %s", 199 dir.get_path(), 200 err.message 201 ); 202 } 203 } 204 205 return is_empty; 206 } 207 208 private static void recursive_copy(GLib.File src, 209 GLib.File dest, 210 GLib.Cancellable? cancellable = null 211 ) throws GLib.Error { 212 switch (src.query_file_type(NONE, cancellable)) { 213 case DIRECTORY: 214 try { 215 dest.make_directory(cancellable); 216 } catch (GLib.IOError.EXISTS err) { 217 // fine 218 } 219 src.copy_attributes(dest, NONE, cancellable); 220 221 GLib.FileEnumerator children = src.enumerate_children( 222 GLib.FileAttribute.STANDARD_NAME, 223 NONE, 224 cancellable 225 ); 226 GLib.FileInfo? child = children.next_file(cancellable); 227 while (child != null) { 228 recursive_copy( 229 src.get_child(child.get_name()), 230 dest.get_child(child.get_name()) 231 ); 232 child = children.next_file(cancellable); 233 } 234 break; 235 236 case REGULAR: 237 src.copy(dest, NONE, cancellable); 238 break; 239 240 default: 241 // no-op 242 break; 243 } 244 } 245 246 public const string OLD_APP_ID = "org.yorba.geary"; 247 private const string MIGRATED_CONFIG_KEY = "migrated-config"; 248 249 public static void old_app_config(Settings newSettings, string old_app_id = OLD_APP_ID) { 250 SettingsSchemaSource schemaSource = SettingsSchemaSource.get_default(); 251 if (Application.Client.GSETTINGS_DIR != null) { 252 try { 253 schemaSource = new SettingsSchemaSource.from_directory(Application.Client.GSETTINGS_DIR, null, false); 254 } catch (Error e) { 255 // If it didn't work, do nothing (i.e. use the default GSettings dir) 256 } 257 } 258 SettingsSchema oldSettingsSchema = schemaSource.lookup(old_app_id, false); 259 260 if (newSettings.get_boolean(MIGRATED_CONFIG_KEY)) 261 return; 262 263 if (oldSettingsSchema != null) { 264 Settings oldSettings = new Settings.full(oldSettingsSchema, null, null); 265 foreach (string key in newSettings.settings_schema.list_keys()) { 266 if (oldSettingsSchema.has_key(key)) { 267 newSettings.set_value(key, oldSettings.get_value(key)); 268 } 269 } 270 } 271 272 newSettings.set_boolean(MIGRATED_CONFIG_KEY, true); 273 } 274 275 276} 277