Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: users logged out after SDK upgrade due to different cache path #1168

Merged
merged 10 commits into from
May 26, 2022
5 changes: 4 additions & 1 deletion parse/src/main/java/com/parse/Parse.java
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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());
}
}