Skip to content

Commit e4b1e17

Browse files
committed
Created Android migration script
1 parent 8780569 commit e4b1e17

File tree

2 files changed

+164
-0
lines changed

2 files changed

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

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+
AsyncStorageMigration.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)