Skip to content

Commit

Permalink
fix: users logged out after SDK upgrade due to different cache path; …
Browse files Browse the repository at this point in the history
…this fixes the bug that was introduced with release 3.0.0 which ignores SDK-internal data that is stored locally on the client side (#1168)
  • Loading branch information
rommansabbir committed May 26, 2022
1 parent 4842329 commit ec7bd03
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 1 deletion.
5 changes: 4 additions & 1 deletion parse/src/main/java/com/parse/Parse.java
Expand Up @@ -76,7 +76,7 @@ private Parse() {
* }
* </pre>
*
* See <a
* <p>See <a
* href="https://github.com/parse-community/Parse-SDK-Android/issues/279">https://github.com/parse-community/Parse-SDK-Android/issues/279</a>
* for a discussion on performance of local datastore, and if it is right for your project.
*
Expand Down Expand Up @@ -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;
Expand Down
133 changes: 133 additions & 0 deletions 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<File> 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);
}
}
20 changes: 20 additions & 0 deletions parse/src/main/java/com/parse/ParseFileUtils.java
Expand Up @@ -16,6 +16,7 @@
*/
package com.parse;

import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
Expand All @@ -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;

Expand Down Expand Up @@ -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<File> 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);
}
}
}

// -----------------------------------------------------------------------

/**
Expand Down
152 changes: 152 additions & 0 deletions 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<File> 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<File> migratedCaches = new ArrayList<>();
ParseFileUtils.getAllNestedFiles(cacheDir.getAbsolutePath(), migratedCaches);

// Check for files file after migration.
File filesDir = InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
ArrayList<File> 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<File> writeSomeMockFiles(Boolean checkForExistingFile) {
ArrayList<File> 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;
}
}
35 changes: 35 additions & 0 deletions parse/src/test/java/com/parse/ParseFileUtilsTest.java
Expand Up @@ -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;
Expand Down Expand Up @@ -101,4 +102,38 @@ public void testWriteJSONObjectToFile() throws Exception {
assertNotNull(json);
assertEquals("bar", json.getString("foo"));
}

@Test
public void testGetAllFilesFromAGivenPath() {
ArrayList<File> 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<File> allWrittenFiles = new ArrayList<>();
ParseFileUtils.getAllNestedFiles(oldRef.getAbsolutePath(), allWrittenFiles);

// Check if they both matches or not.
assertEquals(filesListToSave.size(), allWrittenFiles.size());
}
}

0 comments on commit ec7bd03

Please sign in to comment.