rooey/chromeos-filesystem-onedrive

View on GitHub
src/scripts/onedrive_fs.js

Summary

Maintainability
D
3 days
Test Coverage
'use strict';

// Constants
// Use set DEBUG_ENABLED to true to enable verbose logging (alpha builds)
const DEBUG_ENABLED = true;

const FILE_SYSTEM_ID = 'onedrivefs';
const FILE_SYSTEM_NAME = 'OneDrive';

class OneDriveFS {

    // Constructor

    constructor() {
        this.onedrive_client_map_ = {};
        this.metadata_cache_ = {};
        this.watchers_ = {};
        this.assignEventHandlers();
    }

    // Public functions

    mount(successCallback, errorCallback) {
        const onedriveClient = new OneDriveClient(this);
        onedriveClient.authorize(() => {
            onedriveClient.getDriveData((driveInfo) => {
                onedriveClient.getUserInfo((userInfo) => {
                    this.writeLog('debug', 'driveInfo', driveInfo);

                    const fileSystemId = this.createFileSystemID(driveInfo.id);
                    chrome.fileSystemProvider.getAll(fileSystems => {
                        let mounted = false;
                        for (let i = 0; i < fileSystems.length; i++) {
                            if (fileSystems[i].fileSystemId === fileSystemId) {
                                mounted = true;
                                break;
                            }
                        }
                        if (mounted) {
                            errorCallback('ALREADY_MOUNTED');
                        } else {
                            this.onedrive_client_map_[fileSystemId] = onedriveClient;
                            chrome.storage.local.get('settings', items => {
                                const settings = items.settings || {};
                                const openedFilesLimit = settings.openedFilesLimit || '10';
                                chrome.fileSystemProvider.mount({
                                    fileSystemId: fileSystemId,
                                    displayName: FILE_SYSTEM_NAME + ' ' + driveInfo.type +' ('+ userInfo.displayName + ')',
                                    writable: true,
                                    openedFilesLimit: Number(openedFilesLimit)
                                }, () => {
                                    this.registerMountedCredential(
                                        driveInfo.id, onedriveClient.getToken('accessToken'), onedriveClient.getToken('refreshToken'), () => {
                                        successCallback();
                                    });
                                });
                            });
                        }
                    });
                }, reason => {
                    errorCallback(reason);
                });
            }, reason => {
                errorCallback(reason);
            });
        }, reason => {
            errorCallback(reason);
        });
    }

    resume(fileSystemId, successCallback, errorCallback) {
        this.getMountedCredential(fileSystemId, credential => {
            if (credential) {
                const onedriveClient = new OneDriveClient(this);
                onedriveClient.setTokens(credential.accessToken, credential.refreshToken);
                onedriveClient.setUid(credential.uid);
                this.onedrive_client_map_[fileSystemId] = onedriveClient;
                successCallback();
            } else {
                this.sendMessageToSentry('resume(): CREDENTIAL_NOT_FOUND', {
                    fileSystemId: fileSystemId
                });
                errorCallback('CREDENTIAL_NOT_FOUND');
            }
        });
    }

    unmount(onedriveClient, callback) {
        this.doUnmount(onedriveClient, null, callback);
    }

    onUnmountRequested(options, successCallback, _errorCallback) {
        this.writeLog('debug', 'onUnmountRequested', options);
        const onedriveClient = this.getOneDriveClient(options.fileSystemId);
        this.doUnmount(onedriveClient, options.requestId, successCallback);
    }

    onReadDirectoryRequested(onedriveClient, options, successCallback, errorCallback) {
        onedriveClient.readDirectory(options.directoryPath, entryMetadataList => {
            const cache = this.getMetadataCache(options.fileSystemId);
            cache.put(options.directoryPath, entryMetadataList);
            successCallback(entryMetadataList.map(e => {
                return this.trimMetadata(options, e);
            }), false);
        }, errorCallback);
    }

