cli.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  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/storage-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. `
  34. Usage:
  35. $ storage-cli command [arguments..] [key_file] [passphrase]
  36. Some commands require a key file as the last option holding the identity for
  37. interacting with the runtime API.
  38. Commands:
  39. upload Upload a file to a Colossus storage node. Requires a
  40. storage node URL, and a local file name to upload. As
  41. an optional third parameter, you can provide a Data
  42. Object Type ID - this defaults to "1" if not provided.
  43. download Retrieve a file. Requires a storage node URL and a content
  44. ID, as well as an output filename.
  45. head Send a HEAD request for a file, and print headers.
  46. Requires a storage node URL and a content ID.
  47. Dev Commands: Commands to run on a development chain.
  48. dev-init Setup chain with Alice as lead and storage provider.
  49. dev-check Check the chain is setup with Alice as lead and storage provider.
  50. `,
  51. { flags: FLAG_DEFINITIONS }
  52. )
  53. function assertFile(name, filename) {
  54. assert(filename, `Need a ${name} parameter to proceed!`)
  55. assert(fs.statSync(filename).isFile(), `Path "${filename}" is not a file, aborting!`)
  56. }
  57. function loadIdentity(api, filename, passphrase) {
  58. if (filename) {
  59. assertFile('keyfile', filename)
  60. api.identities.loadUnlock(filename, passphrase)
  61. } else {
  62. debug('Loading Alice as identity')
  63. api.identities.useKeyPair(dev.aliceKeyPair(api))
  64. }
  65. }
  66. const commands = {
  67. // add Alice well known account as storage provider
  68. 'dev-init': async api => {
  69. // dev accounts are automatically loaded, no need to add explicitly to keyring using loadIdentity(api)
  70. const dev = require('./dev')
  71. return dev.init(api)
  72. },
  73. // Checks that the setup done by dev-init command was successful.
  74. 'dev-check': async api => {
  75. // dev accounts are automatically loaded, no need to add explicitly to keyring using loadIdentity(api)
  76. const dev = require('./dev')
  77. return dev.check(api)
  78. },
  79. // The upload method is not correctly implemented
  80. // needs to get the liaison after creating a data object,
  81. // resolve the ipns id to the asset put api url of the storage-node
  82. // before uploading..
  83. upload: async (api, url, filename, doTypeId, keyfile, passphrase) => {
  84. loadIdentity(keyfile, passphrase)
  85. // Check parameters
  86. assertFile('file', filename)
  87. const size = fs.statSync(filename).size
  88. debug(`File "${filename}" is ${chalk.green(size)} Bytes.`)
  89. if (!doTypeId) {
  90. doTypeId = 1
  91. }
  92. debug('Data Object Type ID is: ' + chalk.green(doTypeId))
  93. // Generate content ID
  94. // FIXME this require path is like this because of
  95. // https://github.com/Joystream/apps/issues/207
  96. const { ContentId } = require('@joystream/types/media')
  97. let cid = ContentId.generate()
  98. cid = cid.encode().toString()
  99. debug('Generated content ID: ' + chalk.green(cid))
  100. // Create Data Object
  101. await api.assets.createDataObject(api.identities.key.address, cid, doTypeId, size)
  102. debug('Data object created.')
  103. // TODO in future, optionally contact liaison here?
  104. const request = require('request')
  105. url = `${url}asset/v0/${cid}`
  106. debug('Uploading to URL', chalk.green(url))
  107. const f = fs.createReadStream(filename)
  108. const opts = {
  109. url,
  110. headers: {
  111. 'content-type': '',
  112. 'content-length': `${size}`,
  113. },
  114. json: true,
  115. }
  116. return new Promise((resolve, reject) => {
  117. const r = request.put(opts, (error, response, body) => {
  118. if (error) {
  119. reject(error)
  120. return
  121. }
  122. if (response.statusCode / 100 !== 2) {
  123. reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
  124. return
  125. }
  126. debug('Upload successful:', body.message)
  127. resolve()
  128. })
  129. f.pipe(r)
  130. })
  131. },
  132. // needs to be updated to take a content id and resolve it a potential set
  133. // of providers that has it, and select one (possibly try more than one provider)
  134. // to fetch it from the get api url of a provider..
  135. download: async (api, url, contentId, filename) => {
  136. const request = require('request')
  137. url = `${url}asset/v0/${contentId}`
  138. debug('Downloading URL', chalk.green(url), 'to', chalk.green(filename))
  139. const f = fs.createWriteStream(filename)
  140. const opts = {
  141. url,
  142. json: true,
  143. }
  144. return new Promise((resolve, reject) => {
  145. const r = request.get(opts, (error, response, body) => {
  146. if (error) {
  147. reject(error)
  148. return
  149. }
  150. debug(
  151. 'Downloading',
  152. chalk.green(response.headers['content-type']),
  153. 'of size',
  154. chalk.green(response.headers['content-length']),
  155. '...'
  156. )
  157. f.on('error', err => {
  158. reject(err)
  159. })
  160. f.on('finish', () => {
  161. if (response.statusCode / 100 !== 2) {
  162. reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
  163. return
  164. }
  165. debug('Download completed.')
  166. resolve()
  167. })
  168. })
  169. r.pipe(f)
  170. })
  171. },
  172. // similar to 'download' function
  173. head: async (api, url, contentId) => {
  174. const request = require('request')
  175. url = `${url}asset/v0/${contentId}`
  176. debug('Checking URL', chalk.green(url), '...')
  177. const opts = {
  178. url,
  179. json: true,
  180. }
  181. return new Promise((resolve, reject) => {
  182. request.head(opts, (error, response, body) => {
  183. if (error) {
  184. reject(error)
  185. return
  186. }
  187. if (response.statusCode / 100 !== 2) {
  188. reject(new Error(`${response.statusCode}: ${body.message || 'unknown reason'}`))
  189. return
  190. }
  191. for (const propname in response.headers) {
  192. debug(` ${chalk.yellow(propname)}: ${response.headers[propname]}`)
  193. }
  194. resolve()
  195. })
  196. })
  197. },
  198. }
  199. async function main() {
  200. const api = await RuntimeApi.create()
  201. // Simple CLI commands
  202. const command = cli.input[0]
  203. if (!command) {
  204. throw new Error('Need a command to run!')
  205. }
  206. if (Object.prototype.hasOwnProperty.call(commands, command)) {
  207. // Command recognized
  208. const args = _.clone(cli.input).slice(1)
  209. await commands[command](api, ...args)
  210. } else {
  211. throw new Error(`Command "${command}" not recognized, aborting!`)
  212. }
  213. }
  214. main()
  215. .then(() => {
  216. process.exit(0)
  217. })
  218. .catch(err => {
  219. console.error(chalk.red(err.stack))
  220. process.exit(-1)
  221. })