#!/usr/bin/env node /* * This file is part of the storage node for the Joystream project. * Copyright (C) 2019 Joystream Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ 'use strict' const fs = require('fs') const assert = require('assert') const { RuntimeApi } = require('@joystream/storage-runtime-api') const meow = require('meow') const chalk = require('chalk') const _ = require('lodash') const debug = require('debug')('joystream:storage-cli') const dev = require('./dev') // Parse CLI const FLAG_DEFINITIONS = { // TODO } const cli = meow( ` Usage: $ storage-cli command [arguments..] [key_file] [passphrase] Some commands require a key file as the last option holding the identity for interacting with the runtime API. Commands: upload Upload a file to a Colossus storage node. Requires a storage node URL, and a local file name to upload. As an optional third parameter, you can provide a Data Object Type ID - this defaults to "1" if not provided. download Retrieve a file. Requires a storage node URL and a content ID, as well as an output filename. head Send a HEAD request for a file, and print headers. Requires a storage node URL and a content ID. Dev Commands: Commands to run on a development chain. dev-init Setup chain with Alice as lead and storage provider. dev-check Check the chain is setup with Alice as lead and storage provider. `, { flags: FLAG_DEFINITIONS } ) function assertFile(name, filename) { assert(filename, `Need a ${name} parameter to proceed!`) assert(fs.statSync(filename).isFile(), `Path "${filename}" is not a file, aborting!`) } function loadIdentity(api, filename, passphrase) { if (filename) { assertFile('keyfile', filename) api.identities.loadUnlock(filename, passphrase) } else { debug('Loading Alice as identity') api.identities.useKeyPair(dev.aliceKeyPair(api)) } } const commands = { // add Alice well known account as storage provider 'dev-init': async api => { // dev accounts are automatically loaded, no need to add explicitly to keyring using loadIdentity(api) const dev = require('./dev') return dev.init(api) }, // Checks that the setup done by dev-init command was successful. 'dev-check': async api => { // dev accounts are automatically loaded, no need to add explicitly to keyring using loadIdentity(api) const dev = require('./dev') return dev.check(api) }, // The upload method is not correctly implemented // needs to get the liaison after creating a data object, // resolve the ipns id to the asset put api url of the storage-node // before uploading.. upload: async (api, url, filename, doTypeId, keyfile, passphrase) => { loadIdentity(keyfile, passphrase) // Check parameters assertFile('file', filename) const size = fs.statSync(filename).size debug(`File "${filename}" is ${chalk.green(size)} Bytes.`) if (!doTypeId) { doTypeId = 1 } debug('Data Object Type ID is: ' + chalk.green(doTypeId)) // Generate content ID // FIXME this require path is like this because of // https://github.com/Joystream/apps/issues/207 const { ContentId } = require('@joystream/types/media') let cid = ContentId.generate() cid = cid.encode().toString() debug('Generated content ID: ' + chalk.green(cid)) // Create Data Object await api.assets.createDataObject(api.identities.key.address, cid, doTypeId, size) debug('Data object created.') // TODO in future, optionally contact liaison here? const request = require('request') url = `${url}asset/v0/${cid}` debug('Uploading to URL', chalk.green(url)) const f = fs.createReadStream(filename) const opts = { url, headers: { 'content-type': '', 'content-length': `${size}`, }, json: true, } return new Promise((resolve, reject) => { const r = request.put(opts, (error, response, body) => { if (error) { reject(error) return } if (response.statusCode / 100 !== 2) { reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`)) return } debug('Upload successful:', body.message) resolve() }) f.pipe(r) }) }, // needs to be updated to take a content id and resolve it a potential set // of providers that has it, and select one (possibly try more than one provider) // to fetch it from the get api url of a provider.. download: async (api, url, contentId, filename) => { const request = require('request') url = `${url}asset/v0/${contentId}` debug('Downloading URL', chalk.green(url), 'to', chalk.green(filename)) const f = fs.createWriteStream(filename) const opts = { url, json: true, } return new Promise((resolve, reject) => { const r = request.get(opts, (error, response, body) => { if (error) { reject(error) return } debug( 'Downloading', chalk.green(response.headers['content-type']), 'of size', chalk.green(response.headers['content-length']), '...' ) f.on('error', err => { reject(err) }) f.on('finish', () => { if (response.statusCode / 100 !== 2) { reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`)) return } debug('Download completed.') resolve() }) }) r.pipe(f) }) }, // similar to 'download' function head: async (api, url, contentId) => { const request = require('request') url = `${url}asset/v0/${contentId}` debug('Checking URL', chalk.green(url), '...') const opts = { url, json: true, } return new Promise((resolve, reject) => { request.head(opts, (error, response, body) => { if (error) { reject(error) return } if (response.statusCode / 100 !== 2) { reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`)) return } for (const propname in response.headers) { debug(` ${chalk.yellow(propname)}: ${response.headers[propname]}`) } resolve() }) }) }, } async function main() { const api = await RuntimeApi.create() // Simple CLI commands const command = cli.input[0] if (!command) { throw new Error('Need a command to run!') } if (Object.prototype.hasOwnProperty.call(commands, command)) { // Command recognized const args = _.clone(cli.input).slice(1) await commands[command](api, ...args) } else { throw new Error(`Command "${command}" not recognized, aborting!`) } } main() .then(() => { process.exit(0) }) .catch(err => { console.error(chalk.red(err.stack)) process.exit(-1) })