finished google drive conflict resolving algorithm
This commit is contained in:
parent
e33ac9d70d
commit
ce8baef9e5
|
@ -82,19 +82,22 @@ internal class GoogleDriveDraftsSyncAction(
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun listRemoteDrafts(): List<DriveFileInfo> {
|
override fun listRemoteDrafts(): List<DriveFileInfo> {
|
||||||
val result = ArrayList<DriveFileInfo>()
|
val result = ArrayList<DriveFileInfo>()
|
||||||
var pageToken: String?
|
var nextPageToken: String? = null
|
||||||
do {
|
do {
|
||||||
val listResult = files.list().apply {
|
val listResult = files.list().apply {
|
||||||
fields = "files($requiredRequestFields)"
|
this.fields = requiredFilesRequestFields
|
||||||
q = "'$folderId' in parents and mimeType = '$draftMimeType' and trashed = false"
|
this.q = "'$folderId' in parents and mimeType = '$draftMimeType' and trashed = false"
|
||||||
|
if (nextPageToken != null) {
|
||||||
|
this.pageToken = nextPageToken
|
||||||
|
}
|
||||||
}.execute()
|
}.execute()
|
||||||
listResult.files.filter { file ->
|
listResult.files.filter { file ->
|
||||||
file.mimeType == draftMimeType
|
file.mimeType == draftMimeType
|
||||||
}.mapTo(result) { file ->
|
}.mapTo(result) { file ->
|
||||||
DriveFileInfo(file.id, file.name, Date(file.modifiedTime.value))
|
DriveFileInfo(file.id, file.name, Date(file.modifiedTime.value))
|
||||||
}
|
}
|
||||||
pageToken = listResult.nextPageToken
|
nextPageToken = listResult.nextPageToken
|
||||||
} while (pageToken != null)
|
} while (nextPageToken != null)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
package org.mariotaku.twidere.util.sync.google
|
package org.mariotaku.twidere.util.sync.google
|
||||||
|
|
||||||
|
import com.google.api.client.googleapis.json.GoogleJsonError
|
||||||
import com.google.api.client.googleapis.json.GoogleJsonResponseException
|
import com.google.api.client.googleapis.json.GoogleJsonResponseException
|
||||||
|
import com.google.api.client.http.HttpHeaders
|
||||||
import com.google.api.client.http.InputStreamContent
|
import com.google.api.client.http.InputStreamContent
|
||||||
import com.google.api.services.drive.Drive
|
import com.google.api.services.drive.Drive
|
||||||
import com.google.api.services.drive.model.File
|
import com.google.api.services.drive.model.File
|
||||||
|
import com.google.common.collect.HashMultimap
|
||||||
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,7 +19,8 @@ import java.io.InputStream
|
||||||
|
|
||||||
internal const val folderMimeType = "application/vnd.google-apps.folder"
|
internal const val folderMimeType = "application/vnd.google-apps.folder"
|
||||||
internal const val xmlMimeType = "application/xml"
|
internal const val xmlMimeType = "application/xml"
|
||||||
internal const val requiredRequestFields = "id, name, mimeType, modifiedTime"
|
internal const val requiredRequestFields = "id, name, parents, mimeType, modifiedTime"
|
||||||
|
internal const val requiredFilesRequestFields = "files($requiredRequestFields)"
|
||||||
|
|
||||||
internal fun Drive.getFileOrNull(
|
internal fun Drive.getFileOrNull(
|
||||||
name: String,
|
name: String,
|
||||||
|
@ -46,9 +52,11 @@ internal fun Drive.findFilesOrNull(
|
||||||
}
|
}
|
||||||
query += " and trashed = $trashed"
|
query += " and trashed = $trashed"
|
||||||
find.q = query
|
find.q = query
|
||||||
find.fields = "files($requiredRequestFields)"
|
find.fields = requiredFilesRequestFields
|
||||||
try {
|
try {
|
||||||
return find.execute().files
|
val files = find.execute().files
|
||||||
|
if (files.isEmpty()) return null
|
||||||
|
return files
|
||||||
} catch (e: GoogleJsonResponseException) {
|
} catch (e: GoogleJsonResponseException) {
|
||||||
if (e.statusCode == 404) {
|
if (e.statusCode == 404) {
|
||||||
return null
|
return null
|
||||||
|
@ -100,6 +108,7 @@ internal fun Drive.updateOrCreate(
|
||||||
return run {
|
return run {
|
||||||
val find = files.list()
|
val find = files.list()
|
||||||
find.q = "name = '$name' and '$parent' in parents and mimeType = '$mimeType' and trashed = $trashed"
|
find.q = "name = '$name' and '$parent' in parents and mimeType = '$mimeType' and trashed = $trashed"
|
||||||
|
find.fields = requiredFilesRequestFields
|
||||||
val fileId = try {
|
val fileId = try {
|
||||||
find.execute().files.firstOrNull()?.id ?: return@run null
|
find.execute().files.firstOrNull()?.id ?: return@run null
|
||||||
} catch (e: GoogleJsonResponseException) {
|
} catch (e: GoogleJsonResponseException) {
|
||||||
|
@ -117,6 +126,7 @@ internal fun Drive.updateOrCreate(
|
||||||
file.parents = listOf(parent)
|
file.parents = listOf(parent)
|
||||||
fileConfig?.invoke(file)
|
fileConfig?.invoke(file)
|
||||||
val create = files.create(file, InputStreamContent(mimeType, stream))
|
val create = files.create(file, InputStreamContent(mimeType, stream))
|
||||||
|
create.fields = requiredRequestFields
|
||||||
return@run create.execute()
|
return@run create.execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +148,7 @@ internal fun Drive.Files.performUpdate(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun resolveFilesConflict(client: Drive, list: List<File>): File {
|
internal fun resolveFilesConflict(client: Drive, list: List<File>): File {
|
||||||
// Use newest file
|
// Pick newest file
|
||||||
val newest = list.maxBy { it.modifiedTime.value }!!
|
val newest = list.maxBy { it.modifiedTime.value }!!
|
||||||
|
|
||||||
// Delete all others
|
// Delete all others
|
||||||
|
@ -151,5 +161,85 @@ internal fun resolveFilesConflict(client: Drive, list: List<File>): File {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun resolveFoldersConflict(client: Drive, list: List<File>): File {
|
internal fun resolveFoldersConflict(client: Drive, list: List<File>): File {
|
||||||
return list.first()
|
val files = client.files()
|
||||||
|
|
||||||
|
// Pick newest folder
|
||||||
|
val newest = list.maxBy { it.modifiedTime.value }!!
|
||||||
|
|
||||||
|
// Build a map with all conflicting folders
|
||||||
|
val query = list.joinToString(" or ") { "'${it.id}' in parents" }
|
||||||
|
val filesList = ArrayList<File>()
|
||||||
|
|
||||||
|
val conflictFilesMap = HashMultimap.create<String, File>()
|
||||||
|
var nextPageToken: String? = null
|
||||||
|
do {
|
||||||
|
val result = files.list().apply {
|
||||||
|
this.q = query
|
||||||
|
this.fields = requiredFilesRequestFields
|
||||||
|
if (nextPageToken != null) {
|
||||||
|
this.pageToken = nextPageToken
|
||||||
|
}
|
||||||
|
}.execute()
|
||||||
|
result.files.forEach { file ->
|
||||||
|
file.parents.forEach { parentId ->
|
||||||
|
if (parentId == newest.id) {
|
||||||
|
filesList.add(file)
|
||||||
|
} else {
|
||||||
|
conflictFilesMap.put(parentId, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextPageToken = result.nextPageToken
|
||||||
|
} while (nextPageToken != null)
|
||||||
|
|
||||||
|
// Files in this list will be moved to newest folder
|
||||||
|
val insertList = ArrayList<File>()
|
||||||
|
// Files in this list will be removed
|
||||||
|
val removeList = ArrayList<File>()
|
||||||
|
|
||||||
|
for ((k, l) in conflictFilesMap.asMap()) {
|
||||||
|
for (v in l) {
|
||||||
|
val find = filesList.find { it.name == v.name }
|
||||||
|
if (find == null) {
|
||||||
|
insertList.add(v)
|
||||||
|
} else if (find.modifiedTime.value > v.modifiedTime.value) {
|
||||||
|
// Our file is newer, remove `v`
|
||||||
|
removeList.add(v)
|
||||||
|
} else {
|
||||||
|
// `v` is newer, update ours
|
||||||
|
insertList.add(v)
|
||||||
|
removeList.add(find)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.filterNotTo(removeList) { it == newest }
|
||||||
|
|
||||||
|
if (insertList.isNotEmpty()) {
|
||||||
|
val callback = object : SimpleJsonBatchCallback<File>() {
|
||||||
|
override fun onFailure(error: GoogleJsonError, headers: HttpHeaders) {
|
||||||
|
throw IOException(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.batch().apply {
|
||||||
|
insertList.forEach { file ->
|
||||||
|
files.update(file.id, File()).apply {
|
||||||
|
this.addParents = newest.id
|
||||||
|
this.removeParents = file.parents?.joinToString(",")
|
||||||
|
}.queue(this, callback)
|
||||||
|
}
|
||||||
|
}.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeList.isNotEmpty()) {
|
||||||
|
val callback = SimpleJsonBatchCallback<Void>()
|
||||||
|
client.batch().apply {
|
||||||
|
removeList.forEach { file ->
|
||||||
|
files.delete(file.id).queue(this, callback)
|
||||||
|
}
|
||||||
|
}.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
return newest
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,7 +2,9 @@ package org.mariotaku.twidere.util.sync.google
|
||||||
|
|
||||||
import com.google.api.client.googleapis.batch.json.JsonBatchCallback
|
import com.google.api.client.googleapis.batch.json.JsonBatchCallback
|
||||||
import com.google.api.client.googleapis.json.GoogleJsonError
|
import com.google.api.client.googleapis.json.GoogleJsonError
|
||||||
|
import com.google.api.client.googleapis.json.GoogleJsonResponseException
|
||||||
import com.google.api.client.http.HttpHeaders
|
import com.google.api.client.http.HttpHeaders
|
||||||
|
import com.google.api.client.http.HttpResponseException
|
||||||
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
|
@ -10,14 +12,12 @@ import java.io.IOException
|
||||||
* Created by mariotaku on 1/22/17.
|
* Created by mariotaku on 1/22/17.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
internal class SimpleJsonBatchCallback<T> : JsonBatchCallback<T>() {
|
internal open class SimpleJsonBatchCallback<T> : JsonBatchCallback<T>() {
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun onFailure(error: GoogleJsonError, headers: HttpHeaders) {
|
override fun onFailure(error: GoogleJsonError, headers: HttpHeaders) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun onSuccess(result: T, headers: HttpHeaders) {
|
override fun onSuccess(result: T, headers: HttpHeaders) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,8 +39,10 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static android.text.TextUtils.isEmpty;
|
import static android.text.TextUtils.isEmpty;
|
||||||
|
import static org.mariotaku.twidere.TwidereConstants.USER_COLOR_PREFERENCES_NAME;
|
||||||
|
import static org.mariotaku.twidere.TwidereConstants.USER_NICKNAME_PREFERENCES_NAME;
|
||||||
|
|
||||||
public class UserColorNameManager implements TwidereConstants {
|
public class UserColorNameManager {
|
||||||
|
|
||||||
private final static String NICKNAME_NULL = ".#NULL#";
|
private final static String NICKNAME_NULL = ".#NULL#";
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue