[gnome-games] migrator: Implement automatic migration



commit 25f9d539ffe273c32b3dae7cde41f16e6679399f
Author: Yetizone <andreii lisita gmail com>
Date:   Mon Jul 22 14:30:08 2019 +0300

    migrator: Implement automatic migration

 src/core/migrator.vala  | 270 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/meson.build         |   1 +
 src/ui/application.vala |   5 +
 3 files changed, 276 insertions(+)
---
diff --git a/src/core/migrator.vala b/src/core/migrator.vala
new file mode 100644
index 00000000..e3f4353b
--- /dev/null
+++ b/src/core/migrator.vala
@@ -0,0 +1,270 @@
+// This file is part of GNOME Games. License: GPL-3.0+.
+
+public class Games.Migrator : Object {
+       // Returns true if the migration wasn't necessary or
+       // if it was performed succesfully
+       public static bool apply_migration_if_necessary () {
+               var data_dir_path = Application.get_data_dir ();
+               var data_dir = File.new_for_path (data_dir_path);
+
+               var backup_archive_path = Path.build_filename (data_dir_path, "exported_data.zip");
+               var backup_archive = File.new_for_path (backup_archive_path);
+               var database_path = Application.get_database_path ();
+               string[] backup_excluded_files = { database_path, backup_archive_path };
+
+               var version_file = data_dir.get_child (".version");
+
+               // If the version file exists, there's no need
+               // to apply the migration
+               if (version_file.query_exists ())
+                       return true;
+
+               info ("[Migrator]: Migration is necessary");
+
+               // Attempt to create a backup of the previous data
+               try {
+                       backup_archive.create (FileCreateFlags.NONE);
+                       FileOperations.compress_dir (backup_archive_path, data_dir, backup_excluded_files);
+               }
+               catch (Error e) {
+                       critical ("Unable to backup data, aborting migration: %s", e.message);
+                       return false;
+               }
+
+               try {
+                       // The migration executes file I/O which may result in errors being
+                       // thrown
+                       apply_migration (version_file);
+               }
+               catch (Error e) {
+                       critical ("Migration failed: %s", e.message);
+
+                       // Delete all directories from the data dir
+                       var savestates_dir_path = Path.build_filename (data_dir_path, "/savestates");
+                       var savestates_dir = File.new_for_path (savestates_dir_path);
+
+                       delete_files_no_errors (savestates_dir);
+                       delete_old_directories ();
+
+                       // Attempt to restore data from backup
+                       if (try_restore_data (backup_archive_path, data_dir_path, backup_excluded_files)) {
+                               // Successfully restored data from backup, deleting backup
+                               delete_files_no_errors (backup_archive);
+                       }
+                       else {
+                               // Something went seriously wrong here
+                               // Migration failed and restoring backup data also failed
+                               assert_not_reached ();
+                       }
+
+                       return false;
+               }
+
+               // Migration applied succesfully, deleting backup
+               delete_files_no_errors (backup_archive);
+
+               return true;
+       }
+
+       private static void apply_migration (File version_file) throws Error {
+               // Create the version file
+               version_file.create (FileCreateFlags.NONE);
+
+               // Create the savestates dir
+               var savestates_dir = File.new_for_path (get_savestates_dir_path ());
+               savestates_dir.make_directory ();
+
+               // Currently any game has only one snapshot file
+               // So for every snapshot file create a savestate
+               var snapshots_dir_path = get_old_snapshots_dir_path ();
+               var snapshots_dir = Dir.open (snapshots_dir_path, 0);
+               var file_name = "";
+               while ((file_name = snapshots_dir.read_name ()) != null) {
+                       var file_name_tokens = file_name.split (".snapshot");
+
+                       if (file_name_tokens.length != 2)
+                               continue; // Not a snapshot file
+
+                       // The snapshot files are curently named "[game_uid].snapshot"
+                       var game_uid = file_name_tokens[0];
+                       create_first_game_savestate (game_uid);
+               }
+
+               delete_old_directories ();
+       }
+
+       private static void create_first_game_savestate (string game_uid) throws Error {
+               // Inside the savestates dir there will be a sub-dir for each game
+               // which will contain all of the savestates for that game
+               // These sub-dirs will be named "[game_uid]-[core]"
+
+               // Getting the core_id
+               var platform = platform_from_game_uid (game_uid);
+               var core_manager = RetroCoreManager.get_instance ();
+               var preferred_core = core_manager.get_preferred_core (platform);
+               var core_id = preferred_core.get_id ();
+
+               // Create the directory for the game's savestates
+               var core_id_prefix = core_id.replace (".libretro", ""); // Remove the ".libretro" from the 
core_id
+               var game_savestates_dir_name = game_uid + "-" + core_id_prefix;
+               var game_savestates_dir_path = Path.build_filename (get_savestates_dir_path (), 
game_savestates_dir_name);
+               var game_savestates_dir = File.new_for_path (game_savestates_dir_path);
+
+               game_savestates_dir.make_directory ();
+
+               // Create the directory for the first savestate
+               var now_time = new DateTime.now ();
+               var now_time_str = now_time.to_string ();
+               var savestate_dir_path = Path.build_filename (game_savestates_dir_path, now_time_str);
+               var savestate_dir = File.new_for_path (savestate_dir_path);
+
+               savestate_dir.make_directory ();
+
+               // Use the currently existing game data (snapshot, screenshot,
+               // save file, save dir) to populate the savestate
+               var snapshots_dir_path = get_old_snapshots_dir_path ();
+               var snapshot_path = Path.build_filename (snapshots_dir_path, game_uid + ".snapshot");
+               var screenshot_path = Path.build_filename (snapshots_dir_path, game_uid + ".png");
+               var saves_dir = get_old_saves_dir_path ();
+               var save_dir_path = Path.build_filename (saves_dir, game_uid);
+               var save_file_path = save_dir_path + ".save";
+               var medias_dir = get_old_medias_dir_path ();
+               var media_file_path = Path.build_filename (medias_dir, game_uid + ".media");
+
+               var snapshot_file = File.new_for_path (snapshot_path);
+               var screenshot_file = File.new_for_path (screenshot_path);
+               var save_dir = File.new_for_path (save_dir_path);
+               var save_file = File.new_for_path (save_file_path);
+               var media_file = File.new_for_path (media_file_path);
+
+               var savestate_snapshot_file_path = Path.build_filename (savestate_dir_path, "snapshot");
+               var savestate_snapshot_file = File.new_for_path (savestate_snapshot_file_path);
+               FileOperations.copy_contents (snapshot_file, savestate_snapshot_file);
+
+               var savestate_screenshot_file_path = Path.build_filename (savestate_dir_path, "screenshot");
+               var savestate_screenshot_file = File.new_for_path (savestate_screenshot_file_path);
+               FileOperations.copy_contents (screenshot_file, savestate_screenshot_file);
+
+               if (!save_dir.query_exists ())
+                       save_dir.make_directory ();
+
+               var savestate_save_dir_path = Path.build_filename (savestate_dir_path, "save-dir");
+               var savestate_save_dir = File.new_for_path (savestate_save_dir_path);
+               FileOperations.copy_dir (save_dir, savestate_save_dir);
+
+               if (save_file.query_exists ()) {
+                       var savestate_save_file_path = Path.build_filename (savestate_dir_path, "save");
+                       var savestate_save_file = File.new_for_path (savestate_save_file_path);
+                       FileOperations.copy_contents (save_file, savestate_save_file);
+               }
+
+               if (media_file.query_exists ()) {
+                       var savestate_media_file_path = Path.build_filename (savestate_dir_path, "media");
+                       var savestate_media_file = File.new_for_path (savestate_media_file_path);
+                       FileOperations.copy_contents (media_file, savestate_media_file);
+               }
+
+               // Create a KeyFile with additional data
+               var metadata = new KeyFile ();
+               var metadata_file_path = Path.build_filename (savestate_dir_path, "metadata");
+
+               // Automatic means whether the savestate was created automatically when
+               // quitting/loading the game or manually by the user using the Save button
+               metadata.set_boolean ("Metadata", "Automatic", true);
+               metadata.set_string ("Metadata", "Creation Date", now_time_str);
+               metadata.set_string ("Metadata", "Platform", platform.get_uid_prefix ());
+               metadata.set_string ("Metadata", "Core", core_id);
+               metadata.save_to_file (metadata_file_path);
+       }
+
+
+       private static RetroPlatform platform_from_game_uid (string game_uid) {
+               // [game_uid] is currently formed as "[platform]-[hash]"
+               // So we can use the game_uid to get the platform
+               var platforms_register = PlatformRegister.get_register ();
+               var platforms = platforms_register.get_all_platforms ();
+
+               string best_match = null;
+               RetroPlatform result_platform = null;
+
+               foreach (var platform in platforms) {
+                       var retro_platform = platform as RetroPlatform;
+
+                       if (retro_platform == null)
+                               continue; // not a RetroPlatform
+
+                       var platform_uid_prefix = platform.get_uid_prefix ();
+
+                       if (game_uid.contains (platform_uid_prefix)) {
+                               if (best_match == null || platform_uid_prefix.length > best_match.length) {
+                                       best_match = platform_uid_prefix;
+                                       result_platform = retro_platform;
+                               }
+                       }
+               }
+
+               return result_platform;
+       }
+
+       // Delete the old snapshots, saves and medias directories
+       private static void delete_old_directories () {
+               var snapshots_dir_path = get_old_snapshots_dir_path ();
+               var saves_dir_path = get_old_saves_dir_path ();
+               var medias_dir_path = get_old_medias_dir_path ();
+
+               var snapshots_dir = File.new_for_path (snapshots_dir_path);
+               var saves_dir = File.new_for_path (saves_dir_path);
+               var medias_dir = File.new_for_path (medias_dir_path);
+
+               delete_files_no_errors (snapshots_dir);
+               delete_files_no_errors (saves_dir);
+               delete_files_no_errors (medias_dir);
+       }
+
+       private static string get_old_snapshots_dir_path () {
+               var data_dir = Application.get_data_dir ();
+
+               return @"$data_dir/snapshots";
+       }
+
+       private static string get_old_saves_dir_path () {
+               var data_dir = Application.get_data_dir ();
+
+               return @"$data_dir/saves";
+       }
+
+       private static string get_old_medias_dir_path () {
+               var data_dir = Application.get_data_dir ();
+
+               return @"$data_dir/medias";
+       }
+
+       private static string get_savestates_dir_path () {
+               var data_dir = Application.get_data_dir ();
+
+               return @"$data_dir/savestates";
+       }
+
+       // Method for deleting files that treats errors locally because there isn't
+       // much that can go wrong with deleting files
+       private static void delete_files_no_errors (File file) {
+               try {
+                       FileOperations.delete_files (file, {});
+               }
+               catch (Error e) {
+                       warning ("Cannot delete file %s: %s", file.get_path (), e.message);
+               }
+       }
+
+       private static bool try_restore_data (string backup_archive_path, string data_dir_path, string[] 
backup_excluded_files) {
+               try {
+                       FileOperations.extract_archive (backup_archive_path, data_dir_path, 
backup_excluded_files);
+               }
+               catch (Error e) {
+                       critical ("Failed to restore data from backup archive %s: %s", backup_archive_path, 
e.message);
+                       return false;
+               }
+
+               return true; // Succesfully restored data from backup
+       }
+}
diff --git a/src/meson.build b/src/meson.build
index 86c0a3a1..bee993dc 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -30,6 +30,7 @@ vala_sources = [
   'core/media-info.vala',
   'core/media-set/media-set.vala',
   'core/media-set/media-set-error.vala',
+  'core/migrator.vala',
   'core/platform.vala',
   'core/platform-register.vala',
   'core/players.vala',
diff --git a/src/ui/application.vala b/src/ui/application.vala
index 7a26573c..4ac96e88 100644
--- a/src/ui/application.vala
+++ b/src/ui/application.vala
@@ -329,6 +329,11 @@ public class Games.Application : Gtk.Application {
                                debug ("Error: %s", e.message);
                        }
                }
+
+               // Re-organize data_dir layout if necessary
+               // This operation has to be executed _after_ the PlatformsRegister has
+               // been populated and therefore this call is placed here
+               Migrator.apply_migration_if_necessary ();
        }
 
        private async Game? game_for_uris (Uri[] uris) {


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