cli.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. #!/usr/bin/env node
  2. /*
  3. * This file is part of the storage node for the Joystream project.
  4. * Copyright (C) 2019 Joystream Contributors
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. 'use strict'
  20. const fs = require('fs')
  21. const assert = require('assert')
  22. const { RuntimeApi } = require('@joystream/runtime-api')
  23. const meow = require('meow')
  24. const chalk = require('chalk')
  25. const _ = require('lodash')
  26. const debug = require('debug')('joystream:storage-cli')
  27. const dev = require('./dev')
  28. // Parse CLI
  29. const FLAG_DEFINITIONS = {
  30. // TODO
  31. }
  32. const cli = meow(`
  33. Usage:
  34. $ storage-cli command [arguments..] [key_file] [passphrase]
  35. Some commands require a key file as the last option holding the identity for
  36. interacting with the runtime API.
  37. Commands:
  38. upload Upload a file to a Colossus storage node. Requires a
  39. storage node URL, and a local file name to upload. As
  40. an optional third parameter, you can provide a Data
  41. Object Type ID - this defaults to "1" if not provided.
  42. download Retrieve a file. Requires a storage node URL and a content
  43. ID, as well as an output filename.
  44. head Send a HEAD request for a file, and print headers.
  45. Requires a storage node URL and a content ID.
  46. Dev Commands: Commands to run on a development chain.
  47. dev-init Setup chain with Alice as lead and storage provider.
  48. dev-check Check the chain is setup with Alice as lead and storage provider.
  49. `,
  50. { flags: FLAG_DEFINITIONS })
  51. function assert_file (name, filename) {
  52. assert(filename, `Need a ${name} parameter to proceed!`)
  53. assert(fs.statSync(filename).isFile(), `Path "${filename}" is not a file, aborting!`)
  54. }
  55. function load_identity (api, filename, passphrase) {
  56. if (filename) {
  57. assert_file('keyfile', filename)
  58. api.identities.loadUnlock(filename, passphrase)
  59. } else {
  60. debug('Loading Alice as identity')
  61. api.identities.useKeyPair(dev.aliceKeyPair(api))
  62. }
  63. }
  64. const commands = {
  65. // add Alice well known account as storage provider
  66. 'dev-init': async (api) => {
  67. let dev = require('./dev')
  68. return dev.init(api)
  69. },
  70. // Checks that the setup done by dev-init command was successful.
  71. 'dev-check': async (api) => {
  72. let dev = require('./dev')
  73. return dev.check(api)
  74. // await api.assets.checkLiaisonForDataObject(providerId, '')
  75. },
  76. 'upload': async (api, url, filename, do_type_id, keyfile, passphrase) => {
  77. load_identity(keyfile, passphrase)
  78. // Check parameters
  79. assert_file('file', filename)
  80. const size = fs.statSync(filename).size
  81. debug(`File "${filename}" is ${chalk.green(size)} Bytes.`)
  82. if (!do_type_id) {
  83. do_type_id = 1
  84. }
  85. debug('Data Object Type ID is: ' + chalk.green(do_type_id))
  86. // Generate content ID
  87. // FIXME this require path is like this because of
  88. // https://github.com/Joystream/apps/issues/207
  89. const { ContentId } = require('@joystream/types/lib/media')
  90. var cid = ContentId.generate()
  91. cid = cid.encode().toString()
  92. debug('Generated content ID: ' + chalk.green(cid))
  93. // Create Data Object
  94. const data_object = await api.assets.createDataObject(
  95. api.identities.key.address, cid, do_type_id, size)
  96. debug('Data object created.')
  97. // TODO in future, optionally contact liaison here?
  98. const request = require('request')
  99. url = `${url}asset/v0/${cid}`
  100. debug('Uploading to URL', chalk.green(url))
  101. const f = fs.createReadStream(filename)
  102. const opts = {
  103. url: url,
  104. headers: {
  105. 'content-type': '',
  106. 'content-length': `${size}`
  107. },
  108. json: true
  109. }
  110. return new Promise((resolve, reject) => {
  111. const r = request.put(opts, (error, response, body) => {
  112. if (error) {
  113. reject(error)
  114. return
  115. }
  116. if (response.statusCode / 100 !== 2) {
  117. reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
  118. return
  119. }
  120. debug('Upload successful:', body.message)
  121. resolve()
  122. })
  123. f.pipe(r)
  124. })
  125. },
  126. 'download': async (api, url, content_id, filename) => {
  127. const request = require('request')
  128. url = `${url}asset/v0/${content_id}`
  129. debug('Downloading URL', chalk.green(url), 'to', chalk.green(filename))
  130. const f = fs.createWriteStream(filename)
  131. const opts = {
  132. url: url,
  133. json: true
  134. }
  135. return new Promise((resolve, reject) => {
  136. const r = request.get(opts, (error, response, body) => {
  137. if (error) {
  138. reject(error)
  139. return
  140. }
  141. debug('Downloading', chalk.green(response.headers['content-type']), 'of size', chalk.green(response.headers['content-length']), '...')
  142. f.on('error', (err) => {
  143. reject(err)
  144. })
  145. f.on('finish', () => {
  146. if (response.statusCode / 100 !== 2) {
  147. reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
  148. return
  149. }
  150. debug('Download completed.')
  151. resolve()
  152. })
  153. })
  154. r.pipe(f)
  155. })
  156. },
  157. 'head': async (api, url, content_id) => {
  158. const request = require('request')
  159. url = `${url}asset/v0/${content_id}`
  160. debug('Checking URL', chalk.green(url), '...')
  161. const opts = {
  162. url: url,
  163. json: true
  164. }
  165. return new Promise((resolve, reject) => {
  166. const r = request.head(opts, (error, response, body) => {
  167. if (error) {
  168. reject(error)
  169. return
  170. }
  171. if (response.statusCode / 100 !== 2) {
  172. reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
  173. return
  174. }
  175. for (var propname in response.headers) {
  176. debug(` ${chalk.yellow(propname)}: ${response.headers[propname]}`)
  177. }
  178. resolve()
  179. })
  180. })
  181. }
  182. }
  183. async function main () {
  184. const api = await RuntimeApi.create()
  185. // Simple CLI commands
  186. const command = cli.input[0]
  187. if (!command) {
  188. throw new Error('Need a command to run!')
  189. }
  190. if (commands.hasOwnProperty(command)) {
  191. // Command recognized
  192. const args = _.clone(cli.input).slice(1)
  193. await commands[command](api, ...args)
  194. } else {
  195. throw new Error(`Command "${command}" not recognized, aborting!`)
  196. }
  197. }
  198. main()
  199. .then(() => {
  200. debug('Process exiting gracefully.')
  201. process.exit(0)
  202. })
  203. .catch((err) => {
  204. console.error(chalk.red(err.stack))
  205. process.exit(-1)
  206. })