diff --git a/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java index cd8416189e74f94884a366fe3020e9dcdddd3ec8..45fa47a69a241977f26795f784e019bc97cd87d5 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java @@ -67,7 +67,7 @@ public abstract class AbstractContentScanner { } for(SyncedFileState remainingFileState : fileStates) { - onMissingRemoteFile(remainingFileState); + onMissingFile(remainingFileState); } return syncRequests; }; @@ -93,7 +93,7 @@ public abstract class AbstractContentScanner { * When a file doesn't exist anymore we remove it from device/cloud (depending of implementation) & from Database * @param fileState SyncedFileState for which we lack remote file */ - protected abstract void onMissingRemoteFile(SyncedFileState fileState); + protected abstract void onMissingFile(SyncedFileState fileState); /** * A new file has been found diff --git a/app/src/main/java/foundation/e/drive/contentScanner/AbstractFileLister.java b/app/src/main/java/foundation/e/drive/contentScanner/AbstractFileLister.java new file mode 100644 index 0000000000000000000000000000000000000000..4515cb315d6cc40c7b781a5fed5be0fbe610a020 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/AbstractFileLister.java @@ -0,0 +1,208 @@ +/* + * Copyright © ECORP SAS 2022. + * 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; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFolder; +import timber.log.Timber; + +/** + * Factorisation of codes used to list file on cloud and on device + * Implementation will vary depending of it is RemoteFile or File instance + * that we want to list + * @author vincent Bourgmayer + */ +public abstract class AbstractFileLister { + + protected abstract boolean skipSyncedFolder(@NonNull SyncedFolder syncedFolder); + protected abstract boolean skipFile(@NonNull T file); + + /** + * If the given folder has not changed, compared to database's data, skip it + * @param currentDirectory + * @param syncedFolder + * @param context + * @return + */ + protected abstract boolean skipDirectory(@NonNull T currentDirectory, @NonNull SyncedFolder syncedFolder, @NonNull Context context); + protected abstract boolean isDirectory(@NonNull T file); + protected abstract String getFileName(@NonNull T file); + protected abstract FolderLoader getFolderLoader(); + protected abstract void updateSyncedFolder(@NonNull SyncedFolder syncedFolder,@NonNull T folder); + + /** + * This allows to use RemoteOperationResult for RemoteFileLister + * todo: some optimization could be done with FolderWrapper + * @param RemoteFile or File + */ + /* package */ interface FolderLoader { + /* package */ FolderWrapper getFolderWrapper(); + /* package */ boolean load(@NonNull SyncedFolder folder); + } + + protected final List folders; + private final List contentToScan; + protected boolean diveIntoSkippedDir = false; //to be overriden only + + /** + * constructor to be call with super + * @param folders List of SyncedFolder to read + */ + protected AbstractFileLister(@NonNull List folders) { + this.folders = folders; + this.contentToScan = new ArrayList<>(); + } + + /** + * Perform the listing of files + * @param context + * @return + */ + public boolean listContentToScan(@NonNull Context context) { + final ListIterator iterator = folders.listIterator() ; + boolean isSomeContentToSync = false; + while (iterator.hasNext()) { + final SyncedFolder syncedFolder = iterator.next(); + if (syncedFolder == null) continue; + Timber.v("SyncedFolder : %s, %s, %s, %s, %s", syncedFolder.getLibelle(), syncedFolder.getLocalFolder(), syncedFolder.getLastModified(), syncedFolder.isScanLocal(), syncedFolder.getId()); + isSomeContentToSync = hasDirectoryContentToScan(syncedFolder, iterator, context) || isSomeContentToSync; + } + + if (isSomeContentToSync) { + DbHelper.updateSyncedFolders(folders, context); //@todo: maybe do this when all contents will be synced. + } + return isSomeContentToSync; + } + + /** + * Detailed process for reading a single folder + * @param syncedFolder the SyncedFolder to check + * @param iterator iterator over list of SyncedFolder + * @param context context used to call DB + * @return true the directory has content to synchronized false otherwise + */ + private boolean hasDirectoryContentToScan(@NonNull SyncedFolder syncedFolder, @NonNull ListIterator iterator, @NonNull Context context) { + + final FolderLoader dirLoader = getFolderLoader(); + if (skipSyncedFolder(syncedFolder) + || !isSyncedFolderInDb(syncedFolder, context) + || !dirLoader.load(syncedFolder)) { // I try to get the real directory (File or RemoteFile). + iterator.remove(); + return false; + } + + final FolderWrapper currentDirectory = dirLoader.getFolderWrapper(); + + if (currentDirectory.isMissing()) { + return true; + } + + if (skipDirectory(currentDirectory.getFolder(), syncedFolder, context)) { + iterator.remove(); + syncedFolder.setToSync(false); + /** + * LocalFileLister need to be able to access subFiles even for skipped directory + * because A folder's lastModified value won't change if a subfolder's content is changed, removed or created + * RemoteFileLister is based on eTag, which change even if a subfolder's content is changed + */ + if (!diveIntoSkippedDir) return false; + } else { + syncedFolder.setToSync(true); + } + updateSyncedFolder(syncedFolder, currentDirectory.getFolder()); + + //todo: look where to put in subclasses : syncedFolder.setLastEtag(directory.getEtag()).setToSync(true); + + final List dirContent = currentDirectory.getContent(); + if (dirContent != null && !dirContent.isEmpty()) { + contentToScan.addAll(sortContent(iterator, syncedFolder, dirContent)); + } + return true; + } + + + /** + * Tell if the given syncedFolder has been persisted in database. + * If it's not the case initally, it tries to persist it. + * @param syncedFolder SyncedFolder to check for persistance + * @param context context to contact database + * @return false if not persisted in db + */ + private boolean isSyncedFolderInDb(@NonNull SyncedFolder syncedFolder, @NonNull Context context) { + if (syncedFolder.getId() > 0) return true; + + final int syncedFolder_id = (int) DbHelper.insertSyncedFolder(syncedFolder, context); //It will return -1 if there is an error, like an already existing folder with same value + if (syncedFolder_id <= 0) { + Timber.v("insertion of syncedFolder for %s failed: %s ", syncedFolder.getRemoteFolder(), syncedFolder_id); + return false; + } + + syncedFolder.setId(syncedFolder_id); + return true; + } + + + /** + * Split content of a folder: subfolder on one side are added into the iterator loop + * while subfiles are added to result list + * @param iterator iterator intance over list of SyncedFolder + * @param syncedFolder the SyncFolder which own the content + * @param dirContent Content to sort + * @return List of subfiles to scan or empty list if nothing + */ + private List sortContent(@NonNull ListIterator iterator, @NonNull SyncedFolder syncedFolder, List dirContent) { + final List result = new ArrayList<>(); + if (dirContent == null) return result; + + for (T subFile : dirContent) { + if (subFile == null) continue; + + final String fileName = getFileName(subFile); + if (fileName == null) continue; + + if (isDirectory(subFile)) { + final SyncedFolder subSyncedFolder = new SyncedFolder(syncedFolder, fileName, 0L, "");//Need to set lastModified to 0 to handle it on next iteration + iterator.add(subSyncedFolder); + iterator.previous(); + } else if (syncedFolder.isToSync() && !skipFile(subFile)) { + Timber.v("added %s into list of file to scan", fileName); + result.add(subFile); + } + } + + return result; + } + + /** + * List of file to scan + * @return List or List is expected based on implementations + */ + public List getContentToScan() { + return contentToScan; + } + + /** + * Share the list of syncedFolder's ID for syncedFolder which has content to scan + * @return List of syncedFolder ids + */ + public List getSyncedFoldersId() { + final List result = new ArrayList<>(); + for (SyncedFolder syncedFolder : folders) { + result.add((long) syncedFolder.getId()); + } + return result; + } +} diff --git a/app/src/main/java/foundation/e/drive/contentScanner/FolderWrapper.java b/app/src/main/java/foundation/e/drive/contentScanner/FolderWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..6fb4d3074a3a5453772f88d0190f8d05dbcb5216 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/FolderWrapper.java @@ -0,0 +1,70 @@ +/* + * Copyright © ECORP SAS 2022. + * 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; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * This wrapper is used to contains either a File/RemoteFile instance with its content + * or a "missing file" information. The explanation of this, is because scanning remote file + * may result in different type of error while loading content (network issues, etc.) + * but we want to handle 404 error (= missing resources) has to be considered like a + * potential file deletions. There is probably something better to do for that + * @author vincent Bourgmayer + * @param File or RemoteFile expected + */ +public class FolderWrapper { + + private final boolean missing; + private final T folder; + private final List content; + + protected FolderWrapper(@NonNull T folder) { + content = new ArrayList<>(); + this.folder = folder; + missing = false; + } + + protected FolderWrapper() { + missing = true; + folder = null; + content = null; + } + + /** + * Get the folder (should be RemoteFile or File instance ) represented by this abstraction + * @return null if it is missing + */ + public T getFolder() { + return folder; + } + + /** + * Get subfiles (including sub folder) of the current folder + * @return Null if the directory is missing + */ + public List getContent() { + return content; + } + + public void addContent(@NonNull List newContent) { + content.addAll(newContent); + } + + /** + * Should return true if local File for directory doesn't exists, or if ReadFolderRemoteOperation return 404 + * for a remote file + * @return + */ + public boolean isMissing() { + return missing; + } +} diff --git a/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java index 23ac48dba64ae4d7d25251bbae193dbeda9485a6..845323d7258f49312c2c5b8db147e0c38ee1905c 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java @@ -33,7 +33,7 @@ public class LocalContentScanner extends AbstractContentScanner{ } @Override - protected void onMissingRemoteFile(SyncedFileState fileState) { + protected void onMissingFile(SyncedFileState fileState) { if (!fileState.hasBeenSynchronizedOnce()) { return; } @@ -61,12 +61,10 @@ public class LocalContentScanner extends AbstractContentScanner{ if (parentDir.isScanLocal()) scannableValue += 2; } - //create the syncedFile State final SyncedFileState newSyncedFileState = new SyncedFileState(-1, file.getName(), filePath, parentDir.getRemoteFolder() + file.getName(), "", 0, parentDir.getId(), parentDir.isMediaType(),scannableValue); - //Store it in DB - int storedId = DbHelper.manageSyncedFileStateDB(newSyncedFileState, "INSERT", context); - if (storedId > 0){ + final int storedId = DbHelper.manageSyncedFileStateDB(newSyncedFileState, "INSERT", context); + if (storedId > 0) { newSyncedFileState.setId( storedId ); Timber.d("Add upload SyncRequest for new file %s", filePath); syncRequests.put(storedId, new SyncRequest(newSyncedFileState, SyncRequest.Type.UPLOAD)); diff --git a/app/src/main/java/foundation/e/drive/contentScanner/LocalFileLister.java b/app/src/main/java/foundation/e/drive/contentScanner/LocalFileLister.java new file mode 100644 index 0000000000000000000000000000000000000000..378c52ffb23d16bdf764e678481065b24a9d6a5c --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/LocalFileLister.java @@ -0,0 +1,111 @@ +/* + * Copyright © ECORP SAS 2022. + * 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; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.io.File; +import java.io.FileFilter; +import java.util.Arrays; +import java.util.List; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.fileFilters.FileFilterFactory; +import foundation.e.drive.models.SyncedFolder; +import timber.log.Timber; + +/** + * Implementation of AbstractFileLister with method adapted to local content + * @author vincent Bourgmayer + */ +public class LocalFileLister extends AbstractFileLister { + + public LocalFileLister(@NonNull List directories) { + super(directories); + super.diveIntoSkippedDir = true; + } + + @Override + protected boolean skipDirectory(@NonNull File currentDirectory, @NonNull SyncedFolder syncedFolder, @NonNull Context context) { + return currentDirectory.lastModified() == syncedFolder.getLastModified() + && !DbHelper.syncedFolderHasContentToUpload(syncedFolder.getId(), context); + } + + @Override + protected boolean skipSyncedFolder(@NonNull SyncedFolder syncedFolder) { + final File folder = new File(syncedFolder.getLocalFolder()); + return (syncedFolder.isMediaType() && folder.isHidden()) || !syncedFolder.isScanLocal(); + } + + @Override + protected boolean skipFile(@NonNull File file) { + return file.isHidden(); + } + + @Override + protected boolean isDirectory(@NonNull File file) { + return file.isDirectory(); + } + + @Override + protected String getFileName(@NonNull File file) { + return "/" + file.getName(); + } + + @Override + protected LocalFolderLoader getFolderLoader() { + return new LocalFolderLoader(); + } + + @Override + protected void updateSyncedFolder(@NonNull SyncedFolder syncedFolder, @NonNull File folder) { + syncedFolder.setLastModified(folder.lastModified()); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public class LocalFolderLoader implements FolderLoader { + private FolderWrapper folder; + + @Override + public FolderWrapper getFolderWrapper() { + return folder; + } + + + /** + * "Load" a directory to check if it exist, and list its content + * It is expected to always return true. Its remote equivalent (in RemoteFileLister) + * Might return false because of network, & RemoteFile classes. But that's not the case + * here. + * @param syncedFolder + * @return always true because it's about "File" instance. + */ + @Override + public boolean load(@NonNull SyncedFolder syncedFolder) { + final File dir = new File(syncedFolder.getLocalFolder()); + Timber.v("Local Folder (last modified / exists): %s, %s", dir.lastModified(), dir.exists()); + + if (!dir.exists()) { + folder = new FolderWrapper(); //missing Folder Wrapper + } else { + folder = new FolderWrapper(dir); + final String category = syncedFolder.isMediaType() ? "media" : syncedFolder.getLibelle(); + + final FileFilter filter = FileFilterFactory.getFileFilter(category); + final File[] subFiles = dir.listFiles(filter); + if (subFiles != null) { + folder.addContent(Arrays.asList(subFiles)); + } + } + return true; + } + } +} diff --git a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java index 047bd8ffa8711d85dbf9986f7f9d792d931a36c8..3e7c4ba1a6c5c3136205ebb8775698f686c79978 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java @@ -7,19 +7,19 @@ */ package foundation.e.drive.contentScanner; +import static foundation.e.drive.models.SyncRequest.Type.LOCAL_DELETE; import static foundation.e.drive.utils.FileDiffUtils.getActionForFileDiff; import android.accounts.Account; import android.content.Context; -import android.provider.MediaStore; import com.owncloud.android.lib.resources.files.model.RemoteFile; -import java.io.File; import java.util.List; import foundation.e.drive.database.DbHelper; 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.CommonUtils; @@ -74,7 +74,6 @@ public class RemoteContentScanner extends AbstractContentScanner { final SyncedFileState newFileState = new SyncedFileState(-1, fileName, parentDir.getLocalFolder() + fileName, remoteFilePath, file.getEtag(), 0, parentDir.getId(), parentDir.isMediaType(), scannableValue); - //Store it in DB final int storedId = DbHelper.manageSyncedFileStateDB(newFileState, "INSERT", context); if (storedId > 0) { newFileState.setId(storedId); @@ -86,7 +85,10 @@ public class RemoteContentScanner extends AbstractContentScanner { } @Override - protected void onMissingRemoteFile(SyncedFileState fileState) { + protected void onMissingFile(SyncedFileState fileState) { + this.syncRequests.put(fileState.getId(), new SyncRequest(fileState, LOCAL_DELETE)); + + /* todo: move commented code into synchronizationService if (!CommonUtils.isThisSyncAllowed(account, fileState.isMediaType())) { Timber.d("Sync of current file: %s isn't allowed", fileState.getName()); return; @@ -112,7 +114,7 @@ public class RemoteContentScanner extends AbstractContentScanner { if (DbHelper.manageSyncedFileStateDB(fileState, "DELETE", context) <= 0) { Timber.e("Failed to remove %s from DB", file.getName()); - } + }*/ } @Override diff --git a/app/src/main/java/foundation/e/drive/contentScanner/RemoteFileLister.java b/app/src/main/java/foundation/e/drive/contentScanner/RemoteFileLister.java new file mode 100644 index 0000000000000000000000000000000000000000..99f9a257f6da4cdbfca64577a0c4756e066249aa --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/RemoteFileLister.java @@ -0,0 +1,134 @@ +/* + * Copyright © ECORP SAS 2022. + * 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; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; +import com.owncloud.android.lib.resources.files.model.RemoteFile; + +import java.util.ArrayList; +import java.util.List; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFolder; +import foundation.e.drive.utils.CommonUtils; + + +/** + * Implementation of AbstractFileLister with method adapted to remote content + * @author vincent Bourgmayer + */ +public class RemoteFileLister extends AbstractFileLister { + private OwnCloudClient client; + + public RemoteFileLister(@NonNull List directories, @NonNull OwnCloudClient client) { + super(directories); + this.client = client; + } + + @Override + protected boolean skipSyncedFolder(@NonNull SyncedFolder syncedFolder) { + return (syncedFolder.isMediaType() + && CommonUtils.getFileNameFromPath(syncedFolder.getRemoteFolder()).startsWith(".")) + || !syncedFolder.isScanRemote(); + } + + @Override + protected boolean skipFile(@NonNull RemoteFile file) { + final String fileName = CommonUtils.getFileNameFromPath(file.getRemotePath()); + return fileName == null || fileName.isEmpty() || fileName.startsWith("."); + } + + @Override + protected boolean skipDirectory(@NonNull RemoteFile directory, @NonNull SyncedFolder syncedFolder, @NonNull Context context) { + return directory.getEtag().equals(syncedFolder.getLastEtag()) + && !DbHelper.syncedFolderHasContentToDownload(syncedFolder.getId(), context); + } + + @Override + protected boolean isDirectory(@NonNull RemoteFile file) { + return file.getMimeType().equals("DIR"); + } + + @Override + protected String getFileName(@NonNull RemoteFile file) { + return CommonUtils.getFileNameFromPath(file.getRemotePath()); + } + + @Override + protected RemoteFolderLoader getFolderLoader() { + return new RemoteFolderLoader(); + } + + @Override + protected void updateSyncedFolder(@NonNull SyncedFolder syncedFolder, @NonNull RemoteFile folder) { + syncedFolder.setLastEtag(folder.getEtag()); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public class RemoteFolderLoader implements FolderLoader { + private static final int HTTP_404 = 404; + private FolderWrapper directory; + + @Override + public boolean load(@NonNull SyncedFolder syncedFolder) { + if (syncedFolder.getRemoteFolder() == null) return false; + final RemoteOperationResult remoteOperationResult = readRemoteFolder(syncedFolder.getRemoteFolder(), client); + + if (!remoteOperationResult.isSuccess()) { + if (remoteOperationResult.getHttpCode() == HTTP_404) { + directory = new FolderWrapper<>(); + return true; + } + return false; + } + + final List datas = remoteOperationResult.getData(); + if (datas == null || datas.size() < 1) return false; + + final int dataSize = datas.size(); + final RemoteFile remoteDir = (RemoteFile) datas.get(0); + if (remoteDir == null) return false; + + directory = new FolderWrapper<>(remoteDir); + + final List subFiles = new ArrayList<>(); + for (Object o: datas.subList(1, dataSize)) { + final RemoteFile subFile = (RemoteFile) o; + if (subFile != null) { + subFiles.add(subFile); + } + } + directory.addContent(subFiles); + return true; + } + + /** + * Execute the Propfind query to list files under a directory + * @param remotePath RemotePath of the directory to observe + * @param client Client used to execute query + * @return RemoteOperationResult + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public RemoteOperationResult readRemoteFolder(@NonNull String remotePath, @NonNull OwnCloudClient client) { + final ReadFolderRemoteOperation operation = new ReadFolderRemoteOperation(remotePath); + return operation.execute(client); + } + + @Override + public FolderWrapper getFolderWrapper() { + return directory; + } + } +} diff --git a/app/src/main/java/foundation/e/drive/models/SyncRequest.java b/app/src/main/java/foundation/e/drive/models/SyncRequest.java index d2b820c8e310e828d24b05ddb284bff995abf70b..29bab6b9f2c52f20b2edf7eec51ec9fb2c7c60fe 100644 --- a/app/src/main/java/foundation/e/drive/models/SyncRequest.java +++ b/app/src/main/java/foundation/e/drive/models/SyncRequest.java @@ -10,7 +10,7 @@ package foundation.e.drive.models; import androidx.annotation.Nullable; public class SyncRequest { - public enum Type { UPLOAD, DOWNLOAD, REMOTE_DELETE}; + public enum Type { UPLOAD, DOWNLOAD, REMOTE_DELETE, LOCAL_DELETE}; private final SyncedFileState syncedFileState; diff --git a/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java b/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java index 9fe4a53c74770e3153cf070c8c88c4d449e181f5..9260379fe79db66813aabdc55960f3cc6074d5d0 100644 --- a/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java @@ -89,7 +89,9 @@ public class DownloadFileOperation extends RemoteOperation { if (!localFile.getParentFile().exists()) { localFile.getParentFile().mkdirs(); } else if (localFile.exists()) { - localFile.delete(); + localFile.delete(); //We remove the old file to avoid overwrite issue after + //WARNING todo: however, if the call to renameTo fails after this one, we have lost the local file + } if (!tmpFile.renameTo(localFile)) { diff --git a/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java b/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java index 0ab10daa39211ca6ae44ef4d41473f4635593611..627f31ff87de57fbdd3644fb695310d25ff7a0fb 100644 --- a/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java @@ -10,6 +10,9 @@ package foundation.e.drive.operations; import android.content.Context; + +import androidx.annotation.VisibleForTesting; + import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; @@ -19,6 +22,8 @@ import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; + +import foundation.e.drive.contentScanner.RemoteFileLister; import foundation.e.drive.database.DbHelper; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.utils.CommonUtils; @@ -30,19 +35,17 @@ import timber.log.Timber; * @author Vincent Bourgmayer */ public class ListFileRemoteOperation extends RemoteOperation> { - private static final int HTTP_404 = 404; private final List syncedFolders; private final Context context; - private final int initialFolderNumber; - private final ArrayList remoteFiles; - public ListFileRemoteOperation(List syncedFolders, Context context, int initialFolderNumber) { + private final ArrayList updatedSyncedFoldersId; + + public ListFileRemoteOperation(List syncedFolders, Context context) { Timber.tag(ListFileRemoteOperation.class.getSimpleName()); this.syncedFolders = syncedFolders; this.context = context; - this.initialFolderNumber = initialFolderNumber; - remoteFiles = new ArrayList<>(); + updatedSyncedFoldersId = new ArrayList<>(); } /** @@ -52,138 +55,23 @@ public class ListFileRemoteOperation extends RemoteOperation> run(OwnCloudClient ownCloudClient) { - RemoteOperationResult finalResult; - - boolean atLeastOneDirAsChanged = false; - final ListIterator mSyncedFolderIterator = syncedFolders.listIterator(); + final RemoteFileLister fileLister = new RemoteFileLister(syncedFolders, ownCloudClient); + final boolean isContentToScan = fileLister.listContentToScan(context); - while (mSyncedFolderIterator.hasNext()) { - try { - Thread.sleep(150); - } catch (InterruptedException exception) { - Timber.v( "listFileRemoteOperation's sleep had been interrupted"); - } - - final SyncedFolder syncedFolder = mSyncedFolderIterator.next(); - if (shouldSkipSyncedFolder(syncedFolder)) { - mSyncedFolderIterator.remove(); - continue; - } - - if (syncedFolder.getId() == -1) { - final int syncedFolderId = (int) DbHelper.insertSyncedFolder(syncedFolder, context); - if (syncedFolderId <= 0) { - mSyncedFolderIterator.remove(); - Timber.v("insertion of syncedFolder for %s failed: %s ", syncedFolder.getRemoteFolder(), syncedFolderId); - continue; - } - syncedFolder.setId(syncedFolderId); - } - - final ReadFolderRemoteOperation operation = new ReadFolderRemoteOperation(syncedFolder.getRemoteFolder()); - final RemoteOperationResult result = operation.execute(ownCloudClient); - if (result.isSuccess()) { - final boolean dirHasChanged = onListFileSuccess(mSyncedFolderIterator, syncedFolder, result); - atLeastOneDirAsChanged = dirHasChanged || atLeastOneDirAsChanged; //stay true if set previously - } else { - if (result.getHttpCode() == HTTP_404) { //dir has not been found - atLeastOneDirAsChanged = true; - onHttp404Received(mSyncedFolderIterator, syncedFolder); - } - Timber.d("ReadFolderRemoteOperation failed : http %s, %s => ignored", result.getHttpCode(), result.getLogMessage()); - } - } - finalResult = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.OK); - if (atLeastOneDirAsChanged) { + final RemoteOperationResult result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.OK); + if (isContentToScan) { DbHelper.updateSyncedFolders(this.syncedFolders, this.context); - finalResult.setResultData(remoteFiles); - } - return finalResult; - } - - - /** - * Analyze list of remote files for a given Directory - * @param iterator ListIterator contains list of syncedFolder to check - * @param syncedFolder the SyncedFolder for the given Directory - * @param result Result of ListRemoteFile operation - * @return true if some directory has changed or its content - */ - private boolean onListFileSuccess(final ListIterator iterator, SyncedFolder syncedFolder, RemoteOperationResult result) { - final int dataSize = result.getData().size(); - final RemoteFile directory = (RemoteFile) result.getData().get(0); - - if (directory.getEtag().equals(syncedFolder.getLastEtag()) - && !DbHelper.syncedFolderHasContentToDownload(syncedFolder.getId(), context)) { - return false; - } - syncedFolder.setLastEtag(directory.getEtag()).setToSync(true); - - final List subFiles = result.getData().subList(1, dataSize); - - for (int i = -1, subFilesSize = subFiles.size(); ++i < subFilesSize;) { - final RemoteFile remoteFile = (RemoteFile) subFiles.get(i); - - if (remoteFile.getMimeType().equals("DIR")) { - final String suffixPath = remoteFile.getRemotePath().substring(syncedFolder.getRemoteFolder().length()); - final SyncedFolder subSyncedFolder = new SyncedFolder(syncedFolder, suffixPath, 0L, ""); //need to set empty etag to allow it to be scan - iterator.add(subSyncedFolder); - iterator.previous(); - } else if (!isHiddenFile(remoteFile)) { - Timber.v("Add remote file %s", remoteFile.getRemotePath()); - this.remoteFiles.add(remoteFile); - } - } - return true; - } - - /** - * Handle a case where the checked directory is missing on cloud - * @param iterator ListIterator list of syncedFolder to scan - * @param syncedFolder Missing syncedFolder on cloud - */ - private void onHttp404Received(final ListIterator iterator, SyncedFolder syncedFolder) { - syncedFolder.setToSync(true); - - final File localFolder = new File(syncedFolder.getLocalFolder()); - if (localFolder.listFiles().length == 0) { - localFolder.delete(); - } - - if (!localFolder.exists()) { - if (syncedFolder.getId() > this.initialFolderNumber) { //Do not remove initial syncedFolder - DbHelper.deleteSyncedFolder(syncedFolder.getId(), context); - } - iterator.remove(); + result.setResultData(fileLister.getContentToScan()); } - } - + updatedSyncedFoldersId.addAll(fileLister.getSyncedFoldersId()); - /** - * Indicate if remote file is an hidden file - * @param file Remote file - * @return true if it's an hidden file - */ - private boolean isHiddenFile(RemoteFile file) { - final String fileName = CommonUtils.getFileNameFromPath(file.getRemotePath()); - return fileName.startsWith("."); - } - - /** - * Indicate if we should skip SyncedFolder - * @param syncedFolder - * @return - */ - private boolean shouldSkipSyncedFolder(SyncedFolder syncedFolder) { - return (syncedFolder.isMediaType() - && CommonUtils.getFileNameFromPath(syncedFolder.getRemoteFolder()).startsWith(".")) - || !syncedFolder.isScanRemote(); + return result; } /** * @return list of syncedFolder */ - public List getSyncedFolderList(){ - return this.syncedFolders; + public List getSyncedFoldersId(){ + return this.updatedSyncedFoldersId; } } diff --git a/app/src/main/java/foundation/e/drive/services/ObserverService.java b/app/src/main/java/foundation/e/drive/services/ObserverService.java index 7d6a2239f8e8bcc88645d9cc2984e7dbe6624187..0da565c1a5b804f0f2427ef70583b70d07c52f13 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.ListIterator; import foundation.e.drive.contentScanner.LocalContentScanner; +import foundation.e.drive.contentScanner.LocalFileLister; import foundation.e.drive.contentScanner.RemoteContentScanner; import foundation.e.drive.database.DbHelper; import foundation.e.drive.fileFilters.CrashlogsFileFilter; @@ -69,7 +70,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene private List mSyncedFolders; //List of synced folder private boolean isWorking = false; - private int initialFolderCounter; private Account mAccount; private HashMap syncRequests; //integer is SyncedFileState id; Parcelable is the operation private SynchronizationServiceConnection synchronizationServiceConnection = new SynchronizationServiceConnection(); @@ -113,7 +113,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene } this.syncRequests = new HashMap<>(); - initialFolderCounter = prefs.getInt(AppConstants.INITIALFOLDERS_NUMBER, 0); handlerThread = new HandlerThread("syncService_onResponse"); handlerThread.start(); @@ -263,7 +262,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene } try { - final ListFileRemoteOperation loadOperation = new ListFileRemoteOperation(this.mSyncedFolders, this, this.initialFolderCounter); + final ListFileRemoteOperation loadOperation = new ListFileRemoteOperation(this.mSyncedFolders, this); loadOperation.execute(client, this, handler); } catch (IllegalArgumentException exception) { Timber.e(exception); @@ -288,7 +287,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene } else if (settingsSyncedEnabled) { return DbHelper.getSyncedFolderList(this, false); } else { - return new ArrayList(); + return new ArrayList<>(); } } @@ -309,16 +308,15 @@ public class ObserverService extends Service implements OnRemoteOperationListene final List remoteFiles = ((RemoteOperationResult>)result).getResultData(); if (remoteFiles != null) { - final ListFileRemoteOperation listFileOperation = (ListFileRemoteOperation) operation; - mSyncedFolders = listFileOperation.getSyncedFolderList(); //The list may have been reduced if some directory hasn't changed + final List syncedFoldersId = listFileOperation.getSyncedFoldersId(); - final List syncedFileStateList = DbHelper.getSyncedFileStatesByFolders(this, - getIdsFromFolderToScan()); + final List syncedFileStates = DbHelper.getSyncedFileStatesByFolders(this, + syncedFoldersId); - if (!remoteFiles.isEmpty() || !syncedFileStateList.isEmpty()) { + if (!remoteFiles.isEmpty() || !syncedFileStates.isEmpty()) { final RemoteContentScanner scanner = new RemoteContentScanner(getApplicationContext(), mAccount, mSyncedFolders); - syncRequests.putAll(scanner.scanContent(remoteFiles, syncedFileStateList)); + syncRequests.putAll(scanner.scanContent(remoteFiles, syncedFileStates)); } } @@ -350,22 +348,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene } } - /** - * Method to get Id of SyncedFolder to scan - * @return List id of SyncedFolder to scan - */ - private List getIdsFromFolderToScan() { - final List result = new ArrayList<>(); - for (int i = -1, size = this.mSyncedFolders.size(); ++i < size;) { - final SyncedFolder syncedFolder = this.mSyncedFolders.get(i); - if (syncedFolder.isToSync() ){ - result.add( (long) syncedFolder.getId() ); - } - } - return result; - } - - /* Methods related to device Scanning */ /** @@ -405,76 +387,25 @@ public class ObserverService extends Service implements OnRemoteOperationListene */ private void scanLocalFiles(){ Timber.i("scanLocalFiles()"); - final List fileList = new ArrayList<>(); - final List folderIdList= new ArrayList<>(); - boolean contentToSyncFound = false; - if (CommonUtils.isSettingsSyncEnabled(mAccount)) generateAppListFile(); - final ListIterator iterator = mSyncedFolders.listIterator() ; - - while(iterator.hasNext()) { - final SyncedFolder syncedFolder = iterator.next(); - Timber.v("SyncedFolder : %s, %s, %s, %s, %s", syncedFolder.getLibelle(), syncedFolder.getLocalFolder(), syncedFolder.getLastModified(), syncedFolder.isScanLocal(), syncedFolder.getId()); - - //Check it's not a hidden file - final String fileName = CommonUtils.getFileNameFromPath(syncedFolder.getLocalFolder()); - if (fileName == null || fileName.startsWith(".") || !syncedFolder.isScanLocal()) { - iterator.remove(); - continue; - } - if (syncedFolder.getId() == -1) { - final int syncedFolder_id = (int) DbHelper.insertSyncedFolder(syncedFolder, this); //It will return -1 if there is an error, like an already existing folder with same value - if (syncedFolder_id <= 0) { - iterator.remove(); - continue; - } - syncedFolder.setId(syncedFolder_id); - } - - - final File localDirectory = new File(syncedFolder.getLocalFolder()); //Obtention du fichier local - Timber.v("Local Folder (last modified / exists): %s, %s", localDirectory.lastModified(),localDirectory.exists()); - - if (!localDirectory.exists()) { - contentToSyncFound = true; - folderIdList.add( (long) syncedFolder.getId() ); - continue; - } - - if (localDirectory.lastModified() > syncedFolder.getLastModified() - || DbHelper.syncedFolderHasContentToUpload(syncedFolder.getId(), getApplicationContext())) { - syncedFolder.setLastModified(localDirectory.lastModified()); //@Todo: it would be better to set it after all it's content has been synced - contentToSyncFound = true; - folderIdList.add((long) syncedFolder.getId()); - } + final LocalFileLister fileLister = new LocalFileLister(mSyncedFolders); + final boolean isContentToScan = fileLister.listContentToScan(getApplicationContext()); - final FileFilter filter = FileFilterFactory.getFileFilter( (syncedFolder.isMediaType()) ? "media" : syncedFolder.getLibelle() ); - final File[] subFiles = localDirectory.listFiles(filter); //skip hidden media files - - if (subFiles == null) continue; - for (File subFile : subFiles) { - if (subFile.isDirectory()) { - final SyncedFolder subSyncedFolder = new SyncedFolder(syncedFolder, subFile.getName() + FileUtils.PATH_SEPARATOR, 0L, "");//Need to set lastModified to 0 to handle it on next iteration - iterator.add(subSyncedFolder); - iterator.previous(); - } else if (contentToSyncFound) { - Timber.v("added %s into list of file to scan", subFile.getAbsolutePath()); - fileList.add(subFile); - } - } + if (!isContentToScan) { + return; } - if (contentToSyncFound) { - DbHelper.updateSyncedFolders(mSyncedFolders, this); //@ToDo: maybe do this when all contents will be synced. - final List syncedFileStates = DbHelper.getSyncedFileStatesByFolders(this, - folderIdList); + final List fileList = fileLister.getContentToScan(); + final List folderIdList = fileLister.getSyncedFoldersId(); - if (!syncedFileStates.isEmpty() || !fileList.isEmpty() ) { - final LocalContentScanner scanner= new LocalContentScanner(getApplicationContext(), mAccount, mSyncedFolders); - syncRequests.putAll(scanner.scanContent(fileList, syncedFileStates)); - } + final List syncedFileStates = DbHelper.getSyncedFileStatesByFolders(this, + folderIdList); + + if (!syncedFileStates.isEmpty() || !fileList.isEmpty() ) { + final LocalContentScanner scanner= new LocalContentScanner(getApplicationContext(), mAccount, mSyncedFolders); + syncRequests.putAll(scanner.scanContent(fileList, syncedFileStates)); } } /* end of methods related to device Scanning */ diff --git a/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java b/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java index 3e9410fefe819e42eda70111d630f6d84acc2370..7b5c1814bc6727fe66ba0a35620b43b384895ea3 100644 --- a/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java +++ b/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java @@ -90,6 +90,7 @@ public class FileDiffUtils { */ private static boolean isRemoteSizeSameAsLocalSize(RemoteFile remoteFile, File localFile) { // if local file doesn't exist its size will be 0 - return remoteFile.getLength() == localFile.length(); + // remoteFile.getSize() : getSize() is equal to getLength() except that for folder is also sum the content of the folder! + return remoteFile.getSize() == localFile.length(); } } diff --git a/app/src/test/java/foundation/e/drive/TestUtils.java b/app/src/test/java/foundation/e/drive/TestUtils.java index 1da09d5d35710b3560276f3cff40765546aeb66c..19861db817c02ccc0b891b8837efdc16433f2a7d 100644 --- a/app/src/test/java/foundation/e/drive/TestUtils.java +++ b/app/src/test/java/foundation/e/drive/TestUtils.java @@ -155,6 +155,24 @@ public abstract class TestUtils { } + /** + * Delete a local directory with all its content. + * Adapted from https://www.geeksforgeeks.org/java-program-to-delete-a-directory/ + * @param file + */ + public static void deleteDirectory(File file) + { + for (File subfile : file.listFiles()) { + + if (subfile.isDirectory()) { + deleteDirectory(subfile); + } + subfile.delete(); + } + file.delete(); + } + + public static void initializeWorkmanager(Context context) { final Configuration config = new Configuration.Builder() .setMinimumLoggingLevel(Log.DEBUG) diff --git a/app/src/test/java/foundation/e/drive/contentScanner/LocalFileListerTest.java b/app/src/test/java/foundation/e/drive/contentScanner/LocalFileListerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b0b9c72012013ca1ea218086d7b5ca2d71e4c16a --- /dev/null +++ b/app/src/test/java/foundation/e/drive/contentScanner/LocalFileListerTest.java @@ -0,0 +1,373 @@ +/* + * Copyright © ECORP SAS 2022. + * 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; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.os.Build; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import foundation.e.drive.TestUtils; +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.models.SyncedFolder; + +/** + * @author vincent Bourgmayer + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.O, manifest = Config.NONE) +public class LocalFileListerTest { + + private static final String TEST_FILE_TREE_ROOT_PATH = "../tmp/"; + + private LocalFileLister fileListerUnderTest; + private final Context context; + private final File testRootDirectory; + + public LocalFileListerTest() { + context = ApplicationProvider.getApplicationContext(); + ShadowLog.stream = System.out; + testRootDirectory = new File(TEST_FILE_TREE_ROOT_PATH); + } + + @Before + public void setUp() { + fileListerUnderTest = new LocalFileLister(null); + } + + + @After + public void tearDown() { + SQLiteDatabase.deleteDatabase(context.getDatabasePath(DbHelper.DATABASE_NAME)); + + if (testRootDirectory.exists()) { + TestUtils.deleteDirectory(testRootDirectory); + } + } + + @Test + public void skipSyncedFolder_notMedia_notHidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, false, false); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_notMedia_notHidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, false, true); + Assert.assertFalse(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_notMedia_hidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, true, false); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_notMedia_hidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, true, true); + Assert.assertFalse(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_media_notHidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, false, false); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_media_notHidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, false, true); + Assert.assertFalse("folder shouldn't be skip but it had", fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_media_Hidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, true, false); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_media_Hidden_scannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, true, true); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + private SyncedFolder buildSyncedFolderForSkipSyncedFolderTest(boolean isMedia, boolean isHidden, boolean isLocalScannable) { + final String dirName = isHidden ? ".myFolder/" : "myFolder/"; + return new SyncedFolder("any", TEST_FILE_TREE_ROOT_PATH + dirName, "/remote/path/" + dirName, isLocalScannable, true, true, isMedia); + } + + @Test + public void skipDirectory_lastModifiedSame_noUnsyncedContent_shouldReturnTrue() { + final long lastModified = 123456L; + + final File directory = Mockito.spy(testRootDirectory); + Mockito.when(directory.lastModified()).thenReturn(lastModified); + + final SyncedFolder syncedFolder = new SyncedFolder("any", directory.getAbsolutePath(), "/remote/path/test/", true, true, true, true); + syncedFolder.setId(12); + syncedFolder.setLastModified(lastModified); + + final boolean skip = fileListerUnderTest.skipDirectory(directory, syncedFolder, context); + Assert.assertTrue("directory without new last modified and no content waiting for sync has not been skip", skip); + } + + @Test + public void skipDirectory_lastModifiedSame_unsyncedContent_shouldReturnFalse() { + final long lastModified = 123456L; + + final String dirPath = testRootDirectory.getAbsolutePath(); + + final File directory = Mockito.spy(testRootDirectory); + Mockito.when(directory.lastModified()).thenReturn(lastModified); + + final SyncedFileState dummy = new SyncedFileState(6, "foo.jpg", dirPath + "foo.jpg", "/remote/path/test/foo.jpg", "", 0L, 12, true, 3); + DbHelper.manageSyncedFileStateDB(dummy, "INSERT", context); + + final SyncedFolder syncedFolder = new SyncedFolder("any", dirPath , "/remote/path/test/", true, true, true, true); + syncedFolder.setId(12); + syncedFolder.setLastModified(lastModified); + + final boolean skip = fileListerUnderTest.skipDirectory(directory, syncedFolder, context); + Assert.assertFalse("directory without new last modified but content waiting for sync has been skip", skip); + } + + @Test + public void skipDirectory_lastModifiedChanged_noUnsyncedContent_shouldReturnFalse() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final File directory = Mockito.spy(testRootDirectory); + Mockito.when(directory.lastModified()).thenReturn(654321L); + + final SyncedFolder syncedFolder = new SyncedFolder("any", dirPath, "/remote/path/test/", true, true, true, true); + syncedFolder.setId(12); + syncedFolder.setLastModified(123456L); + + final boolean skip = fileListerUnderTest.skipDirectory(directory, syncedFolder, context); + Assert.assertFalse("directory with new last modified and no content waiting for sync has been skip", skip); + } + + @Test + public void skipDirectory_lastModifiedChanged_unsyncedContent_shouldReturnFalse() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final File directory = Mockito.spy(testRootDirectory); + Mockito.when(directory.lastModified()).thenReturn(654321L); + + final SyncedFolder syncedFolder = new SyncedFolder("any", dirPath, "/remote/path/test/", true, true, true, true); + syncedFolder.setId(12); + syncedFolder.setLastModified(123456L); + + final SyncedFileState dummy = new SyncedFileState(6, "foo.jpg", dirPath + "foo.jpg", "/remote/path/test/foo.jpg", "", 0L, 12, true, 3); + DbHelper.manageSyncedFileStateDB(dummy, "INSERT", context); + + final boolean skip = fileListerUnderTest.skipDirectory(directory, syncedFolder, context); + Assert.assertFalse("directory with new last modified and content waiting for sync has been skip", skip); + } + + //Test about inner class: DirectoryLoader + @Test + public void loadDirectory_dirNotExist_shouldReturnTrue() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final SyncedFolder folder = new SyncedFolder("any", dirPath, "/remote/path/test", true); + + final LocalFileLister.LocalFolderLoader folderLoader = fileListerUnderTest.getFolderLoader(); + + final boolean loadSuccess = folderLoader.load(folder); + Assert.assertTrue("Loading remote folder resulting in 404 should return true", loadSuccess); + Assert.assertTrue(folderLoader.getFolderWrapper().isMissing()); + } + + @Test + public void loadDirectory_dirExist_noContent_shouldReturnTrue() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final SyncedFolder folder = new SyncedFolder("any", dirPath, "/remote/path/test/", true); + + final boolean dirCreationSucceed = testRootDirectory.mkdirs(); + + Assert.assertTrue("Cannot create test folder: " + dirPath, dirCreationSucceed); + + final LocalFileLister.LocalFolderLoader folderLoader = fileListerUnderTest.getFolderLoader(); + + final boolean loadSuccess = folderLoader.load(folder); + final FolderWrapper wrapper = folderLoader.getFolderWrapper(); + Assert.assertTrue("Loading remote folder resulting in 404 should return true", loadSuccess); + Assert.assertFalse("dir shouldExist!", wrapper.isMissing()); + Assert.assertTrue("List of folderWrapper's content expected to be empty but contains: " + wrapper.getContent().size(), wrapper.getContent().isEmpty()); + } + + @Test + public void loadDirectory_dirExist_withSubContent_shouldReturnTrue() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final SyncedFolder folder = new SyncedFolder("any", dirPath, "/remote/path/test/", true); + final boolean dirCreationSucceed = testRootDirectory.mkdirs(); + Assert.assertTrue("cannot create test folder: " + dirPath, dirCreationSucceed); + + + final File subContent = new File(testRootDirectory, "foo.txt"); + try { + Assert.assertTrue("Can't create subFile", subContent.createNewFile()); + } catch (IOException exception) { + exception.printStackTrace(); + Assert.fail("IO Exception: Can't create new file as content of test folder"); + } + + final LocalFileLister.LocalFolderLoader folderLoader = fileListerUnderTest.getFolderLoader(); + + final boolean loadSuccess = folderLoader.load(folder); + final FolderWrapper wrapper = folderLoader.getFolderWrapper(); + Assert.assertTrue("Loading remote folder resulting in 404 should return true", loadSuccess); + Assert.assertFalse("dir shouldExist!", wrapper.isMissing()); + Assert.assertEquals("List of folderWrapper's content size expected to one but was: " + wrapper.getContent().size(), 1, wrapper.getContent().size()); + } + + + /** + * Run on a more realistic files tree to check the whole behaviour with more than + * one directory: + * + * It should contains : + * - one directory with a subdirectory with some files in both + * - one hidden directory with some files + * - one empty directory + * - one "new" directory with subfiles + * + * with correspond syncedFolder in database: + * - except for one, so it will considered as new + * Something like: + * + * ## File tree + * >Folder 1 (unChanged) + * --> .File 1 (should be ignored) + * --> File 2 (should be ignored) + * --> Folder 2 (unsynced dir) + * ----> File 3 (new file should be added) + * ----> .File 4 (should be ignored) + * >Folder 3 (unChanged) + * -->Folder4 (new dir (so unsynced) + * ----> File 5 (new file should be added) + * -->.Folder5 (skip: hidden dir) + * ----> File 6 (skip because directory is hidden) + * + * + * ## SyncedFolder + * SyncedFolder 1 + * SyncedFolder 2 with empty lastModified + * SyncedFolder 3 + * SyncedFolderRemoved which correspond to a missing directory + */ + @Test + public void test_listContentToScan() { + Assert.assertTrue("Preparation, of file Tree and syncedFolder has failed", prepareFakeContent()); + Assert.assertTrue("File listing failed", fileListerUnderTest.listContentToScan(context)); + + final List foldersId = fileListerUnderTest.getSyncedFoldersId(); + Assert.assertEquals("List of folders to scan's id should contains 3 but contained: " + foldersId.size(), 3, foldersId.size()); + Assert.assertTrue(foldersId.contains(2L)); //id of changed folder detected + Assert.assertTrue(foldersId.contains(5L)); //id of new folder detected + Assert.assertTrue(foldersId.contains(4L)); //id of Removed Folder detected + final List contentListed = fileListerUnderTest.getContentToScan(); + Assert.assertEquals("ContentListed should have 2 files but contained: " + contentListed.size(), 2, contentListed.size()); + } + + private boolean prepareFakeContent() { + /* + * Generate folders + */ + if (!testRootDirectory.mkdirs()) return false; + + final File folder1 = new File(testRootDirectory, "folder1/"); + if (!folder1.mkdirs()) return false; + final File folder2 = new File(folder1, "folder2/"); + if (!folder2.mkdirs()) return false; + + final File folder3 = new File(testRootDirectory, "folder3/"); + if (!folder3.mkdirs()) return false; + final File folder4 = new File(folder3, "folder4/"); + if (!folder4.mkdirs()) return false; + final File folder5 = new File(folder3, ".folder5/"); + if (!folder5.mkdirs()) return false; + + + /* + * Generate files + * /!\ must be done before syncedFolders generation + * otherwise lastModified value of directory will change due to file creation + */ + try { + final File file1 = new File(folder1, ".file1.txt"); + if (!file1.createNewFile()) return false; + + final File file2 = new File(folder1, "file2.txt"); + if (!file2.createNewFile()) return false; + + final File file3 = new File(folder2, "file3.txt"); + if (!file3.createNewFile()) return false; + + final File file4 = new File(folder2, ".file4.txt"); + if (!file4.createNewFile()) return false; + + final File file5 = new File(folder4, "file5.txt"); + if (!file5.createNewFile()) return false; + + final File file6 = new File(folder5, "file6.txt"); + if (!file6.createNewFile()) return false; + } catch (IOException exception) { + exception.printStackTrace(); + return false; + } + + /* + * Generate SyncedFolders + */ + final SyncedFolder syncedFolder1 = new SyncedFolder("any", folder1.getAbsolutePath(), "/remote/path/myFolder1/", true); + syncedFolder1.setLastModified(folder1.lastModified()); + syncedFolder1.setId((int) DbHelper.insertSyncedFolder(syncedFolder1, context)); + final SyncedFolder syncedFolder2 = new SyncedFolder("any", folder2.getAbsolutePath(), "/remote/path/myFolder2/", true); + syncedFolder2.setLastModified(0L); + syncedFolder2.setId((int) DbHelper.insertSyncedFolder(syncedFolder2, context)); + final SyncedFolder syncedFolder3 = new SyncedFolder("any", folder3.getAbsolutePath(), "/remote/path/myFolder3/", true); + syncedFolder3.setLastModified(folder3.lastModified()); + syncedFolder3.setId((int) DbHelper.insertSyncedFolder(syncedFolder3, context)); + final SyncedFolder syncedFolderRemoved = new SyncedFolder("any", testRootDirectory.getAbsolutePath() + "lambda/", "/remote/lambda", true); + syncedFolderRemoved.setLastModified(987654321L); + syncedFolderRemoved.setLastEtag("123456789"); + syncedFolderRemoved.setId((int) DbHelper.insertSyncedFolder(syncedFolderRemoved, context)); + + final ArrayList syncedFolders = new ArrayList<>(); + syncedFolders.add(syncedFolder1); + syncedFolders.add(syncedFolder2); + syncedFolders.add(syncedFolder3); + syncedFolders.add(syncedFolderRemoved); + fileListerUnderTest = new LocalFileLister(syncedFolders); + + return true; + } +} diff --git a/app/src/test/java/foundation/e/drive/contentScanner/RemoteContentScannerTest.java b/app/src/test/java/foundation/e/drive/contentScanner/RemoteContentScannerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2c2270b0601d1121c98d66886bcbcc10d3172666 --- /dev/null +++ b/app/src/test/java/foundation/e/drive/contentScanner/RemoteContentScannerTest.java @@ -0,0 +1,259 @@ +/* + * Copyright © ECORP SAS 2022. + * 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; + +import static org.junit.Assert.assertEquals; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; +import android.os.Build; + +import androidx.test.core.app.ApplicationProvider; + +import com.google.common.io.Files; +import com.owncloud.android.lib.resources.files.model.RemoteFile; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import foundation.e.drive.TestUtils; +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncRequest; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.models.SyncedFolder; + + +/** + * @author vincent Bourgmayergit status + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.O, manifest = Config.NONE) +public class RemoteContentScannerTest { + + private final Context context; + private final Account account; + private RemoteContentScanner scannerUnderTest; + + public RemoteContentScannerTest(){ + context = ApplicationProvider.getApplicationContext(); + ShadowLog.stream = System.out; + TestUtils.loadServerCredentials(); + TestUtils.prepareValidAccount(AccountManager.get(context)); + account = TestUtils.getValidAccount(); + + } + + @Before + public void setUp() { + prepareDB(); + List directoryList = new ArrayList<>(); + directoryList.add(new SyncedFolder("Photos", "local/path/", "remote/path/", true, true, true, true)); + scannerUnderTest = new RemoteContentScanner(context, account, directoryList); + } + + private void prepareDB(){ + + final File usedDB = context.getDatabasePath(DbHelper.DATABASE_NAME); + System.out.println(usedDB.getAbsolutePath()); + + final File dbToCopy = new File(getClass().getResource("/databases/database_71SyncedFile.db").getFile()); + Assert.assertTrue("DB files not found", dbToCopy.exists()); + + try { + Files.copy(dbToCopy, usedDB); + } catch (IOException e) { + e.printStackTrace(); + Assert.fail("Impossible to copy db from resources"); + } + + System.out.println(dbToCopy); + assertEquals("There isn't 10 folder in DB as expected", 10, DbHelper.getSyncedFolderList(context, true).size()); + } + + /** + * Create the syncedFolder instance + * @param name the name of the folder + * @return SyncedFolder instance + */ + private SyncedFolder createSyncedFolder(String name) { + return new SyncedFolder(name, + TestUtils.TEST_LOCAL_ROOT_FOLDER_PATH+name+"/", + TestUtils.TEST_REMOTE_ROOT_FOLDER_PATH+name+"/", + true, true, true, true); + } + + + /** + * This is a base, that check that it works as expected in a normal situation + * todo: decline this test to cover unexpected cases! + * i.e: what if dbContent is empty + */ + @Test + public void scanEmptyCloudContent_generateLocalDeletionRequest() { + final List cloudContent = new ArrayList<>(); + Assert.assertTrue("Fake list of cloud Content is not empty", cloudContent.isEmpty()); + + final List dbContent = new ArrayList<>(); + dbContent.add(new SyncedFileState(5, "toto", "local/path/toto", "remote/path/toto", "5555", 22222, 2, true, 3)); + dbContent.add(new SyncedFileState(3, "titi", "local/path/titi", "remote/path/titi", "5556", 22322, 2, true, 3)); + dbContent.add(new SyncedFileState(2, "tata", "local/path/tata", "remote/path/tata", "5557", 22232, 2, true, 3)); + + Assert.assertFalse("List of SyncedFileState is empty", dbContent.isEmpty()); + + final HashMap scanResult = scannerUnderTest.scanContent(cloudContent, dbContent); + + /* If the list of SyncedFileState is not empty then all the local file must be deleted... + * In fact, the list of SyncedFileStates that is given as parameter to scanContent, should only contains content + * that has been identified has potentially changed, new or removed + * However, the issue is that...the scanner remove the file by itself, which should be out of its scope + * TODO: find how to assert the behaviour in current state ? */ + + Assert.assertEquals("scanResult's size doesn't match the expected result", 3, scanResult.size()); + + for (SyncRequest request : scanResult.values()) { + Assert.assertEquals("SyncRequest's type should be LOCAL_DELETE but is "+request.getOperationType(), SyncRequest.Type.LOCAL_DELETE, request.getOperationType()); + } + } + + private List generateListOfNewRemoteFile() { + final ArrayList result = new ArrayList<>(); + + final RemoteFile newFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(newFile1.getRemotePath()).thenReturn("remote/path/tutu"); + Mockito.when(newFile1.getEtag()).thenReturn("33323"); + + + final RemoteFile newFile2 = Mockito.mock(RemoteFile.class); + Mockito.when(newFile2.getRemotePath()).thenReturn("remote/path/tete"); + Mockito.when(newFile2.getEtag()).thenReturn("33423"); + + + result.add(newFile1); + result.add(newFile2); + + return result; + } + + + /** + * This is a base, that check that it works as expected in a normal situation + * todo: decline this test to cover unexpected cases! + * i.e: what if dbContent is empty + */ + @Test + public void scanUpdatedRemoteContent_generateDownloadRequest() { + final List cloudContent = new ArrayList<>(); + + final RemoteFile updatedFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(updatedFile1.getRemotePath()).thenReturn("remote/path/toto"); + Mockito.when(updatedFile1.getEtag()).thenReturn("33423"); + Mockito.when(updatedFile1.getSize()).thenReturn(12l); + + final RemoteFile updatedFile2 = Mockito.mock(RemoteFile.class); + Mockito.when(updatedFile2.getRemotePath()).thenReturn("remote/path/titi"); + Mockito.when(updatedFile2.getEtag()).thenReturn("34523"); + Mockito.when(updatedFile2.getSize()).thenReturn(14l); + + final RemoteFile uptodateFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(uptodateFile1.getRemotePath()).thenReturn("remote/path/tata"); + Mockito.when(uptodateFile1.getEtag()).thenReturn("5557"); + + cloudContent.add(uptodateFile1); + cloudContent.add(updatedFile1); + cloudContent.add(updatedFile2); + + Assert.assertEquals("Expected cloudContent size: 3 but got : " + cloudContent.size(), 3, cloudContent.size()); + + + final List dbContent = new ArrayList<>(); + dbContent.add(new SyncedFileState(5, "toto", "local/path/toto", "remote/path/toto", "5555", 22222, 2, true, 3)); + dbContent.add(new SyncedFileState(3, "titi", "local/path/titi", "remote/path/titi", "5556", 22322, 2, true, 3)); + dbContent.add(new SyncedFileState(2, "tata", "local/path/tata", "remote/path/tata", "5557", 22232, 2, true, 3)); + + Assert.assertFalse("List of SyncedFileState is empty", dbContent.isEmpty()); + + final HashMap scanResult = scannerUnderTest.scanContent(cloudContent, dbContent); + Assert.assertEquals("scanResult's size doesn't match the expected result", 2, scanResult.size()); + + for (SyncRequest request : scanResult.values()) { + Assert.assertEquals("SyncRequest's type should be DOWNLOAD but is "+request.getOperationType(), SyncRequest.Type.DOWNLOAD, request.getOperationType()); + } + } + + /** + * This is a base, that check that it works as expected in a normal situation + * todo: decline this test to cover unexpected cases! + * i.e: what if dbContent is empty + */ + @Test + public void scanNewRemoteContent_generateDownloadRequest() { + final List cloudContent = generateListOfNewRemoteFile(); + + final RemoteFile updatedFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(updatedFile1.getRemotePath()).thenReturn("remote/path/toto"); + Mockito.when(updatedFile1.getEtag()).thenReturn("5555"); + + final RemoteFile updatedFile2 = Mockito.mock(RemoteFile.class); + Mockito.when(updatedFile2.getRemotePath()).thenReturn("remote/path/titi"); + Mockito.when(updatedFile2.getEtag()).thenReturn("5556"); + + final RemoteFile uptodateFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(uptodateFile1.getRemotePath()).thenReturn("remote/path/tata"); + Mockito.when(uptodateFile1.getEtag()).thenReturn("5557"); + + cloudContent.add(uptodateFile1); + cloudContent.add(updatedFile1); + cloudContent.add(updatedFile2); + + Assert.assertEquals("Expected cloudContent size: 5 but got : " + cloudContent.size(), 5, cloudContent.size()); + + + final List dbContent = new ArrayList<>(); + dbContent.add(new SyncedFileState(5, "toto", "local/path/toto", "remote/path/toto", "5555", 22222, 2, true, 3)); + dbContent.add(new SyncedFileState(3, "titi", "local/path/titi", "remote/path/titi", "5556", 22322, 2, true, 3)); + dbContent.add(new SyncedFileState(2, "tata", "local/path/tata", "remote/path/tata", "5557", 22232, 2, true, 3)); + + Assert.assertFalse("List of SyncedFileState is empty", dbContent.isEmpty()); + + final HashMap scanResult = scannerUnderTest.scanContent(cloudContent, dbContent); + Assert.assertEquals("scanResult's size doesn't match the expected result", 2, scanResult.size()); + + for (SyncRequest request : scanResult.values()) { + Assert.assertEquals("SyncRequest's type should be DOWNLOAD but is "+request.getOperationType(), SyncRequest.Type.DOWNLOAD, request.getOperationType()); + } + } + + + + + @Test + public void whenDoWeGetUnexpectedFileDeletion() { + final List cloudContent = new ArrayList<>(); + final List dbContent = new ArrayList<>(); + + final HashMap scanResult = scannerUnderTest.scanContent(cloudContent, dbContent); + Assert.assertEquals("scanResult's size doesn't match the expected result", 0, scanResult.size()); + + for (SyncRequest request : scanResult.values()) { + System.out.println(request.getOperationType() + "for " +request.getSyncedFileState().getRemotePath()); + } + } +} \ No newline at end of file diff --git a/app/src/test/java/foundation/e/drive/contentScanner/RemoteFileListerTest.java b/app/src/test/java/foundation/e/drive/contentScanner/RemoteFileListerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e7c86ee390ecdc11d1d1c68eb00ef920b3c4fa95 --- /dev/null +++ b/app/src/test/java/foundation/e/drive/contentScanner/RemoteFileListerTest.java @@ -0,0 +1,520 @@ +/* + * Copyright © ECORP SAS 2022. + * 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; + +import static org.mockito.Mockito.when; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.os.Build; + +import androidx.test.core.app.ApplicationProvider; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.resources.files.model.RemoteFile; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.internal.configuration.injection.MockInjection; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import foundation.e.drive.TestUtils; +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.models.SyncedFolder; +import foundation.e.drive.utils.DavClientProvider; + +/** + * @author vincent Bourgmayer + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.O, manifest = Config.NONE) +public class RemoteFileListerTest { + private static final String DIR_NO_SKIP_EXPECTED = "directory shouldn't have been skipped"; + private static final String DIR_SKIP_EXPECTED = "directory should have been skipped"; + private static final String TEST_FILE_TREE_ROOT_PATH = "/remote/"; + + private RemoteFileLister fileListerUnderTest; + private final Account account; + private final OwnCloudClient ocClient; + private final Context context; + + public RemoteFileListerTest() { + context = ApplicationProvider.getApplicationContext(); + ShadowLog.stream = System.out; + TestUtils.loadServerCredentials(); + TestUtils.prepareValidAccount(AccountManager.get(context)); + account = TestUtils.getValidAccount(); + ocClient = DavClientProvider.getInstance().getClientInstance(account, context); + } + + @Before + public void setUp() { + fileListerUnderTest = new RemoteFileLister(null, ocClient); + SQLiteDatabase.deleteDatabase(context.getDatabasePath(DbHelper.DATABASE_NAME)); + } + + @Test + public void skipSyncedFolder_notMedia_notHidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, false, false); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("Not media, not hidden, not remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_notMedia_notHidden_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, false, true); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertFalse("Not media, not hidden, remotly scannable " + DIR_NO_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_notMedia_hidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, true, false); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("Not media, hidden, not remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_notMedia_hidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, true, true); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertFalse("Not media, hidden, remotly scannable " + DIR_NO_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_media_notHidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, false, false); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("media, not hidden, not remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_media_notHidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, false, true); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertFalse("media, not hidden, remotly scannable " + DIR_NO_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_media_hidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, true, false); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("media, hidden, not remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_media_hidden_scannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, true, true); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("media, hidden, remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + private SyncedFolder buildSyncedFolderForSkipSyncedFolderTest(boolean isMedia, boolean isHidden, boolean isRemoteScannable) { + final String dirName = isHidden ? ".myFolder/" : "myFolder/"; + return new SyncedFolder("any", "/local/path/" + dirName, TEST_FILE_TREE_ROOT_PATH + dirName, true, isRemoteScannable, true, isMedia); + } + + + @Test + public void skipFile_hidden_shouldReturnTrue() { + final String hiddenRemoteFilePath = TEST_FILE_TREE_ROOT_PATH + ".foo.png"; + final RemoteFile file = Mockito.mock(RemoteFile.class); + when(file.getRemotePath()).thenReturn(hiddenRemoteFilePath); + + final boolean skipFile = fileListerUnderTest.skipFile(file); + Assert.assertTrue("Hidden remoteFile (" + hiddenRemoteFilePath + ") should have been skipped", skipFile); + } + + @Test + public void skipFile_notHidden_shouldReturnFalse() { + final String notHiddenRemoteFilePath = TEST_FILE_TREE_ROOT_PATH + "foo.png"; + final RemoteFile file = Mockito.mock(RemoteFile.class); + when(file.getRemotePath()).thenReturn(notHiddenRemoteFilePath); + + final boolean skipFile = fileListerUnderTest.skipFile(file); + Assert.assertFalse("Not hidden remoteFile (" + notHiddenRemoteFilePath + ") should not have been skipped", skipFile); + } + + @Test + public void skipFile_emptyPath_shouldReturnTrue() { + final String emptyPath = ""; + final RemoteFile file = Mockito.mock(RemoteFile.class); + when(file.getRemotePath()).thenReturn(emptyPath); + + final boolean skipFile = fileListerUnderTest.skipFile(file); + Assert.assertTrue("RemoteFile with empty path (" + emptyPath + ") should have been skipped", skipFile); + } + + @Test + @Ignore + public void skipFile_invalidPath_shouldReturnTrue() { + final String invalidPath = "/invalid/path/"; + /*in this case: No file, just a directory + the current implementation of CommonUtils.getFileName(String path) + will return "path" while empty string should be expected. + todo: fix CommonUtils.getFileName(String path) */ + final RemoteFile file = Mockito.mock(RemoteFile.class); + when(file.getRemotePath()).thenReturn(invalidPath); + + final boolean skipFile = fileListerUnderTest.skipFile(file); + Assert.assertTrue("RemoteFile with empty path (" + invalidPath + ") should have been skipped", skipFile); + } + + @Test + public void skipDirectory_etagChanged_noUnsyncedContent_shouldReturnFalse() { + final RemoteFile remoteFile = Mockito.mock(RemoteFile.class); + when(remoteFile.getEtag()).thenReturn("6666bbbb"); + + final SyncedFolder syncedFolder = new SyncedFolder("", "local", TEST_FILE_TREE_ROOT_PATH, true); + syncedFolder.setId(12); + syncedFolder.setLastEtag("5555aaaa"); + + final boolean skip = fileListerUnderTest.skipDirectory(remoteFile, syncedFolder, context); + + Assert.assertFalse("Remote directory with new etag should not have been skipped but it had", skip); + } + + @Test + public void skipDirectory_etagSame_noUnsyncedContent_shouldReturnTrue() { + final RemoteFile remoteFile = Mockito.mock(RemoteFile.class); + when(remoteFile.getEtag()).thenReturn("5555aaaa"); + + final SyncedFolder syncedFolder = new SyncedFolder("", "local", TEST_FILE_TREE_ROOT_PATH, true); + syncedFolder.setId(12); + syncedFolder.setLastEtag("5555aaaa"); + + final boolean skip = fileListerUnderTest.skipDirectory(remoteFile, syncedFolder, context); + + Assert.assertTrue("Remote directory with no new etag should have been skipped but had not", skip); + } + + @Test + public void skipDirectory_etagSame_contentToSync_shouldReturnFalse() { + final RemoteFile remoteFile = Mockito.mock(RemoteFile.class); + when(remoteFile.getEtag()).thenReturn("5555aaaa"); + + final SyncedFolder syncedFolder = new SyncedFolder("", "local", TEST_FILE_TREE_ROOT_PATH, true); + syncedFolder.setId(12); + syncedFolder.setLastEtag("5555aaaa"); + + final SyncedFileState dummy = new SyncedFileState(6, "foo.jpg", "local/foo.jpg", TEST_FILE_TREE_ROOT_PATH + "foo.jpg", "7777", 0L, 12, true, 3); + DbHelper.manageSyncedFileStateDB(dummy, "INSERT", context); + + final boolean skip = fileListerUnderTest.skipDirectory(remoteFile, syncedFolder, context); + Assert.assertFalse("Remote directory with no new etag but there is content to sync from DB: should not have been skipped but it had", skip); + } + + @Test + public void skipDirectory_etagChanged_contentToSync_shouldReturnFalse() { + final RemoteFile remoteFile = Mockito.mock(RemoteFile.class); + when(remoteFile.getEtag()).thenReturn("6666bbbb"); + + final SyncedFolder syncedFolder = new SyncedFolder("", "local", TEST_FILE_TREE_ROOT_PATH, true); + syncedFolder.setId(12); + syncedFolder.setLastEtag("5555aaaa"); + + final SyncedFileState dummy = new SyncedFileState(6, "foo.jpg", "local/foo.jpg", TEST_FILE_TREE_ROOT_PATH + "foo.jpg", "7777", 0L, 12, true, 3); + DbHelper.manageSyncedFileStateDB(dummy, "INSERT", context); + + final boolean skip = fileListerUnderTest.skipDirectory(remoteFile, syncedFolder, context); + Assert.assertFalse("Remote directory with new etag and there is content to sync from DB: should not have been skipped but it had", skip); + } + + //Test about inner class: DirectoryLoader + @Test + public void loadDirectory_serverResponse404_shouldReturnTrue() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(404); + when(fakeResult.isSuccess()).thenReturn(false); + + final RemoteFileLister.RemoteFolderLoader mockLoaderSpied = Mockito.spy(fileListerUnderTest.getFolderLoader()); + when(mockLoaderSpied.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = mockLoaderSpied.load(folder); + Assert.assertTrue("Loading remote folder resulting in 404 should return true", loadSuccess); + } + + @Test + public void loadDirectory_serverResponse405_shouldReturnFalse() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(405); + when(fakeResult.isSuccess()).thenReturn(false); + + final RemoteFileLister.RemoteFolderLoader mockLoaderSpied = Mockito.spy(fileListerUnderTest.getFolderLoader()); + when(mockLoaderSpied.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = mockLoaderSpied.load(folder); + Assert.assertFalse("Loading remote folder resulting in 405 should return false", loadSuccess); + } + + @Test + public void loadDirectory_successAndErrorCode_shouldReturnTrue() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(405); + when(fakeResult.isSuccess()).thenReturn(true); + + final ArrayList fakeData = new ArrayList<>(); + final RemoteFile mockRemoteFile = Mockito.mock(RemoteFile.class); + fakeData.add(mockRemoteFile); + when(fakeResult.getData()).thenReturn(fakeData); + + final RemoteFileLister.RemoteFolderLoader spiedLoaderUnderTest = Mockito.spy(fileListerUnderTest.getFolderLoader()); + when(spiedLoaderUnderTest.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = spiedLoaderUnderTest.load(folder); + Assert.assertTrue("Loading remote folder with success despite error code (405) should return true", loadSuccess); + Assert.assertEquals("Loaded Folder is not the one expected despite loading success", mockRemoteFile, spiedLoaderUnderTest.getFolderWrapper().getFolder()); + } + + @Test + public void loadDirectory_successAndRemoteContent_shouldReturnTrue() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(405); + when(fakeResult.isSuccess()).thenReturn(true); + + final ArrayList fakeData = new ArrayList<>(); + final RemoteFile mockDirRemoteFile = Mockito.mock(RemoteFile.class); + fakeData.add(mockDirRemoteFile); + final RemoteFile mockContentRemoteFile1 = Mockito.mock(RemoteFile.class); + final RemoteFile mockContentRemoteFile2 = Mockito.mock(RemoteFile.class); + fakeData.add(mockContentRemoteFile1); + fakeData.add(mockContentRemoteFile2); + when(fakeResult.getData()).thenReturn(fakeData); + + final RemoteFileLister.RemoteFolderLoader spiedLoaderUnderTest = Mockito.spy(fileListerUnderTest.getFolderLoader()); + when(spiedLoaderUnderTest.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = spiedLoaderUnderTest.load(folder); + Assert.assertTrue("Loading remote folder with success despite error code (405) should return true", loadSuccess); + Assert.assertEquals("Loaded Folder is not the one expected despite loading success", mockDirRemoteFile, spiedLoaderUnderTest.getFolderWrapper().getFolder()); + Assert.assertEquals("mockDirRemoteFile should have been the directory loader but is not", 2, spiedLoaderUnderTest.getFolderWrapper().getContent().size()); + Assert.assertTrue("mockContentRemoteFile1 should have been in the result of loading but is not", spiedLoaderUnderTest.getFolderWrapper().getContent().contains(mockContentRemoteFile1)); + Assert.assertTrue("mockContentRemoteFile2 should have been in the result of loading but is not", spiedLoaderUnderTest.getFolderWrapper().getContent().contains(mockContentRemoteFile2)); + } + + @Test + public void loadDirectory_successButNoData_shouldReturnFalse() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(204); + when(fakeResult.isSuccess()).thenReturn(true); + + final ArrayList fakeData = new ArrayList<>(); + when(fakeResult.getData()).thenReturn(fakeData); + + final RemoteFileLister.RemoteFolderLoader mockLoaderSpied = Mockito.spy(fileListerUnderTest.getFolderLoader()); + when(mockLoaderSpied.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = mockLoaderSpied.load(folder); + Assert.assertFalse("Loading remote folder resulting in 204 but without any RemoteFile should return false", loadSuccess); + } + + + /** + * Test the whole behaviour of localFileLister with a fake file's tree + * + * ## File tree + * >Folder 1 (unChanged) + * --> File 1 (should be ignored) + * --> Folder 2 (unsynced dir) + * ----> File 2 (new file should be added) + * ----> .File 3 (should be ignored) + * >Folder 3 (unChanged) + * -->Folder4 (skip because folder 3 unchanged) + * ----> File 4 (skip because folder 3 unchanged) + * -->.Folder5 (skip: hidden dir) + * ----> File 5 (skip because directory is hidden) + * + * There is also the missing remote folder, which is expected to be in the result's folder's id + */ + @Test + public void test_listContentToScan() { + prepareFakeContent(); + Assert.assertTrue("File listing failed", fileListerUnderTest.listContentToScan(context)); + + final List foldersId = fileListerUnderTest.getSyncedFoldersId(); + Assert.assertEquals("List of folders to scan's id should contains 2 but contained: " + foldersId.size(), 2, foldersId.size()); + + final List contentListed = fileListerUnderTest.getContentToScan(); + Assert.assertEquals("ContentListed should have 1 files but contained: " + contentListed.size(), 1, contentListed.size()); + } + + /** + * Generate fake file tree & mock object for a full test of RemoteFileLister + */ + private void prepareFakeContent() { + /* + * Generate folders + */ + final String folder1Path = TEST_FILE_TREE_ROOT_PATH + "folder1/"; + final RemoteFile folder1 = Mockito.mock(RemoteFile.class); //unchanged + when(folder1.getRemotePath()).thenReturn(folder1Path); + when(folder1.getEtag()).thenReturn("123456789"); + when(folder1.getMimeType()).thenReturn("DIR"); + + final String folder2Path = folder1Path + "folder2/"; + final RemoteFile folder2 = Mockito.mock(RemoteFile.class); //updated one + when(folder2.getRemotePath()).thenReturn(folder2Path); + when(folder2.getEtag()).thenReturn("234567891"); + when(folder2.getMimeType()).thenReturn("DIR"); + + final String folder3Path = TEST_FILE_TREE_ROOT_PATH + "folder3/"; + final RemoteFile folder3 = Mockito.mock(RemoteFile.class); //unchanged + when(folder3.getRemotePath()).thenReturn(folder3Path); + when(folder3.getEtag()).thenReturn("345678912"); + when(folder3.getMimeType()).thenReturn("DIR"); + + final String folder4Path = folder3Path + "folder4/"; + final RemoteFile folder4 = Mockito.mock(RemoteFile.class); //new one + when(folder4.getRemotePath()).thenReturn(folder4Path); + when(folder4.getEtag()).thenReturn("456789123"); + when(folder4.getMimeType()).thenReturn("DIR"); + + final String folder5Path = folder3.getRemotePath() + ".folder5/"; + final RemoteFile folder5 = Mockito.mock(RemoteFile.class); //hidden one + when(folder5.getRemotePath()).thenReturn(folder5Path); + when(folder5.getEtag()).thenReturn("567891234"); + when(folder5.getMimeType()).thenReturn("DIR"); + + /* + * Generate files + */ + final RemoteFile file1 = Mockito.mock(RemoteFile.class); //ignored because dir unchanged + when(file1.getRemotePath()).thenReturn(folder1Path + "file1.png"); + when(file1.getMimeType()).thenReturn("any"); + final RemoteFile file2 = Mockito.mock(RemoteFile.class); //new file one, should be added + when(file2.getRemotePath()).thenReturn(folder2Path + "file2.png"); + when(file2.getMimeType()).thenReturn("any"); + final RemoteFile file3 = Mockito.mock(RemoteFile.class); //hidden one: ignored + when(file3.getRemotePath()).thenReturn(folder2Path + ".file3.png"); + when(file3.getMimeType()).thenReturn("any"); + final RemoteFile file4 = Mockito.mock(RemoteFile.class); //new one should be added + when(file4.getRemotePath()).thenReturn(folder4Path + "file4.png"); + when(file4.getMimeType()).thenReturn("any"); + final RemoteFile file5 = Mockito.mock(RemoteFile.class); //ignored because dir is hidden + when(file5.getRemotePath()).thenReturn(folder5Path + "file5.png"); + when(file5.getMimeType()).thenReturn("any"); + + /* + * Generate SyncedFolders + */ + final SyncedFolder syncedFolder1 = new SyncedFolder("any", "local/folder1/", folder1Path, true); + syncedFolder1.setLastEtag(folder1.getEtag()); + syncedFolder1.setId((int) DbHelper.insertSyncedFolder(syncedFolder1, context)); + + final SyncedFolder syncedFolder2 = new SyncedFolder("any", "local/folder1/folder2/", folder2Path, true); + syncedFolder2.setLastEtag(""); + syncedFolder2.setId((int) DbHelper.insertSyncedFolder(syncedFolder2, context)); + + final SyncedFolder syncedFolder3 = new SyncedFolder("any","local/folder3/", folder3Path, true); + syncedFolder3.setLastEtag(folder3.getEtag()); + syncedFolder3.setId((int) DbHelper.insertSyncedFolder(syncedFolder3, context)); + + final SyncedFolder syncedFolderRemoved = new SyncedFolder("any", "local/missingFolder/", TEST_FILE_TREE_ROOT_PATH + "lambda/", true); + syncedFolderRemoved.setLastModified(987654321L); + syncedFolderRemoved.setLastEtag("123456789"); + syncedFolderRemoved.setId((int) DbHelper.insertSyncedFolder(syncedFolderRemoved, context)); + + final ArrayList syncedFolders = new ArrayList<>(); + syncedFolders.add(syncedFolder1); + syncedFolders.add(syncedFolder2); + syncedFolders.add(syncedFolder3); + syncedFolders.add(syncedFolderRemoved); + + fileListerUnderTest = Mockito.spy(new RemoteFileLister(syncedFolders, ocClient)); + + + /* Mock the method depending to network and cloud */ + final RemoteFileLister.RemoteFolderLoader spiedDirLoader = Mockito.spy(fileListerUnderTest.getFolderLoader()); + + final RemoteOperationResult folder1Result = Mockito.mock(RemoteOperationResult.class); + when(folder1Result.getHttpCode()).thenReturn(207); + when(folder1Result.isSuccess()).thenReturn(true); + final ArrayList folder1Data = new ArrayList<>(); + folder1Data.add(folder1); + folder1Data.add(file1); + folder1Data.add(folder2); + when(folder1Result.getData()).thenReturn(folder1Data); + when(spiedDirLoader.readRemoteFolder(syncedFolder1.getRemoteFolder(), ocClient)).thenReturn(folder1Result); + + final RemoteOperationResult folder2Result = Mockito.mock(RemoteOperationResult.class); + when(folder2Result.getHttpCode()).thenReturn(207); + when(folder2Result.isSuccess()).thenReturn(true); + final ArrayList folder2Data = new ArrayList<>(); + folder2Data.add(folder2); + folder2Data.add(file2); + folder2Data.add(file3); + when(folder2Result.getData()).thenReturn(folder2Data); + when(spiedDirLoader.readRemoteFolder(syncedFolder2.getRemoteFolder(), ocClient)).thenReturn(folder2Result); + + final RemoteOperationResult folder3Result = Mockito.mock(RemoteOperationResult.class); + when(folder3Result.getHttpCode()).thenReturn(207); + when(folder3Result.isSuccess()).thenReturn(true); + final ArrayList folder3Data = new ArrayList<>(); + folder3Data.add(folder3); + folder3Data.add(folder4); + folder3Data.add(folder5); + when(folder3Result.getData()).thenReturn(folder3Data); + when(spiedDirLoader.readRemoteFolder(syncedFolder3.getRemoteFolder(), ocClient)).thenReturn(folder3Result); + + + final RemoteOperationResult folder4Result = Mockito.mock(RemoteOperationResult.class); + when(folder4Result.getHttpCode()).thenReturn(207); + when(folder4Result.isSuccess()).thenReturn(true); + final ArrayList folder4Data = new ArrayList<>(); + folder4Data.add(folder4); + folder4Data.add(file4); + when(folder4Result.getData()).thenReturn(folder4Data); + when(spiedDirLoader.readRemoteFolder(folder4.getRemotePath(), ocClient)).thenReturn(folder4Result); + + final RemoteOperationResult folder5Result = Mockito.mock(RemoteOperationResult.class); + when(folder5Result.getHttpCode()).thenReturn(207); + when(folder5Result.isSuccess()).thenReturn(true); + final ArrayList folder5Data = new ArrayList<>(); + folder5Data.add(folder4); + folder5Data.add(file4); + when(folder5Result.getData()).thenReturn(folder5Data); + when(spiedDirLoader.readRemoteFolder(folder5.getRemotePath(), ocClient)).thenReturn(folder5Result); + + final RemoteOperationResult missingFolderResult = Mockito.mock(RemoteOperationResult.class); + when(missingFolderResult.getHttpCode()).thenReturn(404); + when(missingFolderResult.isSuccess()).thenReturn(false); + when(spiedDirLoader.readRemoteFolder(syncedFolderRemoved.getRemoteFolder(), ocClient)).thenReturn(missingFolderResult); + + when(fileListerUnderTest.getFolderLoader()).thenReturn(spiedDirLoader); + } +} diff --git a/app/src/test/resources/databases/database_71SyncedFile.db b/app/src/test/resources/databases/database_71SyncedFile.db new file mode 100644 index 0000000000000000000000000000000000000000..fd1a23b650a82d14e6a103169cd3da1ae95d0cd2 Binary files /dev/null and b/app/src/test/resources/databases/database_71SyncedFile.db differ