index.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. 'use strict'
  2. const path = require('node:path')
  3. const crypto = require('node:crypto')
  4. require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') })
  5. const express = require('express')
  6. const app = express()
  7. const port = process.env.PORT ?? 8080
  8. const bodyParser = require('body-parser')
  9. let s3Client
  10. const aws = require('aws-sdk')
  11. const expires = 800 // Define how long until a S3 signature expires.
  12. function getS3Client () {
  13. s3Client ??= new aws.S3({
  14. signatureVersion: 'v4',
  15. region: process.env.COMPANION_AWS_REGION,
  16. credentials : new aws.Credentials(
  17. process.env.COMPANION_AWS_KEY,
  18. process.env.COMPANION_AWS_SECRET,
  19. ),
  20. })
  21. return s3Client
  22. }
  23. app.use(bodyParser.json())
  24. app.get('/', (req, res) => {
  25. const htmlPath = path.join(__dirname, 'public', 'index.html')
  26. res.sendFile(htmlPath)
  27. })
  28. app.get('/drag', (req, res) => {
  29. const htmlPath = path.join(__dirname, 'public', 'drag.html')
  30. res.sendFile(htmlPath)
  31. })
  32. app.post('/sign-s3', (req, res) => {
  33. const s3 = getS3Client()
  34. const Key = `${crypto.randomUUID()}-${req.body.filename}`
  35. const { contentType } = req.body
  36. const s3Params = {
  37. Bucket: process.env.COMPANION_AWS_BUCKET,
  38. Key,
  39. Expires: expires,
  40. ContentType: contentType,
  41. }
  42. s3.getSignedUrl('putObject', s3Params, (err, data, next) => {
  43. if (err) {
  44. next(err)
  45. return
  46. }
  47. const returnData = {
  48. url: data,
  49. method: 'PUT',
  50. }
  51. res.write(JSON.stringify(returnData))
  52. res.end()
  53. })
  54. })
  55. // === <S3 Multipart> ===
  56. // You can remove those endpoints if you only want to support the non-multipart uploads.
  57. app.post('/s3/multipart', (req, res, next) => {
  58. const client = getS3Client()
  59. const { type, metadata, filename } = req.body
  60. if (typeof filename !== 'string') {
  61. return res.status(400).json({ error: 's3: content filename must be a string' })
  62. }
  63. if (typeof type !== 'string') {
  64. return res.status(400).json({ error: 's3: content type must be a string' })
  65. }
  66. const Key = `${crypto.randomUUID()}-${filename}`
  67. const params = {
  68. Bucket: process.env.COMPANION_AWS_BUCKET,
  69. Key,
  70. ContentType: type,
  71. Metadata: metadata,
  72. }
  73. return client.createMultipartUpload(params, (err, data) => {
  74. if (err) {
  75. next(err)
  76. return
  77. }
  78. res.json({
  79. key: data.Key,
  80. uploadId: data.UploadId,
  81. })
  82. })
  83. })
  84. function validatePartNumber (partNumber) {
  85. // eslint-disable-next-line no-param-reassign
  86. partNumber = Number(partNumber)
  87. return Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 10_000
  88. }
  89. app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => {
  90. const client = getS3Client()
  91. const { uploadId, partNumber } = req.params
  92. const { key } = req.query
  93. if (!validatePartNumber(partNumber)) {
  94. return res.status(400).json({ error: 's3: the part number must be an integer between 1 and 10000.' })
  95. }
  96. if (typeof key !== 'string') {
  97. return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' })
  98. }
  99. return client.getSignedUrl('uploadPart', {
  100. Bucket: process.env.COMPANION_AWS_BUCKET,
  101. Key: key,
  102. UploadId: uploadId,
  103. PartNumber: partNumber,
  104. Body: '',
  105. Expires: expires,
  106. }, (err, url) => {
  107. if (err) {
  108. next(err)
  109. return
  110. }
  111. res.json({ url, expires })
  112. })
  113. })
  114. app.get('/s3/multipart/:uploadId', (req, res, next) => {
  115. const client = getS3Client()
  116. const { uploadId } = req.params
  117. const { key } = req.query
  118. if (typeof key !== 'string') {
  119. res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' })
  120. return
  121. }
  122. const parts = []
  123. function listPartsPage (startAt) {
  124. client.listParts({
  125. Bucket: process.env.COMPANION_AWS_BUCKET,
  126. Key: key,
  127. UploadId: uploadId,
  128. PartNumberMarker: startAt,
  129. }, (err, data) => {
  130. if (err) {
  131. next(err)
  132. return
  133. }
  134. parts.push(...data.Parts)
  135. if (data.IsTruncated) {
  136. // Get the next page.
  137. listPartsPage(data.NextPartNumberMarker)
  138. } else {
  139. res.json(parts)
  140. }
  141. })
  142. }
  143. listPartsPage(0)
  144. })
  145. function isValidPart (part) {
  146. return part && typeof part === 'object' && typeof part.PartNumber === 'number' && typeof part.ETag === 'string'
  147. }
  148. app.post('/s3/multipart/:uploadId/complete', (req, res, next) => {
  149. const client = getS3Client()
  150. const { uploadId } = req.params
  151. const { key } = req.query
  152. const { parts } = req.body
  153. if (typeof key !== 'string') {
  154. return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' })
  155. }
  156. if (!Array.isArray(parts) || !parts.every(isValidPart)) {
  157. return res.status(400).json({ error: 's3: `parts` must be an array of {ETag, PartNumber} objects.' })
  158. }
  159. return client.completeMultipartUpload({
  160. Bucket: process.env.COMPANION_AWS_BUCKET,
  161. Key: key,
  162. UploadId: uploadId,
  163. MultipartUpload: {
  164. Parts: parts,
  165. },
  166. }, (err, data) => {
  167. if (err) {
  168. next(err)
  169. return
  170. }
  171. res.json({
  172. location: data.Location,
  173. })
  174. })
  175. })
  176. app.delete('/s3/multipart/:uploadId', (req, res, next) => {
  177. const client = getS3Client()
  178. const { uploadId } = req.params
  179. const { key } = req.query
  180. if (typeof key !== 'string') {
  181. return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' })
  182. }
  183. return client.abortMultipartUpload({
  184. Bucket: process.env.COMPANION_AWS_BUCKET,
  185. Key: key,
  186. UploadId: uploadId,
  187. }, (err) => {
  188. if (err) {
  189. next(err)
  190. return
  191. }
  192. res.json({})
  193. })
  194. })
  195. // === </S3 MULTIPART> ===
  196. app.listen(port, () => {
  197. console.log(`Example app listening on port ${port}`)
  198. })