diff --git a/parse/src/main/java/com/parse/Parse.java b/parse/src/main/java/com/parse/Parse.java
index 39cdc2545..c3ad5c1ee 100644
--- a/parse/src/main/java/com/parse/Parse.java
+++ b/parse/src/main/java/com/parse/Parse.java
@@ -76,7 +76,7 @@ private Parse() {
* }
*
*
- * See See https://github.com/parse-community/Parse-SDK-Android/issues/279
* for a discussion on performance of local datastore, and if it is right for your project.
*
@@ -145,6 +145,9 @@ static void initialize(Configuration configuration, ParsePlugins parsePlugins) {
PLog.w(TAG, "Parse is already initialized");
return;
}
+ // Perform old dir migration on initialize.
+ new ParseCacheDirMigrationUtils(configuration.context).runMigrations();
+
// NOTE (richardross): We will need this here, as ParsePlugins uses the return value of
// isLocalDataStoreEnabled() to perform additional behavior.
isLocalDatastoreEnabled = configuration.localDataStoreEnabled;
diff --git a/parse/src/main/java/com/parse/ParseCacheDirMigrationUtils.java b/parse/src/main/java/com/parse/ParseCacheDirMigrationUtils.java
new file mode 100644
index 000000000..9b7b2725a
--- /dev/null
+++ b/parse/src/main/java/com/parse/ParseCacheDirMigrationUtils.java
@@ -0,0 +1,133 @@
+package com.parse;
+
+import android.content.Context;
+import java.io.File;
+import java.util.ArrayList;
+
+/**
+ * The {@code ParseMigrationUtils} class perform caching dir migration operation for {@code Parse}
+ * SDK.
+ */
+public class ParseCacheDirMigrationUtils {
+ private final String TAG = this.getClass().getName();
+ private final Object lock = new Object();
+ private final Context context;
+
+ protected ParseCacheDirMigrationUtils(Context context) {
+ this.context = context;
+ }
+
+ /*Start old data migrations to new respective locations ("/files/com.parse/", "/cache/com.parse/")*/
+ protected void runMigrations() {
+ synchronized (lock) {
+ runSilentMigration(context);
+ }
+ }
+
+ private void runSilentMigration(Context context) {
+ ArrayList filesToBeMigrated = new ArrayList<>();
+ ParseFileUtils.getAllNestedFiles(
+ getOldParseDir(context).getAbsolutePath(), filesToBeMigrated);
+ if (filesToBeMigrated.isEmpty()) {
+ return;
+ }
+ boolean useFilesDir = false;
+ // Hard coded config file names list.
+ String[] configNamesList = {
+ "installationId",
+ "currentUser",
+ "currentConfig",
+ "currentInstallation",
+ "LocalId",
+ "pushState"
+ };
+ // Start migration for each files in `allFiles`.
+ for (File itemToMove : filesToBeMigrated) {
+ try {
+ for (String configName : configNamesList) {
+ if (itemToMove.getAbsolutePath().contains(configName)) {
+ useFilesDir = true;
+ break;
+ } else {
+ useFilesDir = false;
+ }
+ }
+ File fileToSave =
+ new File(
+ (useFilesDir ? context.getFilesDir() : context.getCacheDir())
+ + "/com.parse/"
+ + getFileOldDir(context, itemToMove),
+ itemToMove.getName());
+ // Perform copy operation if file doesn't exist in the new directory.
+ if (!fileToSave.exists()) {
+ ParseFileUtils.copyFile(itemToMove, fileToSave);
+ logMigrationStatus(
+ itemToMove.getName(),
+ itemToMove.getPath(),
+ fileToSave.getAbsolutePath(),
+ "Successful.");
+ } else {
+ logMigrationStatus(
+ itemToMove.getName(),
+ itemToMove.getPath(),
+ fileToSave.getAbsolutePath(),
+ "Already exist in new location.");
+ }
+ ParseFileUtils.deleteQuietly(itemToMove);
+ PLog.v(TAG, "File deleted: " + "{" + itemToMove.getName() + "}" + " successfully");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ // Check again, if all files has been resolved or not. If yes, delete the old dir
+ // "app_Parse".
+ filesToBeMigrated.clear();
+ ParseFileUtils.getAllNestedFiles(
+ getOldParseDir(context).getAbsolutePath(), filesToBeMigrated);
+ if (filesToBeMigrated.isEmpty()) {
+ try {
+ ParseFileUtils.deleteDirectory(getOldParseDir(context));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ PLog.v(TAG, "Migration completed.");
+ }
+
+ private String getFileOldDir(Context context, File file) {
+ // Parse the old sub directory name where the file should be moved (new location) by
+ // following the old sub directory name.
+ String temp =
+ file.getAbsolutePath()
+ .replace(getOldParseDir(context).getAbsolutePath(), "")
+ .replace("/" + file.getName(), "");
+ // Before returning the path, replace file name from the last, eg. dir name & file name
+ // could be same, as we want to get only dir name.
+ return replaceLast(temp, file.getName());
+ }
+
+ private void logMigrationStatus(
+ String fileName, String oldPath, String newPath, String status) {
+ PLog.v(
+ TAG,
+ "Migration for file: "
+ + "{"
+ + fileName
+ + "}"
+ + " from {"
+ + oldPath
+ + "} to {"
+ + newPath
+ + "}, Status: "
+ + status);
+ }
+
+ /*Replace a given string from the last*/
+ private String replaceLast(String text, String regex) {
+ return text.replaceFirst("(?s)" + regex + "(?!.*?" + regex + ")", "");
+ }
+
+ private File getOldParseDir(Context context) {
+ return context.getDir("Parse", Context.MODE_PRIVATE);
+ }
+}
diff --git a/parse/src/main/java/com/parse/ParseFileUtils.java b/parse/src/main/java/com/parse/ParseFileUtils.java
index 070baa111..50af66982 100644
--- a/parse/src/main/java/com/parse/ParseFileUtils.java
+++ b/parse/src/main/java/com/parse/ParseFileUtils.java
@@ -16,6 +16,7 @@
*/
package com.parse;
+import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -25,6 +26,7 @@
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
+import java.util.List;
import org.json.JSONException;
import org.json.JSONObject;
@@ -347,6 +349,24 @@ private static void doCopyFile(
}
}
+ /**
+ * Get all files path from an given directory (including sub-directory).
+ *
+ * @param directoryName given directory name.
+ * @param files where all the files will be stored.
+ */
+ public static void getAllNestedFiles(@NonNull String directoryName, @NonNull List files) {
+ File[] directoryItems = new File(directoryName).listFiles();
+ if (directoryItems != null)
+ for (File item : directoryItems) {
+ if (item.isFile()) {
+ files.add(item);
+ } else if (item.isDirectory()) {
+ getAllNestedFiles(item.getAbsolutePath(), files);
+ }
+ }
+ }
+
// -----------------------------------------------------------------------
/**
diff --git a/parse/src/test/java/com/parse/ParseCacheDirMigrationUtilsTest.java b/parse/src/test/java/com/parse/ParseCacheDirMigrationUtilsTest.java
new file mode 100644
index 000000000..16869e9b7
--- /dev/null
+++ b/parse/src/test/java/com/parse/ParseCacheDirMigrationUtilsTest.java
@@ -0,0 +1,152 @@
+package com.parse;
+
+import android.content.Context;
+import androidx.test.platform.app.InstrumentationRegistry;
+import java.io.File;
+import java.util.ArrayList;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class ParseCacheDirMigrationUtilsTest {
+ ArrayList writtenFiles = new ArrayList<>();
+ private ParseCacheDirMigrationUtils utils;
+
+ @Before
+ public void setUp() throws Exception {
+ utils =
+ new ParseCacheDirMigrationUtils(
+ InstrumentationRegistry.getInstrumentation().getContext());
+ writtenFiles.clear();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ writtenFiles.clear();
+ }
+
+ @Test
+ public void testMigrationOnParseSDKInitialization() {
+ prepareForMockFilesWriting();
+ writtenFiles.addAll(writeSomeMockFiles(true));
+ Parse.Configuration configuration =
+ new Parse.Configuration.Builder(
+ InstrumentationRegistry.getInstrumentation().getContext())
+ .applicationId(BuildConfig.LIBRARY_PACKAGE_NAME)
+ .server("https://api.parse.com/1")
+ .enableLocalDataStore()
+ .build();
+ Parse.initialize(configuration);
+ }
+
+ @Test
+ public void testMockMigration() {
+ prepareForMockFilesWriting();
+ writtenFiles.addAll(writeSomeMockFiles(true));
+
+ // Run migration.
+ utils.runMigrations();
+
+ // Check for cache file after migration.
+ File cacheDir = InstrumentationRegistry.getInstrumentation().getContext().getCacheDir();
+ ArrayList migratedCaches = new ArrayList<>();
+ ParseFileUtils.getAllNestedFiles(cacheDir.getAbsolutePath(), migratedCaches);
+
+ // Check for files file after migration.
+ File filesDir = InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
+ ArrayList migratedFiles = new ArrayList<>();
+ ParseFileUtils.getAllNestedFiles(filesDir.getAbsolutePath(), migratedFiles);
+
+ // To check migrations result
+ int sizeAfterMigration = (migratedCaches.size() + migratedFiles.size());
+ int sizeBeforeMigrations = writtenFiles.size();
+
+ assert (cacheDir.exists() && !migratedCaches.isEmpty());
+ assert (filesDir.exists() && !migratedFiles.isEmpty());
+ assert sizeBeforeMigrations == sizeAfterMigration;
+ }
+
+ private void prepareForMockFilesWriting() {
+ // Delete `"app_Parse"` dir including nested dir and files.
+ try {
+ ParseFileUtils.deleteDirectory(
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getDir("Parse", Context.MODE_PRIVATE));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ writtenFiles.clear();
+ // Create new `"app_Parse"` dir to write some files.
+ createFileDir(InstrumentationRegistry.getInstrumentation().getContext().getCacheDir());
+ }
+
+ private ArrayList writeSomeMockFiles(Boolean checkForExistingFile) {
+ ArrayList fileToReturn = new ArrayList<>();
+ File oldRef =
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getDir("Parse", Context.MODE_PRIVATE);
+
+ // Writing some config & random files for migration process.
+ File config = new File(oldRef + "/config/", "config");
+ fileToReturn.add(config);
+ File installationId = new File(oldRef + "/CommandCache/", "installationId");
+ fileToReturn.add(installationId);
+ File currentConfig = new File(oldRef + "/", "currentConfig");
+ fileToReturn.add(currentConfig);
+ File currentInstallation = new File(oldRef + "/", "currentInstallation");
+ fileToReturn.add(currentInstallation);
+ File pushState = new File(oldRef + "/push/", "pushState");
+ fileToReturn.add(pushState);
+ File localId = new File(oldRef + "/LocalId/", "LocalId");
+ fileToReturn.add(localId);
+ File cache = new File(oldRef + "/testcache/", "cache");
+ fileToReturn.add(cache);
+ File cache1 = new File(oldRef + "/testcache/", "cache1");
+ fileToReturn.add(cache1);
+ File cache2 = new File(oldRef + "/testcache/another/", "cache4");
+ fileToReturn.add(cache2);
+ File user = new File(oldRef + "/user/", "user_config");
+ fileToReturn.add(user);
+
+ // Write all listed files to the app cache ("app_Parse") directory.
+ for (File item : fileToReturn) {
+ try {
+ ParseFileUtils.writeStringToFile(item, "gger", "UTF-8");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ // To create a file conflict scenario during migration by creating an existing file to the
+ // new files dir ("*/files/com.parse/*").
+ if (checkForExistingFile) {
+ try {
+ ParseFileUtils.writeStringToFile(
+ new File(
+ InstrumentationRegistry.getInstrumentation()
+ .getContext()
+ .getFilesDir()
+ + "/com.parse/CommandCache/",
+ "installationId"),
+ "gger",
+ "UTF-8");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ return fileToReturn;
+ }
+
+ private File createFileDir(File file) {
+ if (!file.exists()) {
+ if (!file.mkdirs()) {
+ return file;
+ }
+ }
+ return file;
+ }
+}
diff --git a/parse/src/test/java/com/parse/ParseFileUtilsTest.java b/parse/src/test/java/com/parse/ParseFileUtilsTest.java
index 4a1465dae..11485d7bd 100644
--- a/parse/src/test/java/com/parse/ParseFileUtilsTest.java
+++ b/parse/src/test/java/com/parse/ParseFileUtilsTest.java
@@ -17,6 +17,7 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
+import java.util.ArrayList;
import org.json.JSONObject;
import org.junit.Rule;
import org.junit.Test;
@@ -101,4 +102,38 @@ public void testWriteJSONObjectToFile() throws Exception {
assertNotNull(json);
assertEquals("bar", json.getString("foo"));
}
+
+ @Test
+ public void testGetAllFilesFromAGivenPath() {
+ ArrayList filesListToSave = new ArrayList<>();
+ File oldRef = new File(temporaryFolder.getRoot() + "/ParseFileUtilsTest/");
+
+ // Writing some files to the `*/ParseFileUtilsTest/*` dir.
+ File config = new File(oldRef + "/config/", "config");
+ filesListToSave.add(config);
+ File installationId = new File(oldRef + "/CommandCache/", "installationId");
+ filesListToSave.add(installationId);
+ File currentConfig = new File(oldRef + "/", "currentConfig");
+ filesListToSave.add(currentConfig);
+ File currentInstallation = new File(oldRef + "/", "currentInstallation");
+ filesListToSave.add(currentInstallation);
+ File pushState = new File(oldRef + "/push/", "pushState");
+ filesListToSave.add(pushState);
+
+ // Write all listed files to the temp (oldRef) directory.
+ for (File item : filesListToSave) {
+ try {
+ ParseFileUtils.writeStringToFile(item, "gger", "UTF-8");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Get all the written files under `*/ParseFileUtilsTest/*`.
+ ArrayList allWrittenFiles = new ArrayList<>();
+ ParseFileUtils.getAllNestedFiles(oldRef.getAbsolutePath(), allWrittenFiles);
+
+ // Check if they both matches or not.
+ assertEquals(filesListToSave.size(), allWrittenFiles.size());
+ }
}