Skip to content

Commit ec7bd03

Browse files
authored
fix: users logged out after SDK upgrade due to different cache path; 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)
1 parent 4842329 commit ec7bd03

File tree

5 files changed

+344
-1
lines changed

5 files changed

+344
-1
lines changed

parse/src/main/java/com/parse/Parse.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ private Parse() {
7676
* }
7777
* </pre>
7878
*
79-
* See <a
79+
* <p>See <a
8080
* href="https://github.com/parse-community/Parse-SDK-Android/issues/279">https://github.com/parse-community/Parse-SDK-Android/issues/279</a>
8181
* for a discussion on performance of local datastore, and if it is right for your project.
8282
*
@@ -145,6 +145,9 @@ static void initialize(Configuration configuration, ParsePlugins parsePlugins) {
145145
PLog.w(TAG, "Parse is already initialized");
146146
return;
147147
}
148+
// Perform old dir migration on initialize.
149+
new ParseCacheDirMigrationUtils(configuration.context).runMigrations();
150+
148151
// NOTE (richardross): We will need this here, as ParsePlugins uses the return value of
149152
// isLocalDataStoreEnabled() to perform additional behavior.
150153
isLocalDatastoreEnabled = configuration.localDataStoreEnabled;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package com.parse;
2+
3+
import android.content.Context;
4+
import java.io.File;
5+
import java.util.ArrayList;
6+
7+
/**
8+
* The {@code ParseMigrationUtils} class perform caching dir migration operation for {@code Parse}
9+
* SDK.
10+
*/
11+
public class ParseCacheDirMigrationUtils {
12+
private final String TAG = this.getClass().getName();
13+
private final Object lock = new Object();
14+
private final Context context;
15+
16+
protected ParseCacheDirMigrationUtils(Context context) {
17+
this.context = context;
18+
}
19+
20+
/*Start old data migrations to new respective locations ("/files/com.parse/", "/cache/com.parse/")*/
21+
protected void runMigrations() {
22+
synchronized (lock) {
23+
runSilentMigration(context);
24+
}
25+
}
26+
27+
private void runSilentMigration(Context context) {
28+
ArrayList<File> filesToBeMigrated = new ArrayList<>();
29+
ParseFileUtils.getAllNestedFiles(
30+
getOldParseDir(context).getAbsolutePath(), filesToBeMigrated);
31+
if (filesToBeMigrated.isEmpty()) {
32+
return;
33+
}
34+
boolean useFilesDir = false;
35+
// Hard coded config file names list.
36+
String[] configNamesList = {
37+
"installationId",
38+
"currentUser",
39+
"currentConfig",
40+
"currentInstallation",
41+
"LocalId",
42+
"pushState"
43+
};
44+
// Start migration for each files in `allFiles`.
45+
for (File itemToMove : filesToBeMigrated) {
46+
try {
47+
for (String configName : configNamesList) {
48+
if (itemToMove.getAbsolutePath().contains(configName)) {
49+
useFilesDir = true;
50+
break;
51+
} else {
52+
useFilesDir = false;
53+
}
54+
}
55+
File fileToSave =
56+
new File(
57+
(useFilesDir ? context.getFilesDir() : context.getCacheDir())
58+
+ "/com.parse/"
59+
+ getFileOldDir(context, itemToMove),
60+
itemToMove.getName());
61+
// Perform copy operation if file doesn't exist in the new directory.
62+
if (!fileToSave.exists()) {
63+
ParseFileUtils.copyFile(itemToMove, fileToSave);
64+
logMigrationStatus(
65+
itemToMove.getName(),
66+
itemToMove.getPath(),
67+
fileToSave.getAbsolutePath(),
68+
"Successful.");
69+
} else {
70+
logMigrationStatus(
71+
itemToMove.getName(),
72+
itemToMove.getPath(),
73+
fileToSave.getAbsolutePath(),
74+
"Already exist in new location.");
75+
}
76+
ParseFileUtils.deleteQuietly(itemToMove);
77+
PLog.v(TAG, "File deleted: " + "{" + itemToMove.getName() + "}" + " successfully");
78+
} catch (Exception e) {
79+
e.printStackTrace();
80+
}
81+
}
82+
// Check again, if all files has been resolved or not. If yes, delete the old dir
83+
// "app_Parse".
84+
filesToBeMigrated.clear();
85+
ParseFileUtils.getAllNestedFiles(
86+
getOldParseDir(context).getAbsolutePath(), filesToBeMigrated);
87+
if (filesToBeMigrated.isEmpty()) {
88+
try {
89+
ParseFileUtils.deleteDirectory(getOldParseDir(context));
90+
} catch (Exception e) {
91+
e.printStackTrace();
92+
}
93+
}
94+
PLog.v(TAG, "Migration completed.");
95+
}
96+
97+
private String getFileOldDir(Context context, File file) {
98+
// Parse the old sub directory name where the file should be moved (new location) by
99+
// following the old sub directory name.
100+
String temp =
101+
file.getAbsolutePath()
102+
.replace(getOldParseDir(context).getAbsolutePath(), "")
103+
.replace("/" + file.getName(), "");
104+
// Before returning the path, replace file name from the last, eg. dir name & file name
105+
// could be same, as we want to get only dir name.
106+
return replaceLast(temp, file.getName());
107+
}
108+
109+
private void logMigrationStatus(
110+
String fileName, String oldPath, String newPath, String status) {
111+
PLog.v(
112+
TAG,
113+
"Migration for file: "
114+
+ "{"
115+
+ fileName
116+
+ "}"
117+
+ " from {"
118+
+ oldPath
119+
+ "} to {"
120+
+ newPath
121+
+ "}, Status: "
122+
+ status);
123+
}
124+
125+
/*Replace a given string from the last*/
126+
private String replaceLast(String text, String regex) {
127+
return text.replaceFirst("(?s)" + regex + "(?!.*?" + regex + ")", "");
128+
}
129+
130+
private File getOldParseDir(Context context) {
131+
return context.getDir("Parse", Context.MODE_PRIVATE);
132+
}
133+
}

parse/src/main/java/com/parse/ParseFileUtils.java

+20
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package com.parse;
1818

19+
import androidx.annotation.NonNull;
1920
import java.io.File;
2021
import java.io.FileInputStream;
2122
import java.io.FileNotFoundException;
@@ -25,6 +26,7 @@
2526
import java.io.OutputStream;
2627
import java.nio.channels.FileChannel;
2728
import java.nio.charset.Charset;
29+
import java.util.List;
2830
import org.json.JSONException;
2931
import org.json.JSONObject;
3032

@@ -347,6 +349,24 @@ private static void doCopyFile(
347349
}
348350
}
349351

352+
/**
353+
* Get all files path from an given directory (including sub-directory).
354+
*
355+
* @param directoryName given directory name.
356+
* @param files where all the files will be stored.
357+
*/
358+
public static void getAllNestedFiles(@NonNull String directoryName, @NonNull List<File> files) {
359+
File[] directoryItems = new File(directoryName).listFiles();
360+
if (directoryItems != null)
361+
for (File item : directoryItems) {
362+
if (item.isFile()) {
363+
files.add(item);
364+
} else if (item.isDirectory()) {
365+
getAllNestedFiles(item.getAbsolutePath(), files);
366+
}
367+
}
368+
}
369+
350370
// -----------------------------------------------------------------------
351371

