cli.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. },
  75. // The upload method is not correctly implemented
  76. // needs to get the liaison after creating a data object,
  77. // resolve the ipns id to the asset put api url of the storage-node
  78. // before uploading..
  79. 'upload': async (api, url, filename, do_type_id, keyfile, passphrase) => {
  80. load_identity(keyfile, passphrase)
  81. // Check parameters
  82. assert_file('file', filename)
  83. const size = fs.statSync(filename).size
  84. debug(`File "${filename}" is ${chalk.green(size)} Bytes.`)
  85. if (!do_type_id) {
  86. do_type_id = 1
  87. }
  88. debug('Data Object Type ID is: ' + chalk.green(do_type_id))
  89. // Generate content ID
  90. // FIXME this require path is like this because of
  91. // https://github.com/Joystream/apps/issues/207
  92. const { ContentId } = require('@joystream/types/lib/media')
  93. var cid = ContentId.generate()
  94. cid = cid.encode().toString()
  95. debug('Generated content ID: ' + chalk.green(cid))
  96. // Create Data Object
  97. const data_object = await api.assets.createDataObject(
  98. api.identities.key.address, cid, do_type_id, size)
  99. debug('Data object created.')
  100. // TODO in future, optionally contact liaison here?
  101. const request = require('request')
  102. url = `${url}asset/v0/${cid}`
  103. debug('Uploading to URL', chalk.green(url))
  104. const f = fs.createReadStream(filename)
  105. const opts = {
  106. url: url,
  107. headers: {
  108. 'content-type': '',
  109. 'content-length': `${size}`
  110. },
  111. json: true
  112. }
  113. return new Promise((resolve, reject) => {
  114. const r = request.put(opts, (error, response, body) => {
  115. if (error) {
  116. reject(error)
  117. return
  118. }
  119. if (response.statusCode / 100 !== 2) {
  120. reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
  121. return
  122. }
  123. debug('Upload successful:', body.message)
  124. resolve()
  125. })
  126. f.pipe(r)
  127. })
  128. },
  129. // needs to be updated to take a content id and resolve it a potential set
  130. // of providers that has it, and select one (possibly try more than one provider)
  131. // to fetch it from the get api url of a provider..
  132. 'download': async (api, url, content_id, filename) => {
  133. const request = require('request')
  134. url = `${url}asset/v0/${content_id}`
  135. debug('Downloading URL', chalk.green(url), 'to', chalk.green(filename))
  136. const f = fs.createWriteStream(filename)
  137. const opts = {
  138. url: url,
  139. json: true
  140. }
  141. return new Promise((resolve, reject) => {
  142. const r = request.get(opts, (error, response, body) => {
  143. if (error) {
  144. reject(error)
  145. return
  146. }
  147. debug('Downloading', chalk.green(response.headers['content-type']), 'of size', chalk.green(response.headers['content-length']), '...')
  148. f.on('error', (err) => {
  149. reject(err)
  150. })
  151. f.on('finish', () => {
  152. if (response.statusCode / 100 !== 2) {
  153. reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
  154. return
  155. }
  156. debug('Download completed.')
  157. resolve()
  158. })
  159. })
  160. r.pipe(f)
  161. })
  162. },
  163. // similar to 'download' function
  164. 'head': async (api, url, content_id) => {
  165. const request = require('request')
  166. url = `${url}asset/v0/${content_id}`
  167. debug('Checking URL', chalk.green(url), '...')
  168. const opts = {
  169. url: url,
  170. json: true
  171. }
  172. return new Promise((resolve, reject) => {
  173. const r = request.head(opts, (error, response, body) => {
  174. if (error) {
  175. reject(error)
  176. return
  177. }
  178. if (response.statusCode / 100 !== 2) {
  179. reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
  180. return
  181. }
  182. for (var propname in response.headers) {
  183. debug(` ${chalk.yellow(propname)}: ${response.headers[propname]}`)
  184. }
  185. resolve()
  186. })
  187. })
  188. }
  189. }
  190. async function main () {
  191. const api = await RuntimeApi.create()
  192. // Simple CLI commands
  193. const command = cli.input[0]
  194. if (!command) {
  195. throw new Error('Need a command to run!')
  196. }
  197. if (commands.hasOwnProperty(command)) {
  198. // Command recognized
  199. const args = _.clone(cli.input).slice(1)
  200. await commands[command](api, ...args)
  201. } else {
  202. throw new Error(`Command "${command}" not recognized, aborting!`)
  203. }
  204. }
  205. main()
  206. .then(() => {
  207. debug('Process exiting gracefully.')
  208. process.exit(0)
  209. })
  210. .catch((err) => {
  211. console.error(chalk.red(err.stack))
  212. process.exit(-1)
  213. })