Skip to content

Commit a21cb9f

Browse files
authored
Created Android scoped storage migration method (#559)
* Created Android migration script * Update AsyncStorageMigration.java * Removed delete step * Revert "Removed delete step" This reverts commit 6fbaf4c. * [feedback] Remove private context * [feedback] inline statement * Update AsyncStorageMigration.java * rename migration module * [feedback] Use correct ReactDatabaseSupplier
1 parent 8780569 commit a21cb9f

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.reactnativecommunity.asyncstorage;
2+
3+
import android.content.Context;
4+
import android.os.Build;
5+
import android.util.Log;
6+
7+
import androidx.annotation.RequiresApi;
8+
9+
import java.io.File;
10+
import java.io.FileInputStream;
11+
import java.io.FileOutputStream;
12+
import java.io.IOException;
13+
import java.nio.channels.FileChannel;
14+
import java.nio.file.Files;
15+
import java.nio.file.attribute.BasicFileAttributes;
16+
import java.util.ArrayList;
17+
18+
// A utility class that migrates a scoped AsyncStorage database to RKStorage.
19+
// This utility only runs if the RKStorage file has not been created yet.
20+
public class AsyncStorageExpoMigration {
21+
static final String LOG_TAG = "AsyncStorageExpoMigration";
22+
23+
public static void migrate(Context context) {
24+
// Only migrate if the default async storage file does not exist.
25+
if (isAsyncStorageDatabaseCreated(context)) {
26+
return;
27+
}
28+
29+
ArrayList<File> expoDatabases = getExpoDatabases(context);
30+
31+
File expoDatabase = getLastModifiedFile(expoDatabases);
32+
33+
if (expoDatabase == null) {
34+
Log.v(LOG_TAG, "No scoped database found");
35+
return;
36+
}
37+
38+
try {
39+
// Create the storage file
40+
ReactDatabaseSupplier.getInstance(context).get();
41+
copyFile(new FileInputStream(expoDatabase), new FileOutputStream(context.getDatabasePath(ReactDatabaseSupplier.DATABASE_NAME)));
42+
Log.v(LOG_TAG, "Migrated most recently modified database " + expoDatabase.getName() + " to RKStorage");
43+
} catch (Exception e) {
44+
Log.v(LOG_TAG, "Failed to migrate scoped database " + expoDatabase.getName());
45+
e.printStackTrace();
46+
return;
47+
}
48+
49+
try {
50+
for (File file : expoDatabases) {
51+
if (file.delete()) {
52+
Log.v(LOG_TAG, "Deleted scoped database " + file.getName());
53+
} else {
54+
Log.v(LOG_TAG, "Failed to delete scoped database " + file.getName());
55+
}
56+
}
57+
} catch (Exception e) {
58+
e.printStackTrace();
59+
}
60+
61+
Log.v(LOG_TAG, "Completed the scoped AsyncStorage migration");
62+
}
63+
64+
private static boolean isAsyncStorageDatabaseCreated(Context context) {
65+
return context.getDatabasePath(ReactDatabaseSupplier.DATABASE_NAME).exists();
66+
}
67+
68+
// Find all database files that the user may have created while using Expo.
69+
private static ArrayList<File> getExpoDatabases(Context context) {
70+
ArrayList<File> scopedDatabases = new ArrayList<>();
71+
try {
72+
File databaseDirectory = context.getDatabasePath("noop").getParentFile();
73+
File[] directoryListing = databaseDirectory.listFiles();
74+
if (directoryListing != null) {
75+
for (File child : directoryListing) {
76+
// Find all databases matching the Expo scoped key, and skip any database journals.
77+
if (child.getName().startsWith("RKStorage-scoped-experience-") && !child.getName().endsWith("-journal")) {
78+
scopedDatabases.add(child);
79+
}
80+
}
81+
}
82+
} catch (Exception e) {
83+
// Just in case anything happens catch and print, file system rules can tend to be different across vendors.
84+
e.printStackTrace();
85+
}
86+
return scopedDatabases;
87+
}
88+
89+
// Returns the most recently modified file.
90+
// If a user publishes an app with Expo, then changes the slug
91+
// and publishes again, a new database will be created.
92+
// We want to select the most recent database and migrate it to RKStorage.
93+
private static File getLastModifiedFile(ArrayList<File> files) {
94+
if (files.size() == 0) {
95+
return null;
96+
}
97+
long lastMod = -1;
98+
File lastModFile = null;
99+
for (File child : files) {
100+
long modTime = getLastModifiedTimeInMillis(child);
101+
if (modTime > lastMod) {
102+
lastMod = modTime;
103+
lastModFile = child;
104+
}
105+
}
106+
if (lastModFile != null) {
107+
return lastModFile;
108+
}
109+
110+
return files.get(0);
111+
}
112+
113+
private static long getLastModifiedTimeInMillis(File file) {
114+
try {
115+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
116+
return getLastModifiedTimeFromBasicFileAttrs(file);
117+
} else {
118+
return file.lastModified();
119+
}
120+
} catch (Exception e) {
121+
e.printStackTrace();
122+
return -1;
123+
}
124+
}
125+
126+
@RequiresApi(Build.VERSION_CODES.O)
127+
private static long getLastModifiedTimeFromBasicFileAttrs(File file) {
128+
try {
129+
return Files.readAttributes(file.toPath(), BasicFileAttributes.class).creationTime().toMillis();
130+
} catch (Exception e) {
131+
return -1;
132+
}
133+
}
134+
135+
private static void copyFile(FileInputStream fromFile, FileOutputStream toFile) throws IOException {
136+
FileChannel fromChannel = null;
137+
FileChannel toChannel = null;
138+
try {
139+
fromChannel = fromFile.getChannel();
140+
toChannel = toFile.getChannel();
141+
fromChannel.transferTo(0, fromChannel.size(), toChannel);
142+
} finally {
143+
try {
144+
if (fromChannel != null) {
145+
fromChannel.close();
146+
}
147+
} finally {
148+
if (toChannel != null) {
149+
toChannel.close();
150+
}
151+
}
152+
}
153+
}
154+
}

android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStorageModule.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,12 @@ public AsyncStorageModule(ReactApplicationContext reactContext) {
9191
@VisibleForTesting
9292
AsyncStorageModule(ReactApplicationContext reactContext, Executor executor) {
9393
super(reactContext);
94+
// The migration MUST run before the AsyncStorage database is created for the first time.
95+
AsyncStorageExpoMigration.migrate(reactContext);
96+
9497
this.executor = new SerialExecutor(executor);
9598
reactContext.addLifecycleEventListener(this);
99+
// Creating the database MUST happen after the migration.
96100
mReactDatabaseSupplier = ReactDatabaseSupplier.getInstance(reactContext);
97101
}
98102

0 commit comments

Comments
 (0)