    onGetMetadataRequested(onedriveClient, options, successCallback, errorCallback) {
        this.writeLog('debug', 'Thumbnail', options.thumbnail);
        const metadataCache = this.getMetadataCache(options.fileSystemId);
        const cache = metadataCache.get(options.entryPath);
        if (cache.directoryExists && cache.fileExists && !options.thumbnail) {
            successCallback(this.trimMetadata(options, cache.metadata));
        } else {
            onedriveClient.getMetadata(
                options.entryPath, entryMetadata => {
                    successCallback(this.trimMetadata(options, entryMetadata));
                }, errorCallback);
        }
    }

    onOpenFileRequested(onedriveClient, options, successCallback, errorCallback) {
        onedriveClient.openFile(options.filePath, options.requestId, options.mode, successCallback, errorCallback);
    }

    onReadFileRequested(onedriveClient, options, successCallback, errorCallback) {
        this.getOpenedFile(options.fileSystemId, options.openRequestId, openedFile => {
            onedriveClient.readFile(
                openedFile.filePath, options.offset, options.length, (data, hasMore) => {
                    successCallback(data, hasMore);
                }, errorCallback);
        });
    }

    onCloseFileRequested(onedriveClient, options, successCallback, errorCallback) {
        this.getOpenedFile(options.fileSystemId, options.openRequestId, openedFile => {
            onedriveClient.closeFile(openedFile.filePath, options.openRequestId, openedFile.mode, successCallback, errorCallback);
        });
    }

    onCreateDirectoryRequested(onedriveClient, options, successCallback, errorCallback) {
        this.updateEntry(
            'createDirectory', onedriveClient, options, successCallback, errorCallback);
    }

    onDeleteEntryRequested(onedriveClient, options, successCallback, errorCallback) {
        this.updateEntry(
            'deleteEntry', onedriveClient, options, successCallback, errorCallback);
    }

    onMoveEntryRequested(onedriveClient, options, successCallback, errorCallback) {
        this.updateEntry('moveEntry', onedriveClient, options, successCallback, errorCallback);
    }

    onCopyEntryRequested(onedriveClient, options, successCallback, errorCallback) {
        this.updateEntry('copyEntry', onedriveClient, options, successCallback, errorCallback);
    }

    onWriteFileRequested(onedriveClient, options, successCallback, errorCallback) {
        this.writeLog('debug', this.name, options);
        this.getOpenedFile(options.fileSystemId, options.openRequestId, openedFile => {
            onedriveClient.writeFile(openedFile.filePath, options.data, options.offset, options.openRequestId, () => {
                const metadataCache = this.getMetadataCache(options.fileSystemId);
                metadataCache.remove(openedFile.filePath);
                successCallback();
            }, errorCallback);
        });
    }

    onTruncateRequested(onedriveClient, options, successCallback, errorCallback) {
        onedriveClient.truncate(options.filePath, options.length, () => {
            const metadataCache = this.getMetadataCache(options.fileSystemId);
            metadataCache.remove(options.filePath);
            successCallback(false);
        }, errorCallback);
    }

    onCreateFileRequested(onedriveClient, options, successCallback, errorCallback) {
        this.updateEntry(
            'createFile', onedriveClient, options, successCallback, errorCallback);
    }

    onAddWatcherRequested(onedriveClient, options, successCallback, _errorCallback) {
        const watchers = this.getWatchers(options.fileSystemId);
        watchers.add(options.entryPath);
        successCallback();
    }

    onRemoveWatcherRequested(onedriveClient, options, successCallback, _errorCallback) {
        const watchers = this.getWatchers(options.fileSystemId);
        watchers.delete(options.entryPath);
        successCallback();
    }

    onAlarm(_alarm) {
        for (let fileSystemId in this.watchers_) {
            const onedriveClient = this.getOneDriveClient(fileSystemId);
            const watchers = this.watchers_[fileSystemId];
            for (let watcher of watchers.values()) {
                this.watchDirectory(fileSystemId, onedriveClient, watcher);
            }
        }
    }

    // Private functions

    trimMetadata(options, metadata) {
        const result = {};
        if (options.isDirectory) {
            this.writeLog('debug', this.name, metadata.isDirectory);
            result.isDirectory = metadata.isDirectory;
        }
        if (options.name) result.name = metadata.name;
        if (options.size) result.size = metadata.size;
        if (options.modificationTime) result.modificationTime = metadata.modificationTime;
        if (options.thumbnail) result.thumbnail = metadata.thumbnail;
        return result;
    }

    updateEntry(operation, onedriveClient, options, successCallback, errorCallback) {
        this.writeLog('debug', 'updateEntry - ' + operation, options);
        switch (operation) {
            case 'createDirectory':
                this.doOperation (operation, options.directoryPath, onedriveClient, options, successCallback, errorCallback);
                break;

            case 'deleteEntry':
                this.doOperation (operation, options.entryPath, onedriveClient, options, successCallback, errorCallback);
                break;

            case 'createFile':
                this.doOperation (operation, options.filePath, onedriveClient, options, successCallback, errorCallback);
                break;

            default:
                onedriveClient[operation](options.sourcePath, options.targetPath, () => {
                    const metadataCache = this.getMetadataCache(options.fileSystemId);
                    metadataCache.remove(options.sourcePath);
                    metadataCache.remove(options.targetPath);
                    successCallback();
                }, errorCallback);
        }
    }

    doOperation(operation, path, onedriveClient, options, successCallback, errorCallback) {
        this.writeLog('debug', 'updateEntry - fallthrough', path);
        onedriveClient[operation](path, () => {
            const metadataCache = this.getMetadataCache(options.fileSystemId);
            metadataCache.remove(path);
            this.writeLog('debug', 'updateEntry - setpath', path);
            successCallback();
        }, errorCallback);
    }

    doUnmount(onedriveClient, requestId, successCallback) {
        this._doUnmount(
            onedriveClient.getUid(),
            successCallback
        );
    }

    _doUnmount(uid, successCallback) {
        this.unregisterMountedCredential(
            uid,
            ()=> {
                const fileSystemId = this.createFileSystemID(uid);
                this.writeLog('debug', this.name, fileSystemId);
                delete this.onedrive_client_map_[fileSystemId];
                this.deleteMetadataCache(fileSystemId);
                this.deleteWatchers(fileSystemId);
                successCallback();
                chrome.fileSystemProvider.unmount({
                    fileSystemId: fileSystemId
                }, () => {
                    // N/A
                });
            }
        );
    }

    registerMountedCredential(uid, accessToken, refreshToken, callback) {
        const fileSystemId = this.createFileSystemID(uid);
        chrome.storage.local.get('credentials', items => {
            const credentials = items.credentials || {};
            credentials[fileSystemId] = {
                accessToken: accessToken,
                refreshToken: refreshToken,
                uid: uid
            };
            chrome.storage.local.set({
                credentials: credentials
            }, callback);
        });
    }

    unregisterMountedCredential(uid, callback) {
        const fileSystemId = this.createFileSystemID(uid);
        chrome.storage.local.get('credentials', items => {
            const credentials = items.credentials || {};
            delete credentials[fileSystemId];
            chrome.storage.local.set({
                credentials: credentials
            }, callback);
        });
    }

    getMountedCredential(fileSystemId, callback) {
        chrome.storage.local.get('credentials', items => {
            const credentials = items.credentials || {};
            const credential = credentials[fileSystemId];
            callback(credential);
        });
    }

    createEventHandler(callback) {
        return (options, successCallback, errorCallback) => {
            const fileSystemId = options.fileSystemId;
            const onedriveClient = this.getOneDriveClient(fileSystemId);
            if (!onedriveClient) {
                this.resume(fileSystemId, () => {
                    callback(options, successCallback, errorCallback);
                }, reason => {
                    this.writeLog('debug', 'resume failed', reason);
                    chrome.notifications.create('', {
                        type: 'basic',
                        title: 'File System for OneDrive',
                        message: 'Resuming failed. Unmount.',
                        iconUrl: '/images/48.png'
                    }, _notificationId => {
                    });
                    this.getMountedCredential(fileSystemId, credential => {
                        if (credential) {
                            this._doUnmount(
                                onedriveClient,
                                credential.uid,
                                () => {
                                    this.sendMessageToSentry('createEventHandler(): FAILED', {
                                        fileSystemId: fileSystemId,
                                        credential: credential
                                    });
                                    errorCallback('FAILED');
                                });
                        } else {
                            this.writeLog('debug', 'Credential not found', fileSystemId);
                            this.sendMessageToSentry('createEventHandler(): Credential for [' + fileSystemId + '] not found', {
                                fileSystemId: fileSystemId
                            });
                            errorCallback('FAILED');
                        }
                    });
                });
            } else {
                callback(options, successCallback, errorCallback);
            }
        };
    }

    assignEventHandlers() {
        chrome.alarms.onAlarm.addListener(alarm => {
            if (alarm.name === 'onedrive_alarm') {
                this.onAlarm(alarm);
            }
        });
        chrome.alarms.create('onedrive_alarm', {
            delayInMinutes: 1,
            periodInMinutes: 1
        });
        chrome.fileSystemProvider.onUnmountRequested.addListener(
            (options, successCallback, errorCallback) => { // Unmount immediately
                this.writeLog('debug', 'pre onUnmountRequested', options);
                const fileSystemId = options.fileSystemId;
                const onedriveClient = this.getOneDriveClient(fileSystemId);
                if (!onedriveClient) {
                    this.resume(fileSystemId, () => {
                        this.onUnmountRequested(options, successCallback, errorCallback);
                    }, reason => {
                        this.sendMessageToSentry('assignEventHandlers(): onUnmountRequested - FAILED', {
                            reason: reason
                        });
                        errorCallback('FAILED');
                    });
                } else {
                    this.onUnmountRequested(options, successCallback, errorCallback);
                }
            });
        const funcNameList = [
            'onReadDirectoryRequested',
            'onGetMetadataRequested',
            'onOpenFileRequested',
            'onReadFileRequested',
            'onCloseFileRequested',
            'onCreateDirectoryRequested',
            'onDeleteEntryRequested',
            'onMoveEntryRequested',
            'onCopyEntryRequested',
            'onWriteFileRequested',
            'onTruncateRequested',
            'onCreateFileRequested',
            'onAddWatcherRequested',
            'onRemoveWatcherRequested'
        ];
        const caller = (self, funcName) => {
            return (options, successCallback, errorCallback) => {
                this.writeLog('debug', funcName, options);
                const onedriveClient = this.getOneDriveClient(options.fileSystemId);
                this[funcName](onedriveClient, options, successCallback, errorCallback);
            };
        };
        for (let i = 0; i < funcNameList.length; i++) {
            chrome.fileSystemProvider[funcNameList[i]].addListener(
                this.createEventHandler(
                    caller(this, funcNameList[i])
                )
            );
        }
    }

    getMetadataCache(fileSystemId) {
        let metadataCache = this.metadata_cache_[fileSystemId];
        if (!metadataCache) {
            metadataCache = new MetadataCache();
            this.metadata_cache_[fileSystemId] = metadataCache;
        }

        this.writeLog('debug', 'metadataCache', metadataCache);
        return metadataCache;
    };

    deleteMetadataCache(fileSystemId) {
        this.writeLog('debug', this.name, fileSystemId);
        delete this.metadata_cache_[fileSystemId];
    };

    createFileSystemID(uid) {
        return FILE_SYSTEM_ID + '://' + uid;
    };

    getOneDriveClient(fileSystemID) {
        return this.onedrive_client_map_[fileSystemID];
    };

    getOpenedFiles(fileSystemId, callback) {
        chrome.fileSystemProvider.get(fileSystemId, fileSystem => {
            callback(fileSystem.openedFiles);
        });
    };

    getOpenedFile(fileSystemId, openRequestId, callback) {
        this.getOpenedFiles(fileSystemId, openedFiles => {
            const openedFile = openedFiles.filter(x => {
                return x.openRequestId === openRequestId;
            });
            if (openedFile.length >= 1) {
                callback(openedFile[0]);
            } else {
                throw new Error('OpenedFile information not found. openRequestId=' + openRequestId);
            }
        });
    };

    writeLog(messageType, message, payload) {
        if ((messageType === 'debug') && (DEBUG_ENABLED !==true)) return;
        console.log('[' + messageType + '] ' + message, payload);
        return;
    };

    sendMessageToSentry(message, extra) {
        this.useOptions('useSentry', use => {
            //ISSUE #57 @rooey
            //Only send to sentry if user has opted-in
            if (!use) {
                return;
            }
            this.writeLog('sentry', message, extra);
            if (Raven.isSetup()) {
                Raven.captureMessage(new Error(message), {
                    extra: extra,
                    tags: {
                        'app.version': chrome.runtime.getManifest().version
                    }
                });
            }
        });
    }

    getWatchers(fileSystemId) {
        let watchers = this.watchers_[fileSystemId];
        if (!watchers) {
            watchers = new Set();
            this.watchers_[fileSystemId] = watchers;
        }
        return watchers;
    }

    deleteWatchers(fileSystemId) {
        delete this.watchers_[fileSystemId];
    }

    /*useWatcher(callback) {
        chrome.storage.local.get('settings', items => {
            const settings = items.settings || {};
            callback(settings.useWatcher || false);
        });
    }*/

    useOptions(options, callback) {
        chrome.storage.local.get('settings', items => {
            const settings = items.settings || {};
            callback(settings[options] || false);
        });
    }

    watchDirectory(fileSystemId, onedriveClient, entryPath) {
        this.useOptions('useWatcher', use => {
            if (!use) {
                return;
            }
            this.writeLog('debug', this.name, entryPath);
            onedriveClient.readDirectory(entryPath, entries => {
                const metadataCache = this.getMetadataCache(fileSystemId);
                const currentList = entries;
                const oldList = metadataCache.directories_[entryPath] || {};
                const nameSet = new Set();
                for (let i = 0; i < currentList.length; i++) {
                    const current = currentList[i];
                    const old = oldList[current.name];
                    if (old) {
                        // Changed
                        const isBothDirectory = current.isDirectory && old.isDirectory;
                        const isMatchType = current.isDirectory === old.isDirectory;
                        const isMatchSize = current.size === old.size;
                        const isMatchModificationTime = current.modificationTime.getTime() === old.modificationTime.getTime();
                        if (!isBothDirectory && !(isMatchType && isMatchSize && isMatchModificationTime)) {
                            this.writeLog('debug', 'Changed:', current.name);
                            this.notifyEntryChanged(fileSystemId, entryPath, 'CHANGED', current.name);
                        }
                    } else {
                        // Added
                        this.writeLog('debug', 'Added:', current.name);
                        this.notifyEntryChanged(fileSystemId, entryPath, 'CHANGED', current.name);
                    }
                    nameSet.add(current.name);
                }
                for (let oldName in oldList) {
                    if (!nameSet.has(oldName)) {
                        // Deleted
                        this.writeLog('debug', 'Deleted:', oldName);
                        this.notifyEntryChanged(fileSystemId, entryPath, 'DELETED', oldName);
                    }
                }
                metadataCache.put(entryPath, currentList);
            }, (reason) => {
                this.sendMessageToSentry('watchDirectory(): ' + reason, {
                    fileSystemId: fileSystemId
                });
            });
        });
    }

    notifyEntryChanged(fileSystemId, directoryPath, changeType, entryPath) {
        this.writeLog('debug', this.name, `notifyEntryChanged: ${directoryPath} ${entryPath} {changeType}${changeType}`);
        //console.log(`notifyEntryChanged: ${directoryPath} ${entryPath} {changeType}${changeType}`);
        chrome.fileSystemProvider.notify({
            fileSystemId: fileSystemId,
            observedPath: directoryPath,
            recursive: false,
            changeType: 'CHANGED',
            changes: [
                {entryPath: entryPath, changeType: changeType}
            ]
        }, () => {});
    }

};

// Export
window.OneDriveFS = OneDriveFS;