From e4b1e1724dff9e22c5ad946658ffcc9140640409 Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Fri, 12 Mar 2021 19:39:00 -0800 Subject: [PATCH 1/9] Created Android migration script --- .../asyncstorage/AsyncStorageMigration.java | 160 ++++++++++++++++++ .../asyncstorage/AsyncStorageModule.java | 4 + 2 files changed, 164 insertions(+) create mode 100644 android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java new file mode 100644 index 00000000..979ac64c --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java @@ -0,0 +1,160 @@ +package com.reactnativecommunity.asyncstorage; + +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import com.facebook.react.modules.storage.ReactDatabaseSupplier; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; + +// A utility class that migrates a scoped AsyncStorage database to RKStorage. +// This utility only runs if the RKStorage file has not been created yet. +public class AsyncStorageMigration { + static final String LOG_TAG = "ScopedStorageMigration"; + + private static Context mContext; + + public static void migrate(Context context) { + mContext = context; + + // Only migrate if the default async storage file does not exist. + if (isAsyncStorageDatabaseCreated()) { + return; + } + + ArrayList expoDatabases = getExpoDatabases(); + + File expoDatabase = getLastModifiedFile(expoDatabases); + + if (expoDatabase == null) { + Log.v(LOG_TAG, "No scoped database found"); + return; + } + + try { + // Create the storage file + ReactDatabaseSupplier.getInstance(mContext).get(); + copyFile(new FileInputStream(expoDatabase), new FileOutputStream(mContext.getDatabasePath(ReactDatabaseSupplier.DATABASE_NAME))); + } catch (Exception e) { + Log.v(LOG_TAG, "Failed to move scoped database"); + e.printStackTrace(); + return; + } + + try { + for (File file : expoDatabases) { + if (file.delete()) { + Log.v(LOG_TAG, "Deleted scoped database " + file.getName()); + } else { + Log.v(LOG_TAG, "Failed to delete scoped database " + file.getName()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + Log.v(LOG_TAG, "Completed the scoped AsyncStorage migration"); + } + + private static boolean isAsyncStorageDatabaseCreated() { + return mContext.getDatabasePath(ReactDatabaseSupplier.DATABASE_NAME).exists(); + } + + // Find all database files that the user may have created while using Expo. + private static ArrayList getExpoDatabases() { + ArrayList scopedDatabases = new ArrayList<>(); + try { + File databaseDirectory = mContext.getDatabasePath("noop").getParentFile(); + File[] directoryListing = databaseDirectory.listFiles(); + if (directoryListing != null) { + for (File child : directoryListing) { + // Find all databases matching the Expo scoped key, and skip any database journals. + if (child.getName().startsWith("RKStorage-scoped-experience-") && !child.getName().endsWith("-journal")) { + scopedDatabases.add(child); + } + } + } + } catch (Exception e) { + // Just in case anything happens catch and print, file system rules can tend to be different across vendors. + e.printStackTrace(); + } + return scopedDatabases; + } + + // Returns the most recently modified file. + // If a user publishes an app with Expo, then changes the slug + // and publishes again, a new database will be created. + // We want to select the most recent database and migrate it to RKStorage. + private static File getLastModifiedFile(ArrayList files) { + if (files.size() == 0) { + return null; + } + long lastMod = -1; + File lastModFile = null; + for (File child : files) { + long modTime = getLastModifiedTimeInMillis(child); + if (modTime > lastMod) { + lastMod = modTime; + lastModFile = child; + } + } + if (lastModFile != null) { + return lastModFile; + } + + return files.get(0); + } + + private static long getLastModifiedTimeInMillis(File file) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return getLastModifiedTimeFromBasicFileAttrs(file); + } else { + return file.lastModified(); + } + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private static long getLastModifiedTimeFromBasicFileAttrs(File file) { + try { + BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class); + return attr.creationTime().toMillis(); + } catch (Exception e) { + return -1; + } + } + + private static void copyFile(FileInputStream fromFile, FileOutputStream toFile) throws IOException { + FileChannel fromChannel = null; + FileChannel toChannel = null; + try { + fromChannel = fromFile.getChannel(); + toChannel = toFile.getChannel(); + fromChannel.transferTo(0, fromChannel.size(), toChannel); + } finally { + try { + if (fromChannel != null) { + fromChannel.close(); + } + } finally { + if (toChannel != null) { + toChannel.close(); + } + } + } + } +} diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java index 9936ee50..a387255f 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java @@ -91,8 +91,12 @@ public AsyncStorageModule(ReactApplicationContext reactContext) { @VisibleForTesting AsyncStorageModule(ReactApplicationContext reactContext, Executor executor) { super(reactContext); + // The migration MUST run before the AsyncStorage database is created for the first time. + AsyncStorageMigration.migrate(reactContext); + this.executor = new SerialExecutor(executor); reactContext.addLifecycleEventListener(this); + // Creating the database MUST happen after the migration. mReactDatabaseSupplier = ReactDatabaseSupplier.getInstance(reactContext); } From ac4fade1d1d333b962a4b86f52eac63c4d043eb1 Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Fri, 12 Mar 2021 20:13:10 -0800 Subject: [PATCH 2/9] Update AsyncStorageMigration.java --- .../asyncstorage/AsyncStorageMigration.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java index 979ac64c..8557bdfe 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java @@ -45,8 +45,9 @@ public static void migrate(Context context) { // Create the storage file ReactDatabaseSupplier.getInstance(mContext).get(); copyFile(new FileInputStream(expoDatabase), new FileOutputStream(mContext.getDatabasePath(ReactDatabaseSupplier.DATABASE_NAME))); + Log.v(LOG_TAG, "Migrated most recently modified database " + expoDatabase.getName() + " to RKStorage"); } catch (Exception e) { - Log.v(LOG_TAG, "Failed to move scoped database"); + Log.v(LOG_TAG, "Failed to migrate scoped database " + expoDatabase.getName()); e.printStackTrace(); return; } From 6fbaf4cf9e0f250450a4f9b18e390fd4ceba304b Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Fri, 12 Mar 2021 21:01:03 -0800 Subject: [PATCH 3/9] Removed delete step --- .../asyncstorage/AsyncStorageMigration.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java index 8557bdfe..3feeb2ef 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java @@ -52,18 +52,6 @@ public static void migrate(Context context) { return; } - try { - for (File file : expoDatabases) { - if (file.delete()) { - Log.v(LOG_TAG, "Deleted scoped database " + file.getName()); - } else { - Log.v(LOG_TAG, "Failed to delete scoped database " + file.getName()); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - Log.v(LOG_TAG, "Completed the scoped AsyncStorage migration"); } From ededf89010af4517f6ba5f88860f8d3cab71b151 Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Fri, 12 Mar 2021 22:28:53 -0800 Subject: [PATCH 4/9] Revert "Removed delete step" This reverts commit 6fbaf4cf9e0f250450a4f9b18e390fd4ceba304b. --- .../asyncstorage/AsyncStorageMigration.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java index 3feeb2ef..8557bdfe 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java @@ -52,6 +52,18 @@ public static void migrate(Context context) { return; } + try { + for (File file : expoDatabases) { + if (file.delete()) { + Log.v(LOG_TAG, "Deleted scoped database " + file.getName()); + } else { + Log.v(LOG_TAG, "Failed to delete scoped database " + file.getName()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + Log.v(LOG_TAG, "Completed the scoped AsyncStorage migration"); } From 443e64f6f0c044afaaa9223947a8b73f5eb2601f Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Sun, 21 Mar 2021 16:16:54 -0700 Subject: [PATCH 5/9] [feedback] Remove private context --- .../asyncstorage/AsyncStorageMigration.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java index 8557bdfe..73a09fd1 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java @@ -22,17 +22,13 @@ public class AsyncStorageMigration { static final String LOG_TAG = "ScopedStorageMigration"; - private static Context mContext; - public static void migrate(Context context) { - mContext = context; - // Only migrate if the default async storage file does not exist. - if (isAsyncStorageDatabaseCreated()) { + if (isAsyncStorageDatabaseCreated(context)) { return; } - ArrayList expoDatabases = getExpoDatabases(); + ArrayList expoDatabases = getExpoDatabases(context); File expoDatabase = getLastModifiedFile(expoDatabases); @@ -43,8 +39,8 @@ public static void migrate(Context context) { try { // Create the storage file - ReactDatabaseSupplier.getInstance(mContext).get(); - copyFile(new FileInputStream(expoDatabase), new FileOutputStream(mContext.getDatabasePath(ReactDatabaseSupplier.DATABASE_NAME))); + ReactDatabaseSupplier.getInstance(context).get(); + copyFile(new FileInputStream(expoDatabase), new FileOutputStream(context.getDatabasePath(ReactDatabaseSupplier.DATABASE_NAME))); Log.v(LOG_TAG, "Migrated most recently modified database " + expoDatabase.getName() + " to RKStorage"); } catch (Exception e) { Log.v(LOG_TAG, "Failed to migrate scoped database " + expoDatabase.getName()); @@ -67,15 +63,15 @@ public static void migrate(Context context) { Log.v(LOG_TAG, "Completed the scoped AsyncStorage migration"); } - private static boolean isAsyncStorageDatabaseCreated() { - return mContext.getDatabasePath(ReactDatabaseSupplier.DATABASE_NAME).exists(); + private static boolean isAsyncStorageDatabaseCreated(Context context) { + return context.getDatabasePath(ReactDatabaseSupplier.DATABASE_NAME).exists(); } // Find all database files that the user may have created while using Expo. - private static ArrayList getExpoDatabases() { + private static ArrayList getExpoDatabases(Context context) { ArrayList scopedDatabases = new ArrayList<>(); try { - File databaseDirectory = mContext.getDatabasePath("noop").getParentFile(); + File databaseDirectory = context.getDatabasePath("noop").getParentFile(); File[] directoryListing = databaseDirectory.listFiles(); if (directoryListing != null) { for (File child : directoryListing) { From 647b402c1a23f8daa051b3adcaa2404b76d8b76c Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Sun, 21 Mar 2021 16:18:17 -0700 Subject: [PATCH 6/9] [feedback] inline statement --- .../asyncstorage/AsyncStorageMigration.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java index 73a09fd1..3aac4acc 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java @@ -128,8 +128,7 @@ private static long getLastModifiedTimeInMillis(File file) { @RequiresApi(Build.VERSION_CODES.O) private static long getLastModifiedTimeFromBasicFileAttrs(File file) { try { - BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class); - return attr.creationTime().toMillis(); + return Files.readAttributes(file.toPath(), BasicFileAttributes.class).creationTime().toMillis(); } catch (Exception e) { return -1; } From e99829f35df8911a09e6762c9862040d02dbb80a Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Sun, 21 Mar 2021 16:19:32 -0700 Subject: [PATCH 7/9] Update AsyncStorageMigration.java --- .../asyncstorage/AsyncStorageMigration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java index 3aac4acc..559a58f4 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java @@ -20,7 +20,7 @@ // A utility class that migrates a scoped AsyncStorage database to RKStorage. // This utility only runs if the RKStorage file has not been created yet. public class AsyncStorageMigration { - static final String LOG_TAG = "ScopedStorageMigration"; + static final String LOG_TAG = "AsyncStorageExpoMigration"; public static void migrate(Context context) { // Only migrate if the default async storage file does not exist. From 00ca567e3c4000883c1c014ae0f350015091681b Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Sun, 21 Mar 2021 16:24:45 -0700 Subject: [PATCH 8/9] rename migration module --- ...syncStorageMigration.java => AsyncStorageExpoMigration.java} | 2 +- .../reactnativecommunity/asyncstorage/AsyncStorageModule.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename android/src/main/java/com/reactnativecommunity/asyncstorage/{AsyncStorageMigration.java => AsyncStorageExpoMigration.java} (99%) diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageExpoMigration.java similarity index 99% rename from android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java rename to android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageExpoMigration.java index 559a58f4..b804ae84 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageMigration.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageExpoMigration.java @@ -19,7 +19,7 @@ // A utility class that migrates a scoped AsyncStorage database to RKStorage. // This utility only runs if the RKStorage file has not been created yet. -public class AsyncStorageMigration { +public class AsyncStorageExpoMigration { static final String LOG_TAG = "AsyncStorageExpoMigration"; public static void migrate(Context context) { diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java index a387255f..0e4c7893 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java @@ -92,7 +92,7 @@ public AsyncStorageModule(ReactApplicationContext reactContext) { AsyncStorageModule(ReactApplicationContext reactContext, Executor executor) { super(reactContext); // The migration MUST run before the AsyncStorage database is created for the first time. - AsyncStorageMigration.migrate(reactContext); + AsyncStorageExpoMigration.migrate(reactContext); this.executor = new SerialExecutor(executor); reactContext.addLifecycleEventListener(this); From f3be28dbc8055940609926d24d6e4890f4a4936d Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Sun, 21 Mar 2021 16:48:17 -0700 Subject: [PATCH 9/9] [feedback] Use correct ReactDatabaseSupplier --- .../asyncstorage/AsyncStorageExpoMigration.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageExpoMigration.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageExpoMigration.java index b804ae84..cd79cc09 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageExpoMigration.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageExpoMigration.java @@ -6,8 +6,6 @@ import androidx.annotation.RequiresApi; -import com.facebook.react.modules.storage.ReactDatabaseSupplier; - import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream;