{id}.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /*
  2. * This file is part of the storage node for the Joystream project.
  3. * Copyright (C) 2019 Joystream Contributors
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. 'use strict'
  19. const debug = require('debug')('joystream:colossus:api:asset')
  20. const filter = require('@joystream/storage-node-backend/filter')
  21. const ipfsProxy = require('../../../lib/middleware/ipfs_proxy')
  22. function errorHandler(response, err, code) {
  23. debug(err)
  24. response.status(err.code || code || 500).send({ message: err.toString() })
  25. }
  26. module.exports = function (storage, runtime, ipfsHttpGatewayUrl, anonymous) {
  27. // Creat the IPFS HTTP Gateway proxy middleware
  28. const proxy = ipfsProxy.createProxy(storage, ipfsHttpGatewayUrl)
  29. const doc = {
  30. // parameters for all operations in this path
  31. parameters: [
  32. {
  33. name: 'id',
  34. in: 'path',
  35. required: true,
  36. description: 'Joystream Content ID',
  37. schema: {
  38. type: 'string',
  39. },
  40. },
  41. ],
  42. // Put for uploads
  43. async put(req, res) {
  44. if (anonymous) {
  45. errorHandler(res, 'Uploads Not Permitted in Anonymous Mode', 400)
  46. return
  47. }
  48. const id = req.params.id // content id
  49. // First check if we're the liaison for the name, otherwise we can bail
  50. // out already.
  51. const roleAddress = runtime.identities.key.address
  52. const providerId = runtime.storageProviderId
  53. let dataObject
  54. try {
  55. debug('calling checkLiaisonForDataObject')
  56. dataObject = await runtime.assets.checkLiaisonForDataObject(providerId, id)
  57. debug('called checkLiaisonForDataObject')
  58. } catch (err) {
  59. errorHandler(res, err, 403)
  60. return
  61. }
  62. const sufficientBalance = await runtime.providerHasMinimumBalance(3)
  63. if (!sufficientBalance) {
  64. errorHandler(res, 'Insufficient balance to process upload!', 503)
  65. return
  66. }
  67. // We'll open a write stream to the backend, but reserve the right to
  68. // abort upload if the filters don't smell right.
  69. let stream
  70. try {
  71. stream = await storage.open(id, 'w')
  72. // We don't know whether the filtering occurs before or after the
  73. // stream was finished, and can only commit if both passed.
  74. let finished = false
  75. let accepted = false
  76. const possiblyCommit = () => {
  77. if (finished && accepted) {
  78. debug('Stream is finished and passed filters; committing.')
  79. stream.commit()
  80. }
  81. }
  82. stream.on('fileInfo', async (info) => {
  83. try {
  84. debug('Detected file info:', info)
  85. // Filter
  86. const filterResult = filter({}, req.headers, info.mimeType)
  87. if (filterResult.code !== 200) {
  88. debug('Rejecting content', filterResult.message)
  89. stream.end()
  90. res.status(filterResult.code).send({ message: filterResult.message })
  91. // Reject the content
  92. await runtime.assets.rejectContent(roleAddress, providerId, id)
  93. return
  94. }
  95. debug('Content accepted.')
  96. accepted = true
  97. // We may have to commit the stream.
  98. possiblyCommit()
  99. } catch (err) {
  100. errorHandler(res, err)
  101. }
  102. })
  103. stream.on('finish', () => {
  104. try {
  105. finished = true
  106. possiblyCommit()
  107. } catch (err) {
  108. errorHandler(res, err)
  109. }
  110. })
  111. stream.on('committed', async (hash) => {
  112. console.log('commited', dataObject)
  113. try {
  114. if (hash !== dataObject.ipfs_content_id.toString()) {
  115. debug('Rejecting content. IPFS hash does not match value in objectId')
  116. await runtime.assets.rejectContent(roleAddress, providerId, id)
  117. res.status(400).send({ message: "Uploaded content doesn't match IPFS hash" })
  118. return
  119. }
  120. debug('accepting Content')
  121. await runtime.assets.acceptContent(roleAddress, providerId, id)
  122. debug('creating storage relationship for newly uploaded content')
  123. // Create storage relationship and flip it to ready.
  124. const dosrId = await runtime.assets.createStorageRelationship(roleAddress, providerId, id)
  125. debug('toggling storage relationship for newly uploaded content')
  126. await runtime.assets.toggleStorageRelationshipReady(roleAddress, providerId, dosrId, true)
  127. debug('Sending OK response.')
  128. res.status(200).send({ message: 'Asset uploaded.' })
  129. } catch (err) {
  130. debug(`${err.message}`)
  131. errorHandler(res, err)
  132. }
  133. })
  134. stream.on('error', (err) => errorHandler(res, err))
  135. req.pipe(stream)
  136. } catch (err) {
  137. errorHandler(res, err)
  138. }
  139. },
  140. async get(req, res) {
  141. proxy(req, res)
  142. },
  143. async head(req, res) {
  144. proxy(req, res)
  145. },
  146. }
  147. // doc.get = proxy
  148. // doc.head = proxy
  149. // Note: Adding the middleware this way is causing problems!
  150. // We are loosing some information from the request, specifically req.query.download parameters for some reason.
  151. // Does it have to do with how/when the apiDoc is being processed? binding issue?
  152. // OpenAPI specs
  153. doc.get.apiDoc = {
  154. description: 'Download an asset.',
  155. operationId: 'assetData',
  156. tags: ['asset', 'data'],
  157. parameters: [
  158. {
  159. name: 'download',
  160. in: 'query',
  161. description: 'Download instead of streaming inline.',
  162. required: false,
  163. allowEmptyValue: true,
  164. schema: {
  165. type: 'boolean',
  166. default: false,
  167. },
  168. },
  169. ],
  170. responses: {
  171. 200: {
  172. description: 'Asset download.',
  173. content: {
  174. default: {
  175. schema: {
  176. type: 'string',
  177. format: 'binary',
  178. },
  179. },
  180. },
  181. },
  182. default: {
  183. description: 'Unexpected error',
  184. content: {
  185. 'application/json': {
  186. schema: {
  187. $ref: '#/components/schemas/Error',
  188. },
  189. },
  190. },
  191. },
  192. },
  193. }
  194. doc.put.apiDoc = {
  195. description: 'Asset upload.',
  196. operationId: 'assetUpload',
  197. tags: ['asset', 'data'],
  198. requestBody: {
  199. content: {
  200. '*/*': {
  201. schema: {
  202. type: 'string',
  203. format: 'binary',
  204. },
  205. },
  206. },
  207. },
  208. responses: {
  209. 200: {
  210. description: 'Asset upload.',
  211. content: {
  212. 'application/json': {
  213. schema: {
  214. type: 'object',
  215. required: ['message'],
  216. properties: {
  217. message: {
  218. type: 'string',
  219. },
  220. },
  221. },
  222. },
  223. },
  224. },
  225. default: {
  226. description: 'Unexpected error',
  227. content: {
  228. 'application/json': {
  229. schema: {
  230. $ref: '#/components/schemas/Error',
  231. },
  232. },
  233. },
  234. },
  235. },
  236. }
  237. doc.head.apiDoc = {
  238. description: 'Asset download information.',
  239. operationId: 'assetInfo',
  240. tags: ['asset', 'metadata'],
  241. responses: {
  242. 200: {
  243. description: 'Asset info.',
  244. },
  245. default: {
  246. description: 'Unexpected error',
  247. content: {
  248. 'application/json': {
  249. schema: {
  250. $ref: '#/components/schemas/Error',
  251. },
  252. },
  253. },
  254. },
  255. },
  256. }
  257. return doc
  258. }