Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ android {
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20240303'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'com.google.android.material:material:1.12.0'
Expand Down
18 changes: 10 additions & 8 deletions app/src/main/java/net/micode/notes/sync/webdav/WebDavClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import java.nio.charset.StandardCharsets;
import java.util.Locale;

class WebDavClient {
class WebDavClient implements WebDavSyncManager.SnapshotTransport {
static final int ERROR_INVALID_URL = 0;

static final int ERROR_AUTH = 1;
Expand Down Expand Up @@ -102,12 +102,14 @@ private String getSnapshot(boolean allowMissingSnapshot) throws IOException {
}
}

void putSnapshot(String snapshot) throws IOException {
@Override
public void putSnapshot(String snapshot) throws IOException {
putJson(getSnapshotUrl(), snapshot);
}

void putBackupSnapshot(String snapshot, long timestamp) throws IOException {
putJson(getBackupUrl(timestamp), snapshot);
@Override
public void putBackupSnapshot(String snapshot) throws IOException {
putJson(getBackupUrl(), snapshot);
}

private void putJson(URL url, String snapshot) throws IOException {
Expand Down Expand Up @@ -163,8 +165,8 @@ private URL getSnapshotUrl() throws IOException {
return resolveSnapshotUrl(mUrl);
}

private URL getBackupUrl(long timestamp) throws IOException {
return resolveBackupUrl(mUrl, timestamp);
private URL getBackupUrl() throws IOException {
return resolveBackupUrl(mUrl);
}

static URL resolveSnapshotUrl(String url) throws IOException {
Expand All @@ -175,9 +177,9 @@ static URL resolveSnapshotUrl(String url) throws IOException {
return toUrl(rebuildUri(uri, appendPathSegment(uri.getRawPath(), SNAPSHOT_FILE_NAME)));
}

static URL resolveBackupUrl(String url, long timestamp) throws IOException {
static URL resolveBackupUrl(String url) throws IOException {
URI uri = parseWebDavUri(url);
String backupSuffix = ".backup-" + timestamp + ".json";
String backupSuffix = ".backup.json";
if (isDirectJsonUrl(uri)) {
String rawPath = getRawPath(uri);
int slashIndex = rawPath.lastIndexOf('/');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public class WebDavSyncManager {

public static final int STATE_REMOTE_NOT_FOUND = 10;

public static final int STATE_BACKUP_ERROR = 11;

private static final String JSON_VERSION = "version";

private static final String JSON_GENERATED_AT = "generated_at";
Expand Down Expand Up @@ -165,6 +167,12 @@ public synchronized void cancelSync() {
mCancelled = true;
}

interface SnapshotTransport {
void putSnapshot(String snapshot) throws IOException;

void putBackupSnapshot(String snapshot) throws IOException;
}

public int sync(Context context, WebDavSyncTask asyncTask) {
synchronized (this) {
if (mSyncing) {
Expand Down Expand Up @@ -194,11 +202,7 @@ public int sync(Context context, WebDavSyncTask asyncTask) {
return STATE_SYNC_CANCELLED;
}

JSONObject remoteSnapshot = null;
if (!TextUtils.isEmpty(remotePayload)) {
remoteSnapshot = new JSONObject(remotePayload);
validateSnapshot(remoteSnapshot);
}
JSONObject remoteSnapshot = parseRemoteSnapshot(remotePayload);

boolean localChanged = hasLocalChanges(context);
long lastSyncTime = NotesPreferenceActivity.getLastSyncTime(context);
Expand All @@ -218,14 +222,13 @@ public int sync(Context context, WebDavSyncTask asyncTask) {
context.getString(R.string.sync_result_downloaded_remote));
} else {
asyncTask.publishProgressMessage(context.getString(R.string.sync_progress_webdav_uploading));
backupRemoteSnapshot(client, remotePayload);
JSONObject localSnapshot = exportSnapshot(context);
if (mCancelled) {
NotesPreferenceActivity.setLastSyncResult(context, STATE_SYNC_CANCELLED,
context.getString(R.string.sync_result_cancelled));
return STATE_SYNC_CANCELLED;
}
client.putSnapshot(localSnapshot.toString());
uploadSnapshotSafely(client, remotePayload, localSnapshot.toString());
cleanupTrash(context);
resetLocalModified(context);
int messageResId = remoteSnapshot != null && remoteGeneratedAt > lastSyncTime
Expand All @@ -242,6 +245,11 @@ public int sync(Context context, WebDavSyncTask asyncTask) {
}
NotesPreferenceActivity.setLastSyncTime(context, System.currentTimeMillis());
return STATE_SUCCESS;
} catch (SnapshotBackupException e) {
Log.e(TAG, "WebDAV backup error", e);
NotesPreferenceActivity.setLastSyncResult(context, STATE_BACKUP_ERROR,
context.getString(R.string.sync_result_backup_error));
return STATE_BACKUP_ERROR;
} catch (WebDavClient.WebDavException e) {
Log.e(TAG, "WebDAV protocol error", e);
int state = mapWebDavError(e);
Expand Down Expand Up @@ -324,6 +332,8 @@ public static int getResultMessageResId(int state) {
return R.string.sync_result_invalid_url;
case STATE_REMOTE_NOT_FOUND:
return R.string.sync_result_remote_not_found;
case STATE_BACKUP_ERROR:
return R.string.sync_result_backup_error;
default:
return R.string.sync_result_internal_error;
}
Expand All @@ -345,17 +355,57 @@ private int mapWebDavError(WebDavClient.WebDavException e) {
}
}

private void backupRemoteSnapshot(WebDavClient client, String remotePayload)
private void backupLocalSnapshot(Context context, WebDavClient client)
throws IOException, JSONException {
backupSnapshotSafely(client, exportSnapshot(context, true).toString());
}

static JSONObject parseRemoteSnapshot(String remotePayload) throws JSONException {
if (isEmpty(remotePayload)) {
return null;
}
JSONObject snapshot = new JSONObject(remotePayload);
validateSnapshot(snapshot);
return snapshot;
}

static void uploadSnapshotSafely(SnapshotTransport client, String previousSnapshot,
String newSnapshot) throws IOException {
backupSnapshotSafely(client, previousSnapshot);
try {
client.putSnapshot(newSnapshot);
} catch (IOException e) {
restoreRemoteSnapshot(client, previousSnapshot, e);
throw e;
}
}

static void backupSnapshotSafely(SnapshotTransport client, String snapshot)
throws IOException {
if (!TextUtils.isEmpty(remotePayload)) {
client.putBackupSnapshot(remotePayload, System.currentTimeMillis());
if (isEmpty(snapshot)) {
return;
}
try {
client.putBackupSnapshot(snapshot);
} catch (IOException e) {
throw new SnapshotBackupException(e);
}
Comment on lines +383 to 392
}

private void backupLocalSnapshot(Context context, WebDavClient client)
throws IOException, JSONException {
client.putBackupSnapshot(exportSnapshot(context, true).toString(),
System.currentTimeMillis());
private static void restoreRemoteSnapshot(SnapshotTransport client, String previousSnapshot,
IOException uploadError) {
if (isEmpty(previousSnapshot)) {
return;
}
try {
client.putSnapshot(previousSnapshot);
} catch (IOException restoreError) {
uploadError.addSuppressed(restoreError);
}
}

private static boolean isEmpty(String value) {
return value == null || value.length() == 0;
}

private boolean hasLocalChanges(Context context) {
Expand All @@ -374,7 +424,7 @@ private boolean hasLocalChanges(Context context) {
}
}

private void validateSnapshot(JSONObject snapshot) throws JSONException {
private static void validateSnapshot(JSONObject snapshot) throws JSONException {
if (snapshot.optInt(JSON_VERSION, -1) != 1 || !snapshot.has(JSON_NOTES)
|| !snapshot.has(JSON_DATA)) {
throw new JSONException("Invalid WebDAV snapshot");
Expand Down Expand Up @@ -599,4 +649,10 @@ private ContentValues dataToValues(JSONObject data) {
values.put(DataColumns.DATA5, data.optString(DataColumns.DATA5, ""));
return values;
}

static class SnapshotBackupException extends IOException {
SnapshotBackupException(IOException cause) {
super(cause);
}
}
}
1 change: 1 addition & 0 deletions app/src/main/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
<string name="sync_result_auth_error">WebDAV 同步失败:请检查用户名和密码</string>
<string name="sync_result_path_error">WebDAV 同步失败:请检查 WebDAV 路径和权限</string>
<string name="sync_result_remote_not_found">未找到 WebDAV 快照。请同步一次以创建 mi-notes-sync.json,或检查路径。</string>
<string name="sync_result_backup_error">WebDAV 同步失败:无法创建备份,现有数据未被更改</string>
Comment on lines 110 to +113
<!-- Preferences -->
<string name="preferences_title">设置</string>
<string name="preferences_account_title">同步账号</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
<string name="sync_result_auth_error">WebDAV 同步失敗:請檢查使用者名稱和密碼</string>
<string name="sync_result_path_error">WebDAV 同步失敗:請檢查 WebDAV 路徑和權限</string>
<string name="sync_result_remote_not_found">未找到 WebDAV 快照。請同步一次以建立 mi-notes-sync.json,或檢查路徑。</string>
<string name="sync_result_backup_error">WebDAV 同步失敗:無法建立備份,現有資料未被變更</string>
Comment on lines 109 to +112
<!-- Preferences -->
<string name="preferences_title">設置</string>
<string name="preferences_account_title">同步賬號</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
<string name="sync_result_auth_error">WebDAV sync failed: check username and password</string>
<string name="sync_result_path_error">WebDAV sync failed: check WebDAV path and permissions</string>
<string name="sync_result_remote_not_found">WebDAV snapshot was not found. Sync once to create mi-notes-sync.json, or check the path.</string>
<string name="sync_result_backup_error">WebDAV sync failed: backup could not be created, so existing data was not changed</string>
Comment on lines 114 to +117
<!-- Preferences -->
<string name="preferences_title">Settings</string>
<string name="preferences_account_title">Sync account</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,33 +73,32 @@ public void resolveSnapshotUrl_preservesAlreadyEncodedDirectFileName() throws Ex

@Test
public void resolveBackupUrl_usesDeterministicFolderBackupName() throws Exception {
URL url = WebDavClient.resolveBackupUrl("https://example.com/dav/笔记", 42);
URL url = WebDavClient.resolveBackupUrl("https://example.com/dav/笔记");

assertEquals("https://example.com/dav/%E7%AC%94%E8%AE%B0/"
+ "mi-notes-sync.backup-42.json",
+ "mi-notes-sync.backup.json",
url.toExternalForm());
}

@Test
public void resolveBackupUrl_preservesChineseDirectFileBaseName() throws Exception {
URL url = WebDavClient.resolveBackupUrl(
"https://example.com/dav/小米便签同步.json", 42);
"https://example.com/dav/小米便签同步.json");

assertEquals("https://example.com/dav/"
+ "%E5%B0%8F%E7%B1%B3%E4%BE%BF%E7%AD%BE%E5%90%8C%E6%AD%A5"
+ ".backup-42.json",
+ ".backup.json",
url.toExternalForm());
}

@Test
public void resolveBackupUrl_preservesAlreadyEncodedDirectFileBaseName() throws Exception {
URL url = WebDavClient.resolveBackupUrl("https://example.com/dav/"
+ "%E5%B0%8F%E7%B1%B3%E4%BE%BF%E7%AD%BE%E5%90%8C%E6%AD%A5.json",
42);
+ "%E5%B0%8F%E7%B1%B3%E4%BE%BF%E7%AD%BE%E5%90%8C%E6%AD%A5.json");

assertEquals("https://example.com/dav/"
+ "%E5%B0%8F%E7%B1%B3%E4%BE%BF%E7%AD%BE%E5%90%8C%E6%AD%A5"
+ ".backup-42.json",
+ ".backup.json",
url.toExternalForm());
assertFalse(url.toExternalForm().contains("%25E5"));
}
Expand Down
Loading