diff --git a/.gitignore b/.gitignore
index a3b5ad7..47fb82e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,4 @@ temp/*
# ignore files in folder
uploads/*
# except
-!uploads/README.md
\ No newline at end of file
+!uploads/README.md
diff --git a/client/src/pages/GroupPhoto/GroupPhoto.js b/client/src/pages/GroupPhoto/GroupPhoto.js
index 24a2b24..ebcbdbd 100644
--- a/client/src/pages/GroupPhoto/GroupPhoto.js
+++ b/client/src/pages/GroupPhoto/GroupPhoto.js
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
import Button from '../../components/Button'
import Page from '../../components/Page'
import { downloadFromS3 } from '../../utils/download'
+import { onWsEvent } from '../../websockets'
import './GroupPhoto.css'
const GroupPhoto = () => {
@@ -11,15 +12,12 @@ const GroupPhoto = () => {
const createGroupPhoto = async () => {
setIsGenerating(true)
- const res = await fetch('/createGroupPhoto', {
+ fetch('/createGroupPhoto', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
- const json = await res.json()
- setFile(json)
- setIsGenerating(false)
}
const getGroupPhoto = async () => {
@@ -36,6 +34,14 @@ const GroupPhoto = () => {
useEffect(() => {
getGroupPhoto()
+ onWsEvent('group-photo', (data) => {
+ if (data.status === 200) {
+ setFile(data.data)
+ } else {
+ console.log(data.message, data.error)
+ }
+ setIsGenerating(false)
+ })
}, [])
const header =
Create Group Photo
diff --git a/client/src/websockets.js b/client/src/websockets.js
new file mode 100644
index 0000000..6d70d52
--- /dev/null
+++ b/client/src/websockets.js
@@ -0,0 +1,12 @@
+const HOST = window.location.origin.replace(/^http/, 'ws')
+
+if (process.env.NODE_ENV === 'development') HOST.replace('3000', '3001')
+
+export const ws = new WebSocket(HOST)
+
+export const onWsEvent = (eventId, callback) => {
+ ws.onmessage = (event) => {
+ const data = JSON.parse(event.data)
+ if (data.id === eventId) callback(data)
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 6abfbca..596ebb9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -468,6 +468,13 @@
"url": "0.10.3",
"uuid": "3.3.2",
"xml2js": "0.4.19"
+ },
+ "dependencies": {
+ "uuid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+ }
}
},
"axe-core": {
@@ -476,14 +483,6 @@
"integrity": "sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==",
"dev": true
},
- "axios": {
- "version": "0.20.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.20.0.tgz",
- "integrity": "sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==",
- "requires": {
- "follow-redirects": "^1.10.0"
- }
- },
"axobject-query": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
@@ -735,6 +734,15 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
+ "bufferutil": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.1.tgz",
+ "integrity": "sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==",
+ "optional": true,
+ "requires": {
+ "node-gyp-build": "~3.7.0"
+ }
+ },
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
@@ -2101,11 +2109,6 @@
"which": "^1.1.1"
}
},
- "follow-redirects": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
- "integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
- },
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@@ -3419,6 +3422,12 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.0.0.tgz",
"integrity": "sha512-sSHCgWfJ+Lui/u+0msF3oyCgvdkhxDbkCS6Q8uiJquzOimkJBvX6hl5aSSA7DR1XbMpdM8r7phjcF63sF4rkKg=="
},
+ "node-gyp-build": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz",
+ "integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==",
+ "optional": true
+ },
"nodemon": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz",
@@ -4906,6 +4915,15 @@
"prepend-http": "^2.0.0"
}
},
+ "utf-8-validate": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.2.tgz",
+ "integrity": "sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==",
+ "optional": true,
+ "requires": {
+ "node-gyp-build": "~3.7.0"
+ }
+ },
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -4917,9 +4935,9 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
- "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
+ "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
},
"v8-compile-cache": {
"version": "2.1.1",
@@ -5082,6 +5100,11 @@
"typedarray-to-buffer": "^3.1.5"
}
},
+ "ws": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
+ "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
+ },
"xdg-basedir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
diff --git a/package.json b/package.json
index ebeca62..382acbf 100644
--- a/package.json
+++ b/package.json
@@ -17,14 +17,15 @@
"license": "ISC",
"dependencies": {
"aws-sdk": "^2.729.0",
- "axios": "^0.20.0",
"concurrently": "^5.2.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"fluent-ffmpeg": "^2.1.2",
"multer": "^1.4.2",
- "sharp": "^0.26.0"
+ "sharp": "^0.26.0",
+ "uuid": "^8.3.0",
+ "ws": "^7.3.1"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
@@ -52,5 +53,9 @@
"eslint --fix",
"prettier --write"
]
+ },
+ "optionalDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
}
}
diff --git a/server/server.js b/server/server.js
index 7caf4e8..6e7cec6 100644
--- a/server/server.js
+++ b/server/server.js
@@ -1,3 +1,4 @@
+/* eslint-disable-next-line global-require */
if (process.env.NODE_ENV !== 'production') require('dotenv').config()
const express = require('express')
@@ -7,10 +8,15 @@ const multer = require('multer')
const path = require('path')
const fs = require('fs')
const AWS = require('aws-sdk')
+const { Server } = require('ws')
+const { v4: uuidv4 } = require('uuid')
const config = require('../config')
-const { createGroupPhotoStream } = require('./utils/group-photo')
+const {
+ outputGifToJpeg,
+ createGroupPhotoStream,
+} = require('./utils/group-photo')
-const makeFileLocation = (file) =>
+const getFileLocation = (file) =>
`https://${config.AWS_BUCKET_NAME}.s3.amazonaws.com/${file.Key}`
const s3 = new AWS.S3({
@@ -35,9 +41,10 @@ const storage = multer.diskStorage({
cb(null, './uploads/')
},
filename(req, file, cb) {
- let filename = `${Date.now()}.${file.originalname.split('.')[1]}`
+ const id = uuidv4()
+ let filename = `${id}.${file.originalname.split('.')[1]}`
if (req.path === '/uploadBlob') {
- filename = `${Date.now()}.webm`
+ filename = `${id}.webm`
}
cb(null, filename)
},
@@ -46,11 +53,12 @@ const storage = multer.diskStorage({
const upload = multer({ storage })
const GREETING_PREFIX = 'public/gifs/greeting-'
+const PHOTO_PREFIX = 'public/photos/photo-'
-const listGifs = async () => {
+const listByPrefix = async (Prefix) => {
const params = {
Bucket: config.AWS_BUCKET_NAME,
- Prefix: GREETING_PREFIX,
+ Prefix,
}
const getAllContents = async (PrevContents = [], NextContinuationToken) => {
@@ -72,90 +80,64 @@ const listGifs = async () => {
const OrderedContents = Contents.map((file) => ({
...file,
- Location: makeFileLocation(file),
- })).reverse()
+ Location: getFileLocation(file),
+ })).sort((a, b) => b.LastModified - a.LastModified)
return OrderedContents
}
app.get('/listGifs', async (_, res) => {
try {
- const result = await listGifs()
+ const result = await listByPrefix(GREETING_PREFIX)
res.send(result)
} catch (e) {
console.log(e)
}
})
-const groupPhotoPath = 'public/group_photo.jpeg'
-
-app.post('/getGroupPhoto', async (_, res) => {
- const params = {
- Bucket: config.AWS_BUCKET_NAME,
- Prefix: groupPhotoPath,
- }
- const result = await s3.listObjects(params).promise()
- result.Contents = result.Contents.map((file) => ({
- ...file,
- Location: makeFileLocation(file),
- }))
- res.send(result)
-})
-
-app.post('/createGroupPhoto', async (_, res) => {
+const uploadGIF = async (res, filename, folderName, onSuccess = () => {}) => {
+ const filepath = `${folderName}/${filename}.gif`
try {
- const result = await listGifs()
- const urls = result.map((file) => makeFileLocation(file))
- const stream = await createGroupPhotoStream(urls)
- if (!stream) return
+ const fileStream = fs.createReadStream(filepath)
+ const GifKey = `${GREETING_PREFIX}${filename}.gif`
+ const PhotoKey = `${PHOTO_PREFIX}${filename}.jpeg`
+
const params = {
- Key: groupPhotoPath,
+ Key: GifKey,
Bucket: config.AWS_BUCKET_NAME,
- Body: stream,
- ContentType: 'image/png',
+ Body: fileStream,
+ ContentType: 'image/gif',
ACL: 'public-read',
}
- s3.upload(params, (err, data) => {
- if (err) {
- console.log(err, err.stack)
- } else {
- console.log(`Group Photo Uploaded to s3: ${groupPhotoPath}`)
- data.LastModified = Date.now()
- res.send(data)
- }
- })
- } catch (e) {
- console.log(e)
- }
-})
+ const data = await s3.upload(params).promise()
+ console.log('Uploaded user gif to', data.Location)
-const uploadGIF = async (res, filename, folderName, onSuccess) => {
- const filepath = `${folderName}/${filename}.gif`
- const fileStream = fs.createReadStream(filepath)
+ res.send(data)
+ onSuccess()
- const params = {
- Key: `${GREETING_PREFIX}${Date.now()}.gif`,
- Bucket: config.AWS_BUCKET_NAME,
- Body: fileStream,
- ContentType: 'image/gif',
- ACL: 'public-read',
- }
+ const jpegPath = await outputGifToJpeg(filepath)
+ const jpegStream = fs.createReadStream(jpegPath)
- await s3
- .upload(params)
- .promise()
- .then((data) => {
- console.log('s3.upload', data)
- res.send(data)
- fs.unlink(filepath, () =>
- console.log(`${filepath} was deleted after upload`),
- )
- if (onSuccess) onSuccess()
- })
- .catch((e) => {
- console.log(e, e.stack)
- res.status(500).send(e)
- })
+ // upload the middle page of the gif to s3 as a JPEG to enable faster processing of group photo
+ await s3
+ .upload({
+ ...params,
+ Key: PhotoKey,
+ Body: jpegStream,
+ ContentType: 'image/jpeg',
+ })
+ .promise()
+
+ fs.unlink(filepath, () =>
+ console.log(`${filepath} was deleted after upload`),
+ )
+ fs.unlink(jpegPath, () =>
+ console.log(`${jpegPath} was deleted after upload`),
+ )
+ } catch (e) {
+ console.log(e, e.stack, filepath)
+ res.status(500).send(e)
+ }
}
app.post('/uploadUserGIF', upload.single('gif'), async (req, res) => {
@@ -174,9 +156,8 @@ app.post('/uploadUserGIF', upload.single('gif'), async (req, res) => {
app.post('/uploadGIF', ({ body }, res) => {
const { filename } = body
uploadGIF(res, filename, 'temp', () => {
- fs.unlink(`uploads/${filename}.webm`, () =>
- console.log('.webm file was deleted'),
- )
+ const filepath = `uploads/${filename}.webm`
+ fs.unlink(filepath, () => console.log(`${filepath} was deleted`))
})
})
@@ -208,7 +189,7 @@ app.post('/video2gif', upload.none(), ({ body }, res) => {
res.send(body)
})
.on('error', (err) => {
- console.log(`an error happened: ${err.message}`)
+ console.log(`The following error occured: ${err.message}`)
res.send(err)
})
.save(`temp/${videoId}.gif`)
@@ -237,7 +218,7 @@ app.get('/download', (req, res) => {
const filepath = `temp/${filename}.gif`
res.download(filepath, (err) => {
if (err) console.log(err)
- console.log('Your file has been downloaded!')
+ console.log(`User downloaded ${filepath}`)
})
})
@@ -272,6 +253,85 @@ app.get('/*', (req, res) =>
res.sendFile(path.join(__dirname, '../client/build', 'index.html')),
)
-app.listen(app.get('port'), () => {
+const server = app.listen(app.get('port'), () => {
console.log(`Find the server at: http://localhost:${app.get('port')}/`)
})
+
+const wss = new Server({ server })
+
+const groupPhotoPath = 'public/group_photo.jpeg'
+
+app.post('/getGroupPhoto', async (_, res) => {
+ const params = {
+ Bucket: config.AWS_BUCKET_NAME,
+ Prefix: groupPhotoPath,
+ }
+ const result = await s3.listObjects(params).promise()
+ result.Contents = result.Contents.map((file) => ({
+ ...file,
+ Location: getFileLocation(file),
+ }))
+ res.send(result)
+})
+
+const fetchImageBuffer = (image) => {
+ const params = { Bucket: config.AWS_BUCKET_NAME, Key: image.Key }
+ return new Promise((resolve, reject) =>
+ s3.getObject(params, (error, result) =>
+ error ? reject(error) : resolve(result.Body),
+ ),
+ )
+}
+
+app.post('/createGroupPhoto', async (req, res) => {
+ res.send({ message: 'Starting group photo processing' })
+ try {
+ console.log('Starting group photo creation')
+ const images = await listByPrefix(PHOTO_PREFIX)
+ const buffers = await Promise.all(
+ images.map((image) => fetchImageBuffer(image)),
+ )
+ console.log('Group photo input buffers fetched from s3')
+ const stream = await createGroupPhotoStream(buffers)
+ if (!stream) return
+ const params = {
+ Key: groupPhotoPath,
+ Bucket: config.AWS_BUCKET_NAME,
+ Body: stream,
+ ContentType: 'image/png',
+ ACL: 'public-read',
+ }
+ s3.upload(params, (err, data) => {
+ if (err) {
+ console.log(err, err.stack)
+ } else {
+ console.log(`Group photo uploaded to s3: ${groupPhotoPath}`)
+ const _data = {
+ ...data,
+ LastModified: Date.now(),
+ }
+ wss.clients.forEach((client) => {
+ client.send(
+ JSON.stringify({
+ id: 'group-photo',
+ status: 200,
+ data: _data,
+ message: 'Group photo processed successfully',
+ }),
+ )
+ })
+ }
+ })
+ } catch (e) {
+ wss.clients.forEach((client) => {
+ client.send(
+ JSON.stringify({
+ id: 'group-photo',
+ status: 500,
+ message: 'Group photo processing failed',
+ error: e,
+ }),
+ )
+ })
+ }
+})
diff --git a/server/utils/group-photo.js b/server/utils/group-photo.js
index 2e8bee7..d1d03d9 100644
--- a/server/utils/group-photo.js
+++ b/server/utils/group-photo.js
@@ -1,5 +1,4 @@
const sharp = require('sharp')
-const axios = require('axios')
const fs = require('fs')
const chunkArray = (array, size) => {
@@ -9,23 +8,6 @@ const chunkArray = (array, size) => {
return [firstChunk].concat(chunkArray(array.slice(size, array.length), size))
}
-const fetchImg = async (url) => {
- try {
- const res = await axios({
- url,
- responseType: 'arraybuffer',
- })
- return res
- } catch (e) {
- return null
- }
-}
-
-const fetchImgs = async (imgs) => {
- const fetched = await Promise.all(imgs.map(fetchImg))
- return fetched.filter((img) => img !== null)
-}
-
const createImageLayout = (imgs) => {
const aspectRatio = 1 + 1 / 3
@@ -72,41 +54,31 @@ const createImageLayout = (imgs) => {
}
}
-const compositeGif = async (buffer, { width, height, top, left, id }) => {
- const toSizedBuffer = (frame) => frame.resize(width, height).raw().toBuffer()
-
- let input
- const firstFrame = await sharp(buffer)
+const createJpegComposite = async (buffer, { width, height, top, left }) => {
try {
- const { pages } = await firstFrame.metadata()
- const page = Math.round(pages / 2) || 0
- const middleFrame = await sharp(buffer, { page })
- input = await toSizedBuffer(middleFrame)
+ const resizedBuffer = await sharp(buffer)
+ .resize(width, height)
+ .raw()
+ .toBuffer({ resolveWithObject: true })
+
+ return {
+ input: resizedBuffer.data,
+ raw: resizedBuffer.info,
+ top,
+ left,
+ }
} catch (e) {
- // so far we have only seen faulty gifs cause this to catch
- console.log(
- 'Failed to slice middle GIF frame (or "page"), defaulting to first frame.',
- { id },
- )
- input = await toSizedBuffer(firstFrame)
- }
-
- return {
- input,
- raw: { width, height, channels: 4 },
- top,
- left,
+ console.log('Image Resize Failed')
+ return null
}
}
-const createGroupPhoto = async (urls) => {
+const createGroupPhoto = async (buffers) => {
const padding = 16
const brandingHeight = 80
const conferenceOutputPath = './temp/conference_logo.png'
- const imgs = await fetchImgs(urls.slice(0, 80))
-
- const layout = createImageLayout(imgs)
+ const layout = createImageLayout(buffers)
await sharp('./branding/Logo.png')
.resize(null, brandingHeight)
@@ -125,12 +97,11 @@ const createGroupPhoto = async (urls) => {
imgRows.map(async (row, i) => {
const composites = await Promise.all(
row.map((img) =>
- compositeGif(img.data.data, {
+ createJpegComposite(img.data, {
top: 0,
left: img.left,
width: layout.imgWidth,
height: layout.imgHeight,
- id: img.data.config.url,
}),
),
)
@@ -142,7 +113,7 @@ const createGroupPhoto = async (urls) => {
channels: 3,
},
})
- .composite(composites)
+ .composite(composites.filter((c) => c))
.toBuffer({ resolveWithObject: true })
return {
@@ -193,15 +164,15 @@ const createGroupPhoto = async (urls) => {
const outputPath = './temp/group-photo.jpeg'
-const createGroupPhotoStream = async (urls) => {
+const createGroupPhotoStream = async (buffers) => {
try {
- const groupPhoto = await createGroupPhoto(urls)
- console.log('Group Photo Processed')
+ const groupPhoto = await createGroupPhoto(buffers)
+ console.log('Group photo processed')
const jpeg = await groupPhoto.jpeg({
quality: 75,
})
await jpeg.toFile(outputPath)
- console.log(`Group Photo Output to ${outputPath}`)
+ console.log(`Group photo output to ${outputPath}`)
return fs.createReadStream(outputPath)
} catch (e) {
console.log(e)
@@ -209,4 +180,25 @@ const createGroupPhotoStream = async (urls) => {
}
}
-module.exports = { createGroupPhotoStream }
+const outputGifToJpeg = async (path) => {
+ let frame
+ const firstPage = await sharp(path)
+ try {
+ const { pages } = await firstPage.metadata()
+ const page = Math.round(pages / 2) || 0
+ const middlePage = await sharp(path, { page })
+ frame = middlePage
+ } catch (e) {
+ // so far we have only seen faulty gifs cause this to catch
+ console.log(
+ 'Failed to slice middle GIF page, defaulting to first page:',
+ path,
+ )
+ frame = firstPage
+ }
+ const filepath = path.replace('.gif', '.jpeg')
+ await frame.jpeg({ quality: 75 }).toFile(filepath)
+ return filepath
+}
+
+module.exports = { outputGifToJpeg, createGroupPhotoStream }
diff --git a/uploads/.gitkeep b/uploads/.gitkeep
new file mode 100644
index 0000000..e69de29