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

Commit 2288477c authored by Vincent Bourgmayer's avatar Vincent Bourgmayer
Browse files

Merge branch '762-add-FileLister-classes' into 'v1-oreo'

Implement new classes to handle listing of existing content

See merge request !180
parents 3bfba645 cf2becc0
Loading
Loading
Loading
Loading
Loading
+221 −0
Original line number Diff line number Diff line
/*
 * 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 com.owncloud.android.lib.resources.files.FileUtils;

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 <T> {

    /**
     * Check if the given syncedFolder can be skip
     * @param syncedFolder The syncedFolder instance
     * @return true if we can skip
     */
    protected abstract boolean skipSyncedFolder(@NonNull SyncedFolder syncedFolder);

    /**
     * Check if the file can be skip or if it should be added to the list to scan
     * @param file file Object to check
     * @return true if file must be ignored
     */
    protected abstract boolean skipFile(@NonNull T file);

    /**
     * If the given folder has not changed, compared to database's data, skip it
     * @param currentDirectory The real latest state of the directory
     * @param syncedFolder The last known state of the directory
     * @param context Context used to access database, to look for unsynced file for the given directory
     * @return true if we can skip this directory
     */
    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<T> createFolderLoader();
    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<T>
     * @param <F> RemoteFile or File
     */
    /* package */ interface FolderLoader<F> {
        FolderWrapper<F> getFolderWrapper();
        boolean load(@NonNull SyncedFolder folder);
    }

    protected final List<SyncedFolder> folders;
    private final List<T> 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<SyncedFolder> folders) {
        this.folders = folders;
        this.contentToScan = new ArrayList<>();
    }

    /**
     * Perform the listing of files
     * @param context
     * @return
     */
    public boolean listContentToScan(@NonNull Context context) {
        final ListIterator<SyncedFolder> 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 folder 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 folder, @NonNull ListIterator<SyncedFolder> iterator, @NonNull Context context) {

        final FolderLoader<T> dirLoader = createFolderLoader();
        if (skipSyncedFolder(folder)
                || !isSyncedFolderInDb(folder, context)
                || !dirLoader.load(folder)) {  // I try to get the real directory  (File or RemoteFile).
            iterator.remove();
            return false;
        }

        final FolderWrapper<T> currentDirectory = dirLoader.getFolderWrapper();

        if (currentDirectory.isMissing()) {
            return true;
        }

        if (skipDirectory(currentDirectory.getFolder(), folder, context)) {
            iterator.remove();
            folder.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 {
            folder.setToSync(true);
        }
        updateSyncedFolder(folder, currentDirectory.getFolder());

        //todo: look where to put in subclasses : syncedFolder.setLastEtag(directory.getEtag()).setToSync(true);

        final List<T> dirContent = currentDirectory.getContent();
        if (dirContent != null && !dirContent.isEmpty()) {
            contentToScan.addAll(sortContent(iterator, folder, 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 folder SyncedFolder to check for persistance
     * @param context context to contact database
     * @return false if not persisted in db
     */
    private boolean isSyncedFolderInDb(@NonNull SyncedFolder folder, @NonNull Context context) {
        if (folder.getId() > 0) return true;

        final int syncedFolderId = (int) DbHelper.insertSyncedFolder(folder, context); //It will return -1 if there is an error, like an already existing folder with same value
        if (syncedFolderId <= 0) {
            Timber.v("insertion of syncedFolder for %s failed: %s ", folder.getRemoteFolder(), syncedFolderId);
            return false;
        }

        folder.setId(syncedFolderId);
        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 folder the SyncFolder which own the content
     * @param dirContent Content to sort
     * @return List of subfiles to scan or empty list if nothing
     */
    private List<T> sortContent(@NonNull ListIterator<SyncedFolder> iterator, @NonNull SyncedFolder folder, List<T> dirContent) {
        final List<T> result = new ArrayList<>();
        if (dirContent == null) return result;

        for (T file : dirContent) {
            if (file == null) continue;

            final String fileName = getFileName(file);
            if (fileName == null) continue;

            if (isDirectory(file)) {
                final SyncedFolder subSyncedFolder = new SyncedFolder(folder, fileName + FileUtils.PATH_SEPARATOR, 0L, "");//Need to set lastModified to 0 to handle it on next iteration
                iterator.add(subSyncedFolder);
                iterator.previous();
            } else if (folder.isToSync() && !skipFile(file)) {
                Timber.v("added %s into list of file to scan", fileName);
                result.add(file);
            }
        }

        return result;
    }

    /**
     * List of file to scan
     * @return List<File> or List<RemoteFile> is expected based on implementations
     */
    public List<T> getContentToScan() {
        return contentToScan;
    }

    /**
     * Share the list of syncedFolder's ID for syncedFolder which has content to scan
     * @return List of syncedFolder ids
     */
    public List<Long> getSyncedFoldersId() {
        final List<Long> result = new ArrayList<>();
        for (SyncedFolder folder : folders)  {
            result.add((long) folder.getId());
        }
        return result;
    }
}
+67 −0
Original line number Diff line number Diff line
/*
 * 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 <T> File or RemoteFile expected
 */
public class FolderWrapper<T> {

    private final T folder;
    private final List<T> content;

    protected FolderWrapper(@NonNull T folder) {
        content = new ArrayList<>();
        this.folder = folder;
    }

    protected FolderWrapper() {
        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<T> getContent() {
        return content;
    }

    public void addContent(@NonNull List<T> 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 folder == null;
    }
}
+113 −0
Original line number Diff line number Diff line
/*
 * 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<File> {

    public LocalFileLister(@NonNull List<SyncedFolder> 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 createFolderLoader() {
        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<File> {
        private FolderWrapper<File> folder;

        @Override
        public FolderWrapper<File> 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 SyncedFolder instance with data about the folder to load
         * @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
                return true;
            } 

            folder = new FolderWrapper(dir);
            final String category = syncedFolder.isMediaType() ? "media" : syncedFolder.getLibelle();

            final FileFilter filter = FileFilterFactory.getFileFilter(category);
            final File[] files = dir.listFiles(filter);
            if (files != null) {
                folder.addContent(Arrays.asList(files));
            }
            
            return true;
        }
    }
}
+134 −0
Original line number Diff line number Diff line
/*
 * 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<RemoteFile> {
    private OwnCloudClient client;

    public RemoteFileLister(@NonNull List<SyncedFolder> directories, @NonNull OwnCloudClient client) {
        super(directories);
        this.client = client;
    }

    @Override
    protected boolean skipSyncedFolder(@NonNull SyncedFolder folder) {
        return (folder.isMediaType()
                && CommonUtils.getFileNameFromPath(folder.getRemoteFolder()).startsWith("."))
                || !folder.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 createFolderLoader() {
        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<RemoteFile> {
        private static final int HTTP_404 = 404;
        private FolderWrapper<RemoteFile> directory;

        @Override
        public boolean load(@NonNull SyncedFolder syncedFolder) {
            if (syncedFolder.getRemoteFolder() == null) return false;
            final RemoteOperationResult<RemoteFile> remoteOperationResult = readRemoteFolder(syncedFolder.getRemoteFolder(), client);

            if (!remoteOperationResult.isSuccess()) {
                if (remoteOperationResult.getHttpCode() == HTTP_404) {
                    directory = new FolderWrapper<>();
                    return true;
                }
                return false;
            }

            final List<Object> 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<RemoteFile> 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<RemoteFile> getFolderWrapper() {
            return directory;
        }
    }
}
+15 −132

File changed.

Preview size limit exceeded, changes collapsed.

Loading