352372
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package com.parse;
2+
3+
import android.content.Context;
4+
import androidx.test.platform.app.InstrumentationRegistry;
5+
import java.io.File;
6+
import java.util.ArrayList;
7+
import org.junit.After;
8+
import org.junit.Before;
9+
import org.junit.Test;
10+
import org.junit.runner.RunWith;
11+
import org.robolectric.RobolectricTestRunner;
12+
13+
@RunWith(RobolectricTestRunner.class)
14+
public class ParseCacheDirMigrationUtilsTest {
15+
ArrayList<File> writtenFiles = new ArrayList<>();
16+
private ParseCacheDirMigrationUtils utils;
17+
18+
@Before
19+
public void setUp() throws Exception {
20+
utils =
21+
new ParseCacheDirMigrationUtils(
22+
InstrumentationRegistry.getInstrumentation().getContext());
23+
writtenFiles.clear();
24+
}
25+
26+
@After
27+
public void tearDown() throws Exception {
28+
writtenFiles.clear();
29+
}
30+
31+
@Test
32+
public void testMigrationOnParseSDKInitialization() {
33+
prepareForMockFilesWriting();
34+
writtenFiles.addAll(writeSomeMockFiles(true));
35+
Parse.Configuration configuration =
36+
new Parse.Configuration.Builder(
37+
InstrumentationRegistry.getInstrumentation().getContext())
38+
.applicationId(BuildConfig.LIBRARY_PACKAGE_NAME)
39+
.server("https://api.parse.com/1")
40+
.enableLocalDataStore()
41+
.build();
42+
Parse.initialize(configuration);
43+
}
44+
45+
@Test
46+
public void testMockMigration() {
47+
prepareForMockFilesWriting();
48+
writtenFiles.addAll(writeSomeMockFiles(true));
49+
50+
// Run migration.
51+
utils.runMigrations();
52+
53+
// Check for cache file after migration.
54+
File cacheDir = InstrumentationRegistry.getInstrumentation().getContext().getCacheDir();
55+
ArrayList<File> migratedCaches = new ArrayList<>();
56+
ParseFileUtils.getAllNestedFiles(cacheDir.getAbsolutePath(), migratedCaches);
57+
58+
// Check for files file after migration.
59+
File filesDir = InstrumentationRegistry.getInstrumentation().getContext().getFilesDir();
60+
ArrayList<File> migratedFiles = new ArrayList<>();
61+
ParseFileUtils.getAllNestedFiles(filesDir.getAbsolutePath(), migratedFiles);
62+
63+
// To check migrations result
64+
int sizeAfterMigration = (migratedCaches.size() + migratedFiles.size());
65+
int sizeBeforeMigrations = writtenFiles.size();
66+
67+
assert (cacheDir.exists() && !migratedCaches.isEmpty());
68+
assert (filesDir.exists() && !migratedFiles.isEmpty());
69+
assert sizeBeforeMigrations == sizeAfterMigration;
70+
}
71+
72+
private void prepareForMockFilesWriting() {
73+
// Delete `"app_Parse"` dir including nested dir and files.
74+
try {
75+
ParseFileUtils.deleteDirectory(
76+
InstrumentationRegistry.getInstrumentation()
77+
.getContext()
78+
.getDir("Parse", Context.MODE_PRIVATE));
79+
} catch (Exception e) {
80+
e.printStackTrace();
81+
}
82+
writtenFiles.clear();
83+
// Create new `"app_Parse"` dir to write some files.
84+
createFileDir(InstrumentationRegistry.getInstrumentation().getContext().getCacheDir());
85+
}
86+
87+
private ArrayList<File> writeSomeMockFiles(Boolean checkForExistingFile) {
88+
ArrayList<File> fileToReturn = new ArrayList<>();
89+
File oldRef =
90+
InstrumentationRegistry.getInstrumentation()
91+
.getContext()
92+
.getDir("Parse", Context.MODE_PRIVATE);
93+
94+
// Writing some config & random files for migration process.
95+
File config = new File(oldRef + "/config/", "config");
96+
fileToReturn.add(config);
97+
File installationId = new File(oldRef + "/CommandCache/", "installationId");
98+
fileToReturn.add(installationId);
99+
File currentConfig = new File(oldRef + "/", "currentConfig");
100+
fileToReturn.add(currentConfig);
101+
File currentInstallation = new File(oldRef + "/", "currentInstallation");
102+
fileToReturn.add(currentInstallation);
103+
File pushState = new File(oldRef + "/push/", "pushState");
104+
fileToReturn.add(pushState);
105+
File localId = new File(oldRef + "/LocalId/", "LocalId");
106+
fileToReturn.add(localId);
107+
File cache = new File(oldRef + "/testcache/", "cache");
108+
fileToReturn.add(cache);
109+
File cache1 = new File(oldRef + "/testcache/", "cache1");
110+
fileToReturn.add(cache1);
111+
File cache2 = new File(oldRef + "/testcache/another/", "cache4");
112+
fileToReturn.add(cache2);
113+
File user = new File(oldRef + "/user/", "user_config");
114+
fileToReturn.add(user);
115+
116+
// Write all listed files to the app cache ("app_Parse") directory.
117+
for (File item : fileToReturn) {
118+
try {
119+
ParseFileUtils.writeStringToFile(item, "gger", "UTF-8");
120+
} catch (Exception e) {
121+
e.printStackTrace();
122+
}
123+
}
124+
// To create a file conflict scenario during migration by creating an existing file to the
125+
// new files dir ("*/files/com.parse/*").
126+
if (checkForExistingFile) {
127+
try {
128+
ParseFileUtils.writeStringToFile(
129+
new File(
130+
InstrumentationRegistry.getInstrumentation()
131+
.getContext()
132+
.getFilesDir()
133+
+ "/com.parse/CommandCache/",
134+
"installationId"),
135+
"gger",
136+
"UTF-8");
137+
} catch (Exception e) {
138+
e.printStackTrace();
139+
}
140+
}
141+
return fileToReturn;
142+
}
143+
144+
private File createFileDir(File file) {
145+
if (!file.exists()) {
146+
if (!file.mkdirs()) {
147+
return file;
148+
}
149+
}
150+
return file;
151+
}
152+
}

