{id}.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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) {
  27. // Creat the IPFS HTTP Gateway proxy middleware
  28. const proxy = ipfsProxy.createProxy(storage)
  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. const id = req.params.id // content id
  45. // First check if we're the liaison for the name, otherwise we can bail
  46. // out already.
  47. const roleAddress = runtime.identities.key.address
  48. const providerId = runtime.storageProviderId
  49. let dataObject
  50. try {
  51. debug('calling checkLiaisonForDataObject')
  52. dataObject = await runtime.assets.checkLiaisonForDataObject(providerId, id)
  53. debug('called checkLiaisonForDataObject')
  54. } catch (err) {
  55. errorHandler(res, err, 403)
  56. return
  57. }
  58. const sufficientBalance = await runtime.providerHasMinimumBalance(3)
  59. if (!sufficientBalance) {
  60. errorHandler(res, 'Insufficient balance to process upload!', 503)
  61. return
  62. }
  63. // We'll open a write stream to the backend, but reserve the right to
  64. // abort upload if the filters don't smell right.
  65. let stream
  66. try {
  67. stream = await storage.open(id, 'w')
  68. // We don't know whether the filtering occurs before or after the
  69. // stream was finished, and can only commit if both passed.
  70. let finished = false
  71. let accepted = false
  72. const possiblyCommit = () => {
  73. if (finished && accepted) {
  74. debug('Stream is finished and passed filters; committing.')
  75. stream.commit()
  76. }
  77. }
  78. stream.on('fileInfo', async (info) => {
  79. try {
  80. debug('Detected file info:', info)
  81. // Filter
  82. const filterResult = filter({}, req.headers, info.mimeType)
  83. if (filterResult.code !== 200) {
  84. debug('Rejecting content', filterResult.message)
  85. stream.end()
  86. res.status(filterResult.code).send({ message: filterResult.message })
  87. // Reject the content
  88. await runtime.assets.rejectContent(roleAddress, providerId, id)
  89. return
  90. }
  91. debug('Content accepted.')
  92. accepted = true
  93. // We may have to commit the stream.
  94. possiblyCommit()
  95. } catch (err) {
  96. errorHandler(res, err)
  97. }
  98. })
  99. stream.on('finish', () => {
  100. try {
  101. finished = true
  102. possiblyCommit()
  103. } catch (err) {
  104. errorHandler(res, err)
  105. }
  106. })
  107. stream.on('committed', async (hash) => {
  108. console.log('commited', dataObject)
  109. try {
  110. if (hash !== dataObject.ipfs_content_id.toString()) {
  111. debug('Rejecting content. IPFS hash does not match value in objectId')
  112. await runtime.assets.rejectContent(roleAddress, providerId, id)
  113. res.status(400).send({ message: "Uploaded content doesn't match IPFS hash" })
  114. return
  115. }
  116. debug('accepting Content')
  117. await runtime.assets.acceptContent(roleAddress, providerId, id)
  118. debug('creating storage relationship for newly uploaded content')
  119. // Create storage relationship and flip it to ready.
  120. const dosrId = await runtime.assets.createStorageRelationship(roleAddress, providerId, id)
  121. debug('toggling storage relationship for newly uploaded content')
  122. await runtime.assets.toggleStorageRelationshipReady(roleAddress, providerId, dosrId, true)
  123. debug('Sending OK response.')
  124. res.status(200).send({ message: 'Asset uploaded.' })
  125. } catch (err) {
  126. debug(`${err.message}`)
  127. errorHandler(res, err)
  128. }
  129. })
  130. stream.on('error', (err) => errorHandler(res, err))
  131. req.pipe(stream)
  132. } catch (err) {
  133. errorHandler(res, err)
  134. }
  135. },
  136. async get(req, res) {
  137. proxy(req, res)
  138. },
  139. async head(req, res) {
  140. proxy(req, res)
  141. },
  142. }
  143. // doc.get = proxy
  144. // doc.head = proxy
  145. // Note: Adding the middleware this way is causing problems!
  146. // We are loosing some information from the request, specifically req.query.download parameters for some reason.
  147. // Does it have to do with how/when the apiDoc is being processed? binding issue?
  148. // OpenAPI specs
  149. doc.get.apiDoc = {
  150. description: 'Download an asset.',
  151. operationId: 'assetData',
  152. tags: ['asset', 'data'],
  153. parameters: [
  154. {
  155. name: 'download',
  156. in: 'query',
  157. description: 'Download instead of streaming inline.',
  158. required: false,
  159. allowEmptyValue: true,
  160. schema: {
  161. type: 'boolean',
  162. default: false,
  163. },
  164. },
  165. ],
  166. responses: {
  167. 200: {
  168. description: 'Asset download.',
  169. content: {
  170. default: {
  171. schema: {
  172. type: 'string',
  173. format: 'binary',
  174. },
  175. },
  176. },
  177. },
  178. default: {
  179. description: 'Unexpected error',
  180. content: {
  181. 'application/json': {
  182. schema: {
  183. $ref: '#/components/schemas/Error',
  184. },
  185. },
  186. },
  187. },
  188. },
  189. }
  190. doc.put.apiDoc = {
  191. description: 'Asset upload.',
  192. operationId: 'assetUpload',
  193. tags: ['asset', 'data'],
  194. requestBody: {
  195. content: {
  196. '*/*': {
  197. schema: {
  198. type: 'string',
  199. format: 'binary',
  200. },
  201. },
  202. },
  203. },
  204. responses: {
  205. 200: {
  206. description: 'Asset upload.',
  207. content: {
  208. 'application/json': {
  209. schema: {
  210. type: 'object',
  211. required: ['message'],
  212. properties: {
  213. message: {
  214. type: 'string',
  215. },
  216. },
  217. },
  218. },
  219. },
  220. },
  221. default: {
  222. description: 'Unexpected error',
  223. content: {
  224. 'application/json': {
  225. schema: {
  226. $ref: '#/components/schemas/Error',
  227. },
  228. },
  229. },
  230. },
  231. },
  232. }
  233. doc.head.apiDoc = {
  234. description: 'Asset download information.',
  235. operationId: 'assetInfo',
  236. tags: ['asset', 'metadata'],
  237. responses: {
  238. 200: {
  239. description: 'Asset info.',
  240. },
  241. default: {
  242. description: 'Unexpected error',
  243. content: {
  244. 'application/json': {
  245. schema: {
  246. $ref: '#/components/schemas/Error',
  247. },
  248. },
  249. },
  250. },
  251. },
  252. }
  253. return doc
  254. }