cli.js 7.6 KB

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