Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7e4a8734 authored by Vincent Bourgmayer's avatar Vincent Bourgmayer
Browse files

Merge branch '6934-help-cloud-with-invalid-timestamp' into 'main'

Help cloud to fix corrupted file "modificationTimeStamp"

See merge request !248
parents 79477809 a6c19042
Loading
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -69,6 +69,10 @@ android {
        }
    }

    kotlinOptions {
        jvmTarget = "11"
    }

    testOptions {
        unitTests {
            returnDefaultValues = true
+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
@@ -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
    }

    /**
@@ -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
+0 −1
Original line number Diff line number Diff line
@@ -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;

+28 −14
Original line number Diff line number Diff line
@@ -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;

@@ -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;

@@ -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;
        }
    }

@@ -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
+3 −3
Original line number Diff line number Diff line
@@ -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)
@@ -85,7 +85,7 @@ data class SyncedFileState (var id: Int,
    }

    fun isLastEtagStored() : Boolean {
        return this.lastEtag.isNotEmpty()
        return !this.lastEtag.isNullOrEmpty()
    }

    fun hasBeenSynchronizedOnce(): Boolean {
@@ -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