Source: common/blob/Artifact.js

/*globals define*/
/*eslint-env node, browser*/

/**
 * @author lattmann / https://github.com/lattmann
 */

define([
    'blob/BlobMetadata',
    'blob/BlobConfig',
    'common/core/tasync',
    'q'
], function (BlobMetadata, BlobConfig, tasync, Q) {
    'use strict';

    /**
     * Creates a new instance of artifact, i.e. complex object, in memory. This object can be saved in the blob-storage
     * on the server and later retrieved with its metadata hash.
     * @param {string} name Artifact's name without extension
     * @param {BlobClient} blobClient
     * @param {BlobMetadata} descriptor
     * @constructor
     * @alias Artifact
     */
    var Artifact = function (name, blobClient, descriptor) {
        this.name = name;
        this.blobClient = blobClient;
        this.blobClientPutFile = tasync.unwrap(tasync.throttle(tasync.wrap(blobClient.putFile), 5));
        this.blobClientGetMetadata = tasync.unwrap(tasync.throttle(tasync.wrap(blobClient.getMetadata), 5));
        // TODO: use BlobMetadata class here
        this.descriptor = descriptor || {
            name: name + '.zip',
            size: 0,
            mime: 'application/zip',
            content: {},
            contentType: 'complex'
        }; // name and hash pairs
    };

    /**
     * Adds content to the artifact as a file.
     * @param {string} name - filename
     * @param {Blob} content - File object or Blob.
     * @param {function} [callback] - if provided no promise will be returned.
     *
     * @return {external:Promise}  On success the promise will be resolved with {string} <b>metadataHash</b>.<br>
     * On error the promise will be rejected with {@link Error} <b>error</b>.
     */
    Artifact.prototype.addFile = function (name, content, callback) {
        var self = this,
            filename = name.substring(name.lastIndexOf('/') + 1),
            deferred = Q.defer();

        self.blobClientPutFile.call(self.blobClient, filename, content, function (err, metadataHash) {
            if (err) {
                deferred.reject(err);
                return;
            }

            self.addObjectHash(name, metadataHash, function (err, metadataHash) {
                if (err) {
                    deferred.reject(err);
                    return;
                }

                deferred.resolve(metadataHash);
            });
        });

        return deferred.promise.nodeify(callback);
    };

    /**
     * Adds files as soft-link.
     * @param {string} name - filename.
     * @param {Blob} content - File object or Blob.
     * @param {function} [callback] - if provided no promise will be returned.
     *
     * @return {external:Promise}  On success the promise will be resolved with {string} <b>metadataHash</b>.<br>
     * On error the promise will be rejected with {@link Error} <b>error</b>.
     */
    Artifact.prototype.addFileAsSoftLink = function (name, content, callback) {
        var deferred = Q.defer(),
            self = this,
            filename = name.substring(name.lastIndexOf('/') + 1);

        self.blobClientPutFile.call(self.blobClient, filename, content,
            function (err, metadataHash) {
                if (err) {
                    deferred.reject(err);
                    return;
                }
                var size;
                if (content.size !== undefined) {
                    size = content.size;
                }
                if (content.length !== undefined) {
                    size = content.length;
                }

                self.addMetadataHash(name, metadataHash, size)
                    .then(deferred.resolve)
                    .catch(deferred.reject);
            });

        return deferred.promise.nodeify(callback);
    };

    /**
     * Adds a hash to the artifact using the given file path.
     * @param {string} name - Path to the file in the artifact. Note: 'a/b/c.txt'
     * @param {string} metadataHash - Metadata hash that has to be added.
     * @param {function} [callback] - if provided no promise will be returned.
     *
     * @return {external:Promise}  On success the promise will be resolved with {string} <b>hash</b>.<br>
     * On error the promise will be rejected with {@link Error} <b>error</b>.
     */
    Artifact.prototype.addObjectHash = function (name, metadataHash, callback) {
        var self = this,
            deferred = Q.defer();

        if (BlobConfig.hashRegex.test(metadataHash) === false) {
            deferred.reject('Blob hash is invalid');
        } else {
            self.blobClientGetMetadata.call(self.blobClient, metadataHash, function (err, metadata) {
                if (err) {
                    deferred.reject(err);
                    return;
                }

                if (Object.hasOwn(self.descriptor.content, name)) {
                    deferred.reject(new Error('Another content with the same name was already added. ' +
                        JSON.stringify(self.descriptor.content[name])));

                } else {
                    self.descriptor.size += metadata.size;

                    self.descriptor.content[name] = {
                        content: metadata.content,
                        contentType: BlobMetadata.CONTENT_TYPES.OBJECT
                    };
                    deferred.resolve(metadataHash);
                }
            });
        }

        return deferred.promise.nodeify(callback);
    };

    /**
     * Adds a hash to the artifact using the given file path.
     * @param {string} name - Path to the file in the artifact. Note: 'a/b/c.txt'
     * @param {string} metadataHash - Metadata hash that has to be added.
     * @param {number} [size] - Size of the referenced blob.
     * @param {function} [callback] - if provided no promise will be returned.
     *
     * @return {external:Promise}  On success the promise will be resolved with {string} <b>hash</b>.<br>
     * On error the promise will be rejected with {@link Error} <b>error</b>.
     */
    Artifact.prototype.addMetadataHash = function (name, metadataHash, size, callback) {
        var self = this,
            deferred = Q.defer(),
            addMetadata = function (size) {
                if (Object.hasOwn(self.descriptor.content, name)) {
                    deferred.reject(new Error('Another content with the same name was already added. ' +
                        JSON.stringify(self.descriptor.content[name])));

                } else {
                    self.descriptor.size += size;

                    self.descriptor.content[name] = {
                        content: metadataHash,
                        contentType: BlobMetadata.CONTENT_TYPES.SOFT_LINK
                    };
                    deferred.resolve(metadataHash);
                }
            };

        if (typeof size === 'function') {
            callback = size;
            size = undefined;
        }

        if (BlobConfig.hashRegex.test(metadataHash) === false) {
            deferred.reject(new Error('Blob hash is invalid'));
        } else if (size === undefined) {
            self.blobClientGetMetadata.call(self.blobClient, metadataHash, function (err, metadata) {
                if (err) {
                    deferred.reject(err);
                    return;
                }
                addMetadata(metadata.size);
            });
        } else {
            addMetadata(size);
        }

        return deferred.promise.nodeify(callback);
    };

    /**
     * Adds multiple files.
     * @param {Object.<string, Blob>} files files to add
     * @param {function} [callback] - if provided no promise will be returned.
     *
     * @return {external:Promise}  On success the promise will be resolved with {string[]} <b>metadataHashes</b>.<br>
     * On error the promise will be rejected with {@link Error|string} <b>error</b>.
     */
    Artifact.prototype.addFiles = function (files, callback) {
        var self = this,
            fileNames = Object.keys(files);

        return Q.all(fileNames.map(function (fileName) {
            return self.addFile(fileName, files[fileName]);
        })).nodeify(callback);
    };

    /**
     * Adds multiple files as soft-links.
     * @param {Object.<string, Blob>} files files to add
     * @param {function} [callback] - if provided no promise will be returned.
     *
     * @return {external:Promise}  On success the promise will be resolved with {string[]} <b>metadataHashes</b>.<br>
     * On error the promise will be rejected with {@link Error} <b>error</b>.
     */
    Artifact.prototype.addFilesAsSoftLinks = function (files, callback) {
        var self = this,
            fileNames = Object.keys(files);

        return Q.all(fileNames.map(function (fileName) {
            return self.addFileAsSoftLink(fileName, files[fileName]);
        })).nodeify(callback);
    };

    /**
     * Adds hashes to the artifact using the given file paths.
     * @param {object.<string, string>} metadataHashes - Keys are file paths and values metadata hashes.
     * @param {function} [callback] - if provided no promise will be returned.
     *
     * @return {external:Promise}  On success the promise will be resolved with {string[]} <b>hashes</b>.<br>
     * On error the promise will be rejected with {@link Error} <b>error</b>.
     */
    Artifact.prototype.addObjectHashes = function (metadataHashes, callback) {
        var self = this,
            fileNames = Object.keys(metadataHashes);

        return Q.all(fileNames.map(function (fileName) {
            return self.addObjectHash(fileName, metadataHashes[fileName]);
        })).nodeify(callback);
    };

    /**
     * Adds hashes to the artifact using the given file paths.
     * @param {object.<string, string>} metadataHashes - Keys are file paths and values metadata hashes.
     * @param {function} [callback] - if provided no promise will be returned.
     *
     * @return {external:Promise}  On success the promise will be resolved with {string[]} <b>hashes</b>.<br>
     * On error the promise will be rejected with {@link Error} <b>error</b>.
     */
    Artifact.prototype.addMetadataHashes = function (metadataHashes, callback) {
        var self = this,
            fileNames = Object.keys(metadataHashes);

        return Q.all(fileNames.map(function (fileName) {
            return self.addMetadataHash(fileName, metadataHashes[fileName]);
        })).nodeify(callback);
    };

    /**
     * Saves this artifact and uploads the metadata to the server's storage.
     * @param {function} [callback] - if provided no promise will be returned.
     *
     * @return {external:Promise}  On success the promise will be resolved with {string} <b>metadataHash</b>.<br>
     * On error the promise will be rejected with {@link Error} <b>error</b>.
     */
    Artifact.prototype.save = function (callback) {
        var deferred = Q.defer();

        this.blobClient.putMetadata(this.descriptor, function (err, hash) {
            if (err) {
                deferred.reject(err);
            } else {
                deferred.resolve(hash);
            }
        });

        return deferred.promise.nodeify(callback);
    };

    return Artifact;
});