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()); + } }