parse/src/test/java/com/parse/ParseFileUtilsTest.java

+35
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.io.FileInputStream;
1818
import java.io.FileOutputStream;
1919
import java.io.InputStream;
20+
import java.util.ArrayList;
2021
import org.json.JSONObject;
2122
import org.junit.Rule;
2223
import org.junit.Test;
@@ -101,4 +102,38 @@ public void testWriteJSONObjectToFile() throws Exception {
101102
assertNotNull(json);
102103
assertEquals("bar", json.getString("foo"));
103104
}
105+
106+
@Test
107+
public void testGetAllFilesFromAGivenPath() {
108+
ArrayList<File> filesListToSave = new ArrayList<>();
109+
File oldRef = new File(temporaryFolder.getRoot() + "/ParseFileUtilsTest/");
110+
111+
// Writing some files to the `*/ParseFileUtilsTest/*` dir.
112+
File config = new File(oldRef + "/config/", "config");
113+
filesListToSave.add(config);
114+
File installationId = new File(oldRef + "/CommandCache/", "installationId");
115+
filesListToSave.add(installationId);
116+
File currentConfig = new File(oldRef + "/", "currentConfig");
117+
filesListToSave.add(currentConfig);
118+
File currentInstallation = new File(oldRef + "/", "currentInstallation");
119+
filesListToSave.add(currentInstallation);
120+
File pushState = new File(oldRef + "/push/", "pushState");
121+
filesListToSave.add(pushState);
122+
123+
// Write all listed files to the temp (oldRef) directory.
124+
for (File item : filesListToSave) {
125+
try {
126+
ParseFileUtils.writeStringToFile(item, "gger", "UTF-8");
127+
} catch (Exception e) {
128+
e.printStackTrace();
129+
}
130+
}
131+
132+
// Get all the written files under `*/ParseFileUtilsTest/*`.
133+
ArrayList<File> allWrittenFiles = new ArrayList<>();
134+
ParseFileUtils.getAllNestedFiles(oldRef.getAbsolutePath(), allWrittenFiles);
135+
136+
// Check if they both matches or not.
137+
assertEquals(filesListToSave.size(), allWrittenFiles.size());
138+
}
104139
}

0 commit comments

Comments
 (0)