Source: component/files.js

/*! meta-admin/modules/files */
/*jslint
    browser, long
*/
/*global
    Blob, confirm
*/
import {saveAs} from "file-saver";
import Logger from "js-logger";
import ko from "knockout";
import objects from "meta-client/modules/objects";
import model from "meta-core/modules/model";
import protocol from "meta-core/modules/protocol";
import _ from "underscore";
import buffers from "util-web/modules/buffers";
import crypto from "util-web/modules/crypto";
import dates from "util-web/modules/dates";
import ext from "util-web/modules/ext";
import i18next from "util-web/modules/i18next";
import nav from "util-web/modules/nav";
import ui from "util-web/modules/ui";
import template from "./files.html";

const LOG = Logger.get("meta-admin/files");
const BACKUP_QUERY = `tags:${model.BACKUP_TAG}`;
const BASE_64_SPLIT = "base64,";
const BASE_64_SPLIT_SIZE = BASE_64_SPLIT.length;

/**
 * files component.
 *
 * @module meta-admin/modules/component/files
 */
export default Object.freeze({
    template,
    viewModel: {
        createViewModel: function ({viewmodel}) {
            const vm = {};

            vm.FILE_CONTENT_TYPES = ext.FILE_CONTENT_TYPES;
            vm.viewmodel = viewmodel;

            vm.fileQuery = ko.observable();
            vm.fileName = ko.observable();
            vm.fileTags = ko.observable();
            vm.fileChannel = ko.observable();
            vm.fileExpiration = ko.observable();
            vm.fileExpirationSub = dates.insertDateTimes({fromDateObservable: vm.fileExpiration});
            vm.fileType = ko.observable(ext.FILE_CONTENT_TYPES.BUFFER);
            vm.fileSearcher = objects({
                searchReplyObjectsPath: "files",
                toVm: (fileInfo) => vm.viewmodel.fileInfoVm(protocol.fileInfo(fileInfo))
            });
            vm.fileProgress = ko.observable();
            vm.backupServiceOptions = ko.observableArray();
            vm.backupService = ko.observable();
            vm.isBackupInProgres = ko.observable(false);
            vm.errorReply = ko.observable();

            vm.copyKey = function (fileInfo) {
                return navigator.clipboard.writeText(fileInfo.key());
            };

            vm.copyHash = function (fileInfo) {
                return navigator.clipboard.writeText(fileInfo.hash());
            };

            vm.searchFilesInternal = function (limit, clearSearch) {
                const query = vm.fileQuery();
                LOG.debug(`searchFilesInternal, query=${query}`);
                if (clearSearch) {
                    vm.fileSearcher.clear();
                }
                return vm.fileSearcher.processSearch(vm.viewmodel.client.fileSearch(protocol.fileSearchRequest({
                    cursor: vm.fileSearcher.searchCursor(),
                    limit,
                    query,
                    sort: model.lucene.FILE_FIELD_UPLOAD_TIMESTAMP,
                    sortReverse: true
                }))).then(function () {
                    vm.errorReply(undefined);
                }, function (exc) {
                    LOG.warn("searchFilesInternal failed", exc);
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                });
            };

            vm.searchFiles = function (form) {
                if (ui.validateForm(form) === false) {
                    return;
                }
                LOG.debug("searchFiles");
                return vm.searchFilesInternal(100, true).then(function () {
                    ui.resetForm(form);
                });
            };
            vm.loadMore = function () {
                LOG.debug("loadMore");
                return vm.searchFilesInternal(100, false);
            };
            vm.loadAll = function () {
                LOG.debug("loadAll");
                return vm.searchFilesInternal(0, false);
            };

            vm.uploadFile = function (ignore, event) {
                let uploadBuffer;
                let contentSize;
                LOG.debug("uploadFile");
                return ext.loadFileContent({event, type: vm.fileType()}).then(function (file) {
                    if (ext.FILE_CONTENT_TYPES.BUFFER === vm.fileType()) {
                        uploadBuffer = file.content;
                    } else {
                        uploadBuffer = buffers.stringToBuffer(file.content);
                    }
                    contentSize = uploadBuffer.byteLength;
                    LOG.debug(`uploadFile ready, contentSize=${contentSize}`);
                    return crypto.hashBuffer(uploadBuffer);
                }).then(function (hash) {
                    const tags = vm.fileTags();
                    return vm.viewmodel.client.upload(protocol.uploadRequest({
                        channel: vm.fileChannel(),
                        expiration: vm.fileExpiration(),
                        hash,
                        name: vm.fileName(),
                        size: contentSize,
                        tags: (
                            _.isEmpty(tags) === false
                                ? tags.split(",")
                                : undefined
                        )
                    }));
                }).then(function (reply) {
                    if (reply.duplicate) {
                        vm.errorReply(i18next.t("file_duplicate"));
                        return Promise.resolve(undefined);
                    }
                    vm.errorReply(undefined);
                    vm.fileProgress(reply.progress);
                    return Promise.all([
                        reply.sendFile(uploadBuffer),
                        reply.donePromise
                    ]);
                }).then(function () {
                    vm.fileChannel(undefined);
                    vm.fileExpiration(undefined);
                    vm.fileName(undefined);
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                });
            };

            vm.downloadFile = function (fileInfo) {
                const key = fileInfo.key();
                let expectedHash;
                let fileName;
                let downloadBlob;
                LOG.debug(`downloadFile, key=${key}`);
                return vm.viewmodel.client.download(protocol.downloadRequest({key})).then(function (downloadReply) {
                    expectedHash = downloadReply.file.hash;
                    fileName = downloadReply.file.name;
                    vm.fileProgress(downloadReply.progress);
                    return downloadReply.donePromise;
                }).then(function (blob) {
                    downloadBlob = blob;
                    return blob.arrayBuffer();
                }).then(function (buffer) {
                    return Promise.all([crypto.hashBuffer(buffer), Promise.resolve(buffer)]);
                }).then(function (result) {
                    const hash = result[0];
                    if (hash !== expectedHash) {
                        throw new Error(`hash invalid, key=${key}, expectedHash=${expectedHash}, hash=${hash}`);
                    }
                    vm.errorReply(undefined);
                    const buffer = result[1];
                    let fileBlob;
                    let dataUrl;
                    let dataUrlBase64;
                    switch (vm.fileType()) {
                    case ext.FILE_CONTENT_TYPES.BUFFER:
                        fileBlob = downloadBlob;
                        break;
                    case ext.FILE_CONTENT_TYPES.DATAURL:
                        dataUrl = buffers.bufferToString(buffer);
                        dataUrlBase64 = dataUrl.substring(dataUrl.indexOf(BASE_64_SPLIT) + BASE_64_SPLIT_SIZE);
                        fileBlob = new Blob([buffers.base64ToBuffer(dataUrlBase64)]);
                        break;
                    case ext.FILE_CONTENT_TYPES.TEXT:
                        fileBlob = new Blob([buffers.bufferToString(buffer)]);
                        break;
                    }
                    saveAs(fileBlob, key + (
                        _.isEmpty(fileName)
                            ? ".file"
                            : "_" + fileName
                    ));
                }, function (exc) {
                    LOG.warn("downloadFile failed", exc);
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                });
            };

            vm.deleteFile = function (fileInfo) {
                const key = fileInfo.key();
                LOG.debug(`deleteFile, key=${key}`);
                if (confirm(i18next.t("file_delete_confirm")) === false) {
                    return;
                }
                return vm.viewmodel.client.fileDelete(protocol.fileDeleteRequest({key})).then(function () {
                    vm.errorReply(undefined);
                    vm.fileSearcher.objects.remove((fileVm) => fileVm.key() === key);
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                });
            };

            vm.searchBackups = function () {
                const service = vm.backupService();
                const query = BACKUP_QUERY + (
                    _.isEmpty(service)
                        ? ""
                        : ` AND tags:"${service}"`
                );
                vm.fileQuery(query);
                return vm.searchFiles();
            };

            vm.createBackup = function (form) {
                if (ui.validateForm(form) === false) {
                    return;
                }
                const service = vm.backupService();
                LOG.debug(`createBackup, service=${service}`);
                vm.isBackupInProgres(true);
                return vm.viewmodel.client.backup(protocol.backupRequest({service})).then(function () {
                    vm.errorReply(undefined);
                    ui.resetForm(form);
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(function () {
                    vm.isBackupInProgres(false);
                });
            };

            vm.restoreBackup = function (fileInfo) {
                const key = fileInfo.key();
                LOG.debug(`restoreBackup, key=${key}`);
                if (confirm(i18next.t("restore_backup_confirm")) === false) {
                    return;
                }
                vm.isBackupInProgres(true);
                return vm.viewmodel.client.backupRestore(protocol.backupRestoreRequest({key})).then(function () {
                    vm.errorReply(undefined);
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(function () {
                    vm.isBackupInProgres(false);
                });
            };

            vm.isExporting = ko.observable(false);

            vm.doExport = function () {
                const fileExport = {
                    files: vm.fileSearcher.objects().map(vm.viewmodel.vmToFileInfo)
                };
                LOG.debug("doExport");
                vm.isExporting(true);
                saveAs(
                    new Blob([JSON.stringify(fileExport, undefined, 4)], {type: "application/json;charset=utf-8"}),
                    `file_export-${dates.now().toMillis()}.json`
                );
                vm.isExporting(false);
            };

            vm.onClientNotification = function (clientNotification) {
                const data = _.get(clientNotification, "data");
                if (protocol.UPLOAD_DONE_NOTIFICATION_ACTION !== clientNotification.action || _.isObject(data) === false) {
                    return;
                }
                LOG.debug("onClientNotification", data);
                vm.fileSearcher.objects.unshift(vm.viewmodel.fileInfoVm(data));
            };
            vm[protocol.CLIENT_NOTIFICATION] = vm.onClientNotification;
            const eventHandle = vm.viewmodel.client.registerOnEvent(vm);

            nav.register({
                id: vm.viewmodel.COMPONENTS.FILES,
                onUpdate: vm.searchFiles
            });

            vm.dispose = function () {
                LOG.debug("dispose");
                vm.viewmodel.client.unregisterOnEvent(eventHandle);
                vm.fileExpirationSub.dispose();
            };

            ko.when(function () {
                return vm.viewmodel.client.isAuth() && vm.viewmodel.isAdmin();
            }).then(function () {
                return vm.viewmodel.client.getServiceInfo(protocol.serviceInfoRequest());
            }).then(function (reply) {
                vm.backupServiceOptions(reply.info["FileService"].backupDelegateServiceNames);
            });

            return Object.freeze(vm);
        }
    }
});