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

Commit cf2becc0 authored by Vincent Bourgmayer's avatar Vincent Bourgmayer
Browse files

Implement new classes to handle listing of existing content

parent 3bfba645
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