Loading app/build.gradle +4 −0 Original line number Diff line number Diff line Loading @@ -69,6 +69,10 @@ android { } } kotlinOptions { jvmTarget = "11" } testOptions { unitTests { returnDefaultValues = true Loading app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java→app/src/main/java/foundation/e/drive/contentScanner/FileDiffUtils.kt +116 −0 Original line number Diff line number Diff line /* * Copyright © MURENA SAS 2022-2023. * Copyright © MURENA SAS 2023. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html */ package foundation.e.drive.contentScanner package foundation.e.drive.utils; import androidx.annotation.NonNull; import com.owncloud.android.lib.resources.files.model.RemoteFile; import java.io.File; import foundation.e.drive.models.SyncedFileState; /** * This class encapsulate code to compare syncedFile & Remote file * but also RemoteFolder and SyncedFolder * @author vincent Bourgmayer */ public class FileDiffUtils { public enum Action { Upload, Download, skip, updateDB } import androidx.annotation.VisibleForTesting import com.owncloud.android.lib.resources.files.model.RemoteFile import foundation.e.drive.models.SyncedFileState import foundation.e.drive.utils.AppConstants import java.io.File object FileDiffUtils { enum class Action { Upload, Download, Skip, UpdateDB } /** * Define what to do of RemoteFile for which we know the Database equivalent * @param remoteFile RemoteFile * @param fileState SyncedFileState instance * @return Action from Enum */ @NonNull public static Action getActionForFileDiff(@NonNull RemoteFile remoteFile, @NonNull SyncedFileState fileState) { if (hasAlreadyBeenDownloaded(fileState) && !hasEtagChanged(remoteFile, fileState)) { return Action.skip; } final File localFile = new File(fileState.getLocalPath()); @JvmStatic fun getActionForFileDiff(remoteFile: RemoteFile, fileState: SyncedFileState): Action { if (!hasEtagChanged(remoteFile, fileState)) { if (isCorruptedTimestamp(remoteFile.modifiedTimestamp)) return Action.Upload if (isRemoteSizeSameAsLocalSize(remoteFile, localFile)) { return Action.updateDB; if (hasAlreadyBeenDownloaded(fileState)) return Action.Skip } return Action.Download; val localFileSize = getLocalFileSize(fileState.localPath) if (isRemoteAndLocalSizeEqual(remoteFile.size, localFileSize)) { return Action.UpdateDB } return Action.Download } /** * Define what to do of local file for which we know the Database equivalent Loading @@ -56,24 +41,25 @@ public class FileDiffUtils { * @param fileState SyncedFileState instance. Containing data from Database * @return Action from Enum */ @NonNull public static Action getActionForFileDiff(@NonNull File localFile, @NonNull SyncedFileState fileState) { //If no etag is stored in sfs, the file hasn't been sync up to server. then do upload if (fileState.getLastModified() < localFile.lastModified() || !fileState.isLastEtagStored()) { return Action.Upload; } return Action.skip; @JvmStatic fun getActionForFileDiff(localFile: File, fileState: SyncedFileState): Action { val isFileMoreRecentThanDB = fileState.lastModified < localFile.lastModified() return if (isFileMoreRecentThanDB || !fileState.isLastEtagStored()) { Action.Upload } else Action.Skip } /** * Compare RemoteFile's eTag with the one stored in Database * @param file RemoteFile * @param remoteFile RemoteFile * @param fileState last store file's state * @return true if ETag */ private static boolean hasEtagChanged(@NonNull RemoteFile file, @NonNull SyncedFileState fileState) { //if SyncedFileState has no Etag then it hasn't been uploaded and so must not exist on server return fileState.isLastEtagStored() && !file.getEtag().equals(fileState.getLastEtag()); @VisibleForTesting @JvmStatic fun hasEtagChanged(remoteFile: RemoteFile, fileState: SyncedFileState): Boolean { return fileState.isLastEtagStored() && remoteFile.etag != fileState.lastEtag } /** Loading @@ -82,19 +68,49 @@ public class FileDiffUtils { * @param fileState SyncedFileState containing data from Database * @return true if localLastModified store in Database == 0 */ private static boolean hasAlreadyBeenDownloaded(@NonNull SyncedFileState fileState) { return fileState.getLastModified() > 0L; @VisibleForTesting @JvmStatic fun hasAlreadyBeenDownloaded(fileState: SyncedFileState): Boolean { return fileState.lastModified > 0 } /** * * @param remoteFile RemoteFile instance * @param localFile File instance * Compare file size for remote file & local file * @param remoteFileSize RemoteFile.size. Note: it's equal to getLength() except for * folder where it also sum size of its content * @param localFileSize File.length() * @return true if remote file size is same as local file size */ private static boolean isRemoteSizeSameAsLocalSize(@NonNull RemoteFile remoteFile, @NonNull File localFile) { // if local file doesn't exist its size will be 0 // remoteFile.getSize() : getSize() is equal to getLength() except that for folder is also sum the content of the folder! return remoteFile.getSize() == localFile.length(); @VisibleForTesting @JvmStatic fun isRemoteAndLocalSizeEqual(remoteFileSize: Long, localFileSize: Long): Boolean { return remoteFileSize == localFileSize } /** * Check if timestamp is equal to max of unsigned int 32 * * For yet unknown reason, some remote files have this value on cloud (DB & file system) * the only way to fix them is to force re upload of the file with correct value * @param timestampInSecond remote file timestamp (Long) * @return true if the timestamp is equal to max of unsigned int 32 */ @VisibleForTesting @JvmStatic fun isCorruptedTimestamp(timestampInSecond: Long): Boolean { return timestampInSecond >= AppConstants.CORRUPTED_TIMESTAMP_IN_SECOND } /** * Fetch file size on device * @param path Path of the file * @return 0 if path is empty or file size */ @VisibleForTesting @JvmStatic fun getLocalFileSize(path: String): Long { if (path.isEmpty()) return 0 return File(path).length() } } No newline at end of file app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java +0 −1 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import foundation.e.drive.database.DbHelper; import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.utils.FileDiffUtils; import foundation.e.drive.utils.FileUtils; import timber.log.Timber; Loading app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java +28 −14 Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ import static foundation.e.drive.models.SyncRequest.Type.DISABLE_SYNCING; import static foundation.e.drive.models.SyncedFileStateKt.DO_NOT_SCAN; import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_CLOUD; import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_DEVICE; import static foundation.e.drive.utils.FileDiffUtils.getActionForFileDiff; import static foundation.e.drive.contentScanner.FileDiffUtils.getActionForFileDiff; import android.content.Context; Loading @@ -26,7 +26,6 @@ import foundation.e.drive.models.DownloadRequest; import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.utils.FileDiffUtils; import foundation.e.drive.utils.FileUtils; import timber.log.Timber; Loading @@ -47,19 +46,18 @@ public class RemoteContentScanner extends AbstractContentScanner<RemoteFile> { @Override protected void onKnownFileFound(@NonNull RemoteFile file, @NonNull SyncedFileState fileState) { if (fileState.getScanScope() == DO_NOT_SCAN) return; final FileDiffUtils.Action action = getActionForFileDiff(file, fileState); if (action == FileDiffUtils.Action.Download) { Timber.d("Add download SyncRequest for %s", file.getRemotePath()); syncRequests.put(fileState.getId(), new DownloadRequest(file, fileState)); } else if (action == FileDiffUtils.Action.updateDB) { fileState.setLastEtag(file.getEtag()); final int affectedRows = DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context); if (affectedRows == 0) Timber.d("Error while updating eTag in DB for: %s", file.getRemotePath()); switch (action) { case Upload: addUploadRequest(fileState); break; case Download: addDownloadRequest(fileState, file); break; case UpdateDB: addUpdateDBRequest(fileState, file); break; } } Loading Loading @@ -108,4 +106,20 @@ public class RemoteContentScanner extends AbstractContentScanner<RemoteFile> { protected boolean isSyncedFolderParentOfFile(@NonNull SyncedFolder syncedFolder, @NonNull String dirPath) { return syncedFolder.getRemoteFolder().equals(dirPath); } private void addUploadRequest(SyncedFileState fileState) { fileState.setLastEtag(""); //Force fake lastEtag value to bypass condition check on UploadFileOperation syncRequests.put(fileState.getId(), new SyncRequest(fileState, SyncRequest.Type.UPLOAD)); } private void addUpdateDBRequest(SyncedFileState fileState, RemoteFile file) { fileState.setLastEtag(file.getEtag()); final int affectedRows = DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context); if (affectedRows == 0) Timber.d("Error while updating eTag in DB for: %s", file.getRemotePath()); } private void addDownloadRequest(SyncedFileState fileState, RemoteFile file) { Timber.d("Add download SyncRequest for %s", file.getRemotePath()); syncRequests.put(fileState.getId(), new DownloadRequest(file, fileState)); } } No newline at end of file app/src/main/java/foundation/e/drive/models/SyncedFileState.kt +3 −3 Original line number Diff line number Diff line Loading @@ -56,7 +56,7 @@ data class SyncedFileState (var id: Int, parcel.readLong(), parcel.readByte() != 0.toByte(), parcel.readInt() ) { } ) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeInt(id) Loading Loading @@ -85,7 +85,7 @@ data class SyncedFileState (var id: Int, } fun isLastEtagStored() : Boolean { return this.lastEtag.isNotEmpty() return !this.lastEtag.isNullOrEmpty() } fun hasBeenSynchronizedOnce(): Boolean { Loading @@ -93,7 +93,7 @@ data class SyncedFileState (var id: Int, } fun disableScanning() { this.scanScope == DO_NOT_SCAN this.scanScope = DO_NOT_SCAN } override fun toString(): String { Loading Loading
app/build.gradle +4 −0 Original line number Diff line number Diff line Loading @@ -69,6 +69,10 @@ android { } } kotlinOptions { jvmTarget = "11" } testOptions { unitTests { returnDefaultValues = true Loading
app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java→app/src/main/java/foundation/e/drive/contentScanner/FileDiffUtils.kt +116 −0 Original line number Diff line number Diff line /* * Copyright © MURENA SAS 2022-2023. * Copyright © MURENA SAS 2023. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html */ package foundation.e.drive.contentScanner package foundation.e.drive.utils; import androidx.annotation.NonNull; import com.owncloud.android.lib.resources.files.model.RemoteFile; import java.io.File; import foundation.e.drive.models.SyncedFileState; /** * This class encapsulate code to compare syncedFile & Remote file * but also RemoteFolder and SyncedFolder * @author vincent Bourgmayer */ public class FileDiffUtils { public enum Action { Upload, Download, skip, updateDB } import androidx.annotation.VisibleForTesting import com.owncloud.android.lib.resources.files.model.RemoteFile import foundation.e.drive.models.SyncedFileState import foundation.e.drive.utils.AppConstants import java.io.File object FileDiffUtils { enum class Action { Upload, Download, Skip, UpdateDB } /** * Define what to do of RemoteFile for which we know the Database equivalent * @param remoteFile RemoteFile * @param fileState SyncedFileState instance * @return Action from Enum */ @NonNull public static Action getActionForFileDiff(@NonNull RemoteFile remoteFile, @NonNull SyncedFileState fileState) { if (hasAlreadyBeenDownloaded(fileState) && !hasEtagChanged(remoteFile, fileState)) { return Action.skip; } final File localFile = new File(fileState.getLocalPath()); @JvmStatic fun getActionForFileDiff(remoteFile: RemoteFile, fileState: SyncedFileState): Action { if (!hasEtagChanged(remoteFile, fileState)) { if (isCorruptedTimestamp(remoteFile.modifiedTimestamp)) return Action.Upload if (isRemoteSizeSameAsLocalSize(remoteFile, localFile)) { return Action.updateDB; if (hasAlreadyBeenDownloaded(fileState)) return Action.Skip } return Action.Download; val localFileSize = getLocalFileSize(fileState.localPath) if (isRemoteAndLocalSizeEqual(remoteFile.size, localFileSize)) { return Action.UpdateDB } return Action.Download } /** * Define what to do of local file for which we know the Database equivalent Loading @@ -56,24 +41,25 @@ public class FileDiffUtils { * @param fileState SyncedFileState instance. Containing data from Database * @return Action from Enum */ @NonNull public static Action getActionForFileDiff(@NonNull File localFile, @NonNull SyncedFileState fileState) { //If no etag is stored in sfs, the file hasn't been sync up to server. then do upload if (fileState.getLastModified() < localFile.lastModified() || !fileState.isLastEtagStored()) { return Action.Upload; } return Action.skip; @JvmStatic fun getActionForFileDiff(localFile: File, fileState: SyncedFileState): Action { val isFileMoreRecentThanDB = fileState.lastModified < localFile.lastModified() return if (isFileMoreRecentThanDB || !fileState.isLastEtagStored()) { Action.Upload } else Action.Skip } /** * Compare RemoteFile's eTag with the one stored in Database * @param file RemoteFile * @param remoteFile RemoteFile * @param fileState last store file's state * @return true if ETag */ private static boolean hasEtagChanged(@NonNull RemoteFile file, @NonNull SyncedFileState fileState) { //if SyncedFileState has no Etag then it hasn't been uploaded and so must not exist on server return fileState.isLastEtagStored() && !file.getEtag().equals(fileState.getLastEtag()); @VisibleForTesting @JvmStatic fun hasEtagChanged(remoteFile: RemoteFile, fileState: SyncedFileState): Boolean { return fileState.isLastEtagStored() && remoteFile.etag != fileState.lastEtag } /** Loading @@ -82,19 +68,49 @@ public class FileDiffUtils { * @param fileState SyncedFileState containing data from Database * @return true if localLastModified store in Database == 0 */ private static boolean hasAlreadyBeenDownloaded(@NonNull SyncedFileState fileState) { return fileState.getLastModified() > 0L; @VisibleForTesting @JvmStatic fun hasAlreadyBeenDownloaded(fileState: SyncedFileState): Boolean { return fileState.lastModified > 0 } /** * * @param remoteFile RemoteFile instance * @param localFile File instance * Compare file size for remote file & local file * @param remoteFileSize RemoteFile.size. Note: it's equal to getLength() except for * folder where it also sum size of its content * @param localFileSize File.length() * @return true if remote file size is same as local file size */ private static boolean isRemoteSizeSameAsLocalSize(@NonNull RemoteFile remoteFile, @NonNull File localFile) { // if local file doesn't exist its size will be 0 // remoteFile.getSize() : getSize() is equal to getLength() except that for folder is also sum the content of the folder! return remoteFile.getSize() == localFile.length(); @VisibleForTesting @JvmStatic fun isRemoteAndLocalSizeEqual(remoteFileSize: Long, localFileSize: Long): Boolean { return remoteFileSize == localFileSize } /** * Check if timestamp is equal to max of unsigned int 32 * * For yet unknown reason, some remote files have this value on cloud (DB & file system) * the only way to fix them is to force re upload of the file with correct value * @param timestampInSecond remote file timestamp (Long) * @return true if the timestamp is equal to max of unsigned int 32 */ @VisibleForTesting @JvmStatic fun isCorruptedTimestamp(timestampInSecond: Long): Boolean { return timestampInSecond >= AppConstants.CORRUPTED_TIMESTAMP_IN_SECOND } /** * Fetch file size on device * @param path Path of the file * @return 0 if path is empty or file size */ @VisibleForTesting @JvmStatic fun getLocalFileSize(path: String): Long { if (path.isEmpty()) return 0 return File(path).length() } } No newline at end of file
app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java +0 −1 Original line number Diff line number Diff line Loading @@ -22,7 +22,6 @@ import foundation.e.drive.database.DbHelper; import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.utils.FileDiffUtils; import foundation.e.drive.utils.FileUtils; import timber.log.Timber; Loading
app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java +28 −14 Original line number Diff line number Diff line Loading @@ -11,7 +11,7 @@ import static foundation.e.drive.models.SyncRequest.Type.DISABLE_SYNCING; import static foundation.e.drive.models.SyncedFileStateKt.DO_NOT_SCAN; import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_CLOUD; import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_DEVICE; import static foundation.e.drive.utils.FileDiffUtils.getActionForFileDiff; import static foundation.e.drive.contentScanner.FileDiffUtils.getActionForFileDiff; import android.content.Context; Loading @@ -26,7 +26,6 @@ import foundation.e.drive.models.DownloadRequest; import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.utils.FileDiffUtils; import foundation.e.drive.utils.FileUtils; import timber.log.Timber; Loading @@ -47,19 +46,18 @@ public class RemoteContentScanner extends AbstractContentScanner<RemoteFile> { @Override protected void onKnownFileFound(@NonNull RemoteFile file, @NonNull SyncedFileState fileState) { if (fileState.getScanScope() == DO_NOT_SCAN) return; final FileDiffUtils.Action action = getActionForFileDiff(file, fileState); if (action == FileDiffUtils.Action.Download) { Timber.d("Add download SyncRequest for %s", file.getRemotePath()); syncRequests.put(fileState.getId(), new DownloadRequest(file, fileState)); } else if (action == FileDiffUtils.Action.updateDB) { fileState.setLastEtag(file.getEtag()); final int affectedRows = DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context); if (affectedRows == 0) Timber.d("Error while updating eTag in DB for: %s", file.getRemotePath()); switch (action) { case Upload: addUploadRequest(fileState); break; case Download: addDownloadRequest(fileState, file); break; case UpdateDB: addUpdateDBRequest(fileState, file); break; } } Loading Loading @@ -108,4 +106,20 @@ public class RemoteContentScanner extends AbstractContentScanner<RemoteFile> { protected boolean isSyncedFolderParentOfFile(@NonNull SyncedFolder syncedFolder, @NonNull String dirPath) { return syncedFolder.getRemoteFolder().equals(dirPath); } private void addUploadRequest(SyncedFileState fileState) { fileState.setLastEtag(""); //Force fake lastEtag value to bypass condition check on UploadFileOperation syncRequests.put(fileState.getId(), new SyncRequest(fileState, SyncRequest.Type.UPLOAD)); } private void addUpdateDBRequest(SyncedFileState fileState, RemoteFile file) { fileState.setLastEtag(file.getEtag()); final int affectedRows = DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context); if (affectedRows == 0) Timber.d("Error while updating eTag in DB for: %s", file.getRemotePath()); } private void addDownloadRequest(SyncedFileState fileState, RemoteFile file) { Timber.d("Add download SyncRequest for %s", file.getRemotePath()); syncRequests.put(fileState.getId(), new DownloadRequest(file, fileState)); } } No newline at end of file
app/src/main/java/foundation/e/drive/models/SyncedFileState.kt +3 −3 Original line number Diff line number Diff line Loading @@ -56,7 +56,7 @@ data class SyncedFileState (var id: Int, parcel.readLong(), parcel.readByte() != 0.toByte(), parcel.readInt() ) { } ) override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeInt(id) Loading Loading @@ -85,7 +85,7 @@ data class SyncedFileState (var id: Int, } fun isLastEtagStored() : Boolean { return this.lastEtag.isNotEmpty() return !this.lastEtag.isNullOrEmpty() } fun hasBeenSynchronizedOnce(): Boolean { Loading @@ -93,7 +93,7 @@ data class SyncedFileState (var id: Int, } fun disableScanning() { this.scanScope == DO_NOT_SCAN this.scanScope = DO_NOT_SCAN } override fun toString(): String { Loading