diff --git a/.gitignore b/.gitignore index caa6f98fa..d771a4658 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ yarn.lock package.json package-lock.json db.json -server/db backup.json +db backup.json diff --git a/client/src/app/api/apiConstants.js b/client/src/app/api/apiConstants.js index 33a92277f..ba1a35c67 100644 --- a/client/src/app/api/apiConstants.js +++ b/client/src/app/api/apiConstants.js @@ -10,3 +10,4 @@ export const serverURL = calculateServer(); export const eventURL = serverURL + EVENT_TABLE; export const eventsURL = serverURL + EVENTS_TABLE; export const playbackURL = serverURL + 'playback'; +export const ontimeURL = serverURL + 'ontime'; diff --git a/client/src/app/api/ontimeApi.js b/client/src/app/api/ontimeApi.js new file mode 100644 index 000000000..4ebc5f73b --- /dev/null +++ b/client/src/app/api/ontimeApi.js @@ -0,0 +1,42 @@ +import axios from 'axios'; +import { ontimeURL } from './apiConstants'; + +export const downloadEvents = async () => { + await axios({ + url: ontimeURL + '/db', + method: 'GET', + responseType: 'blob', // important + }).then((response) => { + let headerLine = response.headers['Content-Disposition']; + console.log(response); + let filename = 'events.json'; + + // try and get the filename from the response + if (headerLine != null) { + let startFileNameIndex = headerLine.indexOf('"') + 1; + let endFileNameIndex = headerLine.lastIndexOf('"'); + filename = headerLine.substring(startFileNameIndex, endFileNameIndex); + } + + const url = window.URL.createObjectURL(new Blob([response.data])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', filename); + document.body.appendChild(link); + link.click(); + }); +}; + +export const uploadEvents = async (file) => { + console.log('uploading', file); + const formData = new FormData(); + formData.append('jsondb', file); // appending file + await axios + .post(ontimeURL + '/db', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + .then((res) => console.log(res.data)) + .catch((err) => console.error(err)); +}; diff --git a/client/src/common/components/buttons/DownloadIconBtn.jsx b/client/src/common/components/buttons/DownloadIconBtn.jsx new file mode 100644 index 000000000..8b30023b4 --- /dev/null +++ b/client/src/common/components/buttons/DownloadIconBtn.jsx @@ -0,0 +1,18 @@ +import { IconButton } from '@chakra-ui/button'; +import { FiDownload } from 'react-icons/fi'; + +export default function DownloadIconBtn(props) { + const { clickhandler, ...rest } = props; + return ( + } + isRound + variant='outline' + onClick={clickhandler} + _focus={{ boxShadow: 'none' }} + colorScheme='whiteAlpha' + {...rest} + /> + ); +} diff --git a/client/src/features/editors/Editor.jsx b/client/src/features/editors/Editor.jsx index bc727071b..586f2bd2e 100644 --- a/client/src/features/editors/Editor.jsx +++ b/client/src/features/editors/Editor.jsx @@ -9,6 +9,8 @@ import { useDisclosure } from '@chakra-ui/hooks'; import SettingsModal from '../modals/SettingsModal'; import SettingsIconBtn from 'common/components/buttons/SettingsIconBtn'; import { useEffect } from 'react'; +import DownloadIconBtn from 'common/components/buttons/DownloadIconBtn'; +import { downloadEvents } from 'app/api/ontimeApi'; export default function Editor() { const { isOpen, onOpen, onClose } = useDisclosure(); @@ -18,6 +20,10 @@ export default function Editor() { document.title = 'ontime - Editor'; }, []); + const handleDownload = () => { + downloadEvents(); + }; + return ( <> @@ -67,6 +73,7 @@ export default function Editor() {
+
diff --git a/client/src/features/editors/Editor.module.css b/client/src/features/editors/Editor.module.css index 73563ae0c..d335b7aaa 100644 --- a/client/src/features/editors/Editor.module.css +++ b/client/src/features/editors/Editor.module.css @@ -107,6 +107,12 @@ padding-top: 1.5em; } +.settings > .content { + display: flex; + flex-direction: column; + gap: 1em; +} + .cornerButtonContainer { position: relative; top: -4.5em; diff --git a/server/app.js b/server/app.js deleted file mode 100644 index 3210ded28..000000000 --- a/server/app.js +++ /dev/null @@ -1,80 +0,0 @@ -// get config -const config = require('./config.json'); - -// init database -const low = require('lowdb'); -const FileSync = require('lowdb/adapters/FileSync'); - -const adapter = new FileSync(config.database.filename); -const db = low(adapter); - -// dependencies -const express = require('express'); -const http = require('http'); -const cors = require('cors'); -const { dbModel } = require('./data/dataModel.js'); - -db.defaults(dbModel).write(); - -// export db -module.exports.db = db; - -// Import Routes -const eventsRouter = require('./routes/eventsRouter.js'); -const eventRouter = require('./routes/eventRouter.js'); -// No settings yet -// const settingsRouter = require('./routes/settingsRouter.js'); - -// Setup default port -const port = process.env.PORT || config.server.port; - -// Global Objects -const EventTimer = require('./classes/EventTimer.js'); - -// Create express APP -const app = express(); - -// setup cors for all routes -app.use(cors()); - -// enable pre-flight cors -app.options('*', cors()); - -// Implement middleware -app.use(express.urlencoded({ extended: true })); -app.use(express.json()); - -// Implement route endpoints -app.use('/events', eventsRouter); -app.use('/event', eventRouter); - -// implement general router -app.get('/', (req, res) => { - res.send('ontime API'); -}); - -// Implement route for errors -app.use((err, req, res, next) => { - console.error(err.stack); - res.status(500).send('Something broke!'); -}); - -// create HTTP server -const server = http.createServer(app); - -// get data (if any) -const eventlist = db.get('events').value(); - -// init timer -global.timer = new EventTimer(server, config); -global.timer.setupWithEventList(eventlist); - -// Start server -server.listen(port, () => - console.log(`HTTP Server is listening on port ${port}`) -); - -// Start OSC server -const { initiateOSC } = require('./controllers/OscController.js'); - -initiateOSC(config.osc); diff --git a/server/config.json b/server/config.json deleted file mode 100644 index 40d18e4a8..000000000 --- a/server/config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "timer": { - "refresh": 1000 - }, - "server": { - "port": 4001 - }, - "database": { - "filename": "db.json", - "tablename": "events" - }, - "osc": { - "port": 8888 - } -} \ No newline at end of file diff --git a/server/controllers/eventController.js b/server/controllers/eventController.js deleted file mode 100644 index f78e4204e..000000000 --- a/server/controllers/eventController.js +++ /dev/null @@ -1,144 +0,0 @@ -// get database -const { db } = require('../app.js'); - -const table = 'event'; - -function getSettings() { - return db.get(table).value(); -} - -// Create controller for GET request to 'event' -// Returns ACK message -exports.getAll = async (req, res) => { - const settings = getSettings(); - if (settings) res.json(settings); - else res.sendStatus(400); -}; - -// Create controller for POST request to 'event' -// Returns ACK message -exports.post = async (req, res) => { - if (!req.body) { - res.status(400).send('No object found in request'); - return; - } - // TODO: validate data - try { - db.get(table) - .assign({ ...req.body }) - .write(); - res.sendStatus(200); - } catch (error) { - res.status(400).send(error); - } -}; - -// Create controller for GET request to 'event/title' -// Returns ACK message -exports.titleGet = async (req, res) => { - const settings = getSettings(); - - if (settings) res.json(settings.title); - else res.sendStatus(400); -}; - -// Create controller for POST request to 'event/title' -// Returns ACK message -exports.titlePost = async (req, res) => { - if (!req.body) { - res.status(400).send('No object found in request'); - return; - } - - // TODO: validate data - try { - db.get(table).assign({ title: req.body.title }).write(); - res.sendStatus(200); - } catch (error) { - res.status(400).send(error); - } -}; - -// Create controller for GET request to '/event/url' -// Returns ACK message -exports.urlGet = async (req, res) => { - const event = getSettings(); - - if (event) res.json(event.url); - else res.sendStatus(400); -}; - -// Create controller for POST request to '/event/url' -// Returns ACK message -exports.urlPost = async (req, res) => { - if (!req.body) { - res.status(400).send('No object found in request'); - return; - } - - // TODO: validate data - try { - db.get(table).assign({ url: req.body.url }).write(); - res.sendStatus(200); - } catch (error) { - res.status(400).send(error); - } -}; - -// Create controller for GET request to 'event/publicInfo' -// Returns ACK message -exports.publicInfoGet = async (req, res) => { - const settings = getSettings(); - - if (settings) res.json(settings.publicInfo); - else res.sendStatus(400); -}; - -// Create controller for POST request to '/event/publicInfo' -// Returns ACK message -exports.publicInfoPost = async (req, res) => { - if (!req.body) { - res.status(400).send('No object found in request'); - return; - } - - // TODO: validate data - try { - db.get(table).assign({ publicInfo: req.body.publicInfo }).write(); - res.sendStatus(200); - } catch (error) { - res.status(400).send(error); - } -}; - -// Create controller for GET request to 'event/backstageInfo' -// Returns ACK message -exports.backstageInfoGet = async (req, res) => { - const settings = getSettings(); - - if (settings) res.json(settings.backstageInfo); - else res.sendStatus(400); -}; - -// Create controller for POST request to '/event/info' -// Returns ACK message -exports.backstageInfoPost = async (req, res) => { - if (!req.body) { - res.status(400).send('No object found in request'); - return; - } - - // TODO: validate data - try { - db.get(table).assign({ backstageInfo: req.body.backstageInfo }).write(); - res.sendStatus(200); - } catch (error) { - res.status(400).send(error); - } -}; - -// Create controller for GET request to 'event/osc' -// Returns ACK message -exports.osc = async (req, res) => { - res.send('Not yet implemented').status(500); -}; diff --git a/server/data/.gitkeep b/server/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/server/data/eventsData.json b/server/data/eventsData.json deleted file mode 100644 index f5aadad75..000000000 --- a/server/data/eventsData.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "id":"xxxxx0", - "duration": 300000, - "type": "delay" - }, - { - "id":"xxxxx1", - "title": "Is the internet a fad?", - "subtitle": "It is", - "presenter": "Carlos Valente", - "timeStart": 32400000, - "timeEnd": 34200000, - "type": "event" - }, - { - "id":"xxxxx2", - "duration": 1500000, - "type": "delay" - }, - { - "id":"xxxxx3", - "title": "Is reddit a dictatorship?", - "subtitle": "It is", - "presenter": "Carlos Valente", - "timeStart": 34200000, - "timeEnd": 36000000, - "type": "event" - }, - { - "id":"xxxxx4", - "title": "Out of words", - "subtitle": "", - "presenter": "Carlos Valente", - "timeStart": 36000000, - "timeEnd": 39600000, - "type": "event" - }, - { - "id":"xxxxx5", - "title": "Really", - "subtitle": "", - "presenter": "Carlos Valente", - "timeStart": 39600000, - "timeEnd": 41400000, - "type": "event" - }, - { - "id":"xxxxx6", - "title": "...", - "subtitle": "", - "presenter": "Carlos Valente", - "timeStart": null, - "timeEnd": null, - "type": "event" - } -] \ No newline at end of file diff --git a/server/data/playbackData.json b/server/data/playbackData.json deleted file mode 100644 index 0337f8752..000000000 --- a/server/data/playbackData.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "current": null, - "next": null, - "currentTimer": null, - "numEvents": 0, - "state": "pause", - "prevState": "pause" -} \ No newline at end of file diff --git a/server/jsconfig.json b/server/jsconfig.json new file mode 100644 index 000000000..bbf408cd7 --- /dev/null +++ b/server/jsconfig.json @@ -0,0 +1,3 @@ +{ + "exclude": ["node_modules", "dist"] +} diff --git a/server/package.json b/server/package.json index 8ecb67af8..6ff8e68f8 100644 --- a/server/package.json +++ b/server/package.json @@ -2,11 +2,13 @@ "name": "passport-example", "version": "0.0.1", "description": "Example with Passport (http://www.passportjs.org/)", + "type": "module", "dependencies": { "body-parser": "~1.19.0", "express": "~4.17.1", "express-session": "~1.17.1", - "lowdb": "^1.0.0", + "lowdb": "2.1.0", + "multer": "^1.4.2", "nanoid": "^3.1.22", "node-osc": "6.0.2", "passport": "~0.4.1", @@ -14,6 +16,6 @@ "socket.io": "^4.0.0" }, "scripts": { - "start": "node app.js" + "start": "node src/app.js" } } diff --git a/server/routes/eventRouter.js b/server/routes/eventRouter.js deleted file mode 100644 index 327fe30b5..000000000 --- a/server/routes/eventRouter.js +++ /dev/null @@ -1,37 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -// import event controller -const eventController = require('../controllers/eventController'); - -// create route between controller and '/settings' endpoint -router.get('/', eventController.getAll); - -// create route between controller and '/settings' endpoint -router.post('/', eventController.post); - -// create route between controller and '/event/title' endpoint -router.get('/title', eventController.titleGet); - -// create route between controller and '/event/title' endpoint -router.post('/title', eventController.titlePost); - -// create route between controller and '/event/info' endpoint -router.get('/publicInfo', eventController.publicInfoGet); - -// create route between controller and '/event/info' endpoint -router.post('/publicInfo', eventController.publicInfoPost); - -// create route between controller and '/event/info' endpoint -router.get('/backstageInfo', eventController.backstageInfoGet); - -// create route between controller and '/event/info' endpoint -router.post('/backstageInfo', eventController.backstageInfoPost); - -// create route between controller and '/event/url' endpoint -router.get('/url', eventController.urlGet); - -// create route between controller and '/event/url' endpoint -router.post('/url', eventController.urlPost); - -module.exports = router; diff --git a/server/routes/eventsRouter.js b/server/routes/eventsRouter.js deleted file mode 100644 index b8bf90cd9..000000000 --- a/server/routes/eventsRouter.js +++ /dev/null @@ -1,34 +0,0 @@ -const express = require('express'); -const router = express.Router(); - -// import events controller -const eventsController = require('../controllers/eventsController'); - -// create route between controller and '/events/' endpoint -router.get('/', eventsController.eventsGetAll); - -// create route between controller and '/events/:eventId' endpoint -router.get('/:eventId', eventsController.eventsGetById); - -// create route between controller and '/events/' endpoint -router.post('/', eventsController.eventsPost); - -// create route between controller and '/events/' endpoint -router.put('/', eventsController.eventsPut); - -// create route between controller and '/events/' endpoint -router.patch('/', eventsController.eventsPatch); - -// create route between controller and '/events/reorder' endpoint -router.patch('/reorder/', eventsController.eventsReorder); - -// create route between controller and '/events/applydelay/:eventId' endpoint -router.patch('/applydelay/:eventId', eventsController.eventsApplyDelay); - -// create route between controller and '/events/all' endpoint -router.delete('/all', eventsController.eventsDeleteAll); - -// create route between controller and '/events/:eventId' endpoint -router.delete('/:eventId', eventsController.eventsDelete); - -module.exports = router; diff --git a/server/src/app.js b/server/src/app.js new file mode 100644 index 000000000..e6d4dd214 --- /dev/null +++ b/server/src/app.js @@ -0,0 +1,89 @@ +// get config +import { config } from './config/config.js'; + +// init database +import { Low, JSONFile } from 'lowdb'; +import { join } from 'path'; + +const file = join('data/', config.database.filename); +const adapter = new JSONFile(file); +export const db = new Low(adapter); + +// dependencies +import express from 'express'; +import http from 'http'; +import cors from 'cors'; +import { dbModel } from './data/dataModel.js'; + +// Read data from JSON file, this will set db.data content +await db.read(); + +// If file.json doesn't exist, db.data will be null +// Set default data +// db.data ||= { events: [] }; NODE v15 - v16 +if (db.data == null) { + db.data = dbModel; + db.write(); +} + +// get data +export const data = db.data; + +// Import Routes +import { router as eventsRouter } from './routes/eventsRouter.js'; +import { router as eventRouter } from './routes/eventRouter.js'; +import { router as ontimeRouter } from './routes/ontimeRouter.js'; + +// Setup default port +const port = process.env.PORT || config.server.port; + +// Global Objects +import { EventTimer } from './classes/EventTimer.js'; + +// Create express APP +const app = express(); + +// setup cors for all routes +app.use(cors()); + +// enable pre-flight cors +app.options('*', cors()); + +// Implement middleware +app.use('/uploads', express.static('uploads')); + +app.use(express.urlencoded({ extended: true })); +app.use(express.json({ limit: '1mb' })); + +// Implement route endpoints +app.use('/events', eventsRouter); +app.use('/event', eventRouter); +app.use('/ontime', ontimeRouter); + +// implement general router +app.get('/', (req, res) => { + res.send('ontime API'); +}); + +// Implement route for errors +app.use((err, req, res, next) => { + console.error(err.stack); + res.status(500).send('Something broke!'); +}); + +// create HTTP server +const server = http.createServer(app); + +// init timer +global.timer = new EventTimer(server, config); +global.timer.setupWithEventList(data.events); + +// Start server +server.listen(port, () => + console.log(`HTTP Server is listening on port ${port}`) +); + +// Start OSC server +import { initiateOSC } from './controllers/OscController.js'; + +initiateOSC(config.osc); diff --git a/server/classes/EventTimer.js b/server/src/classes/EventTimer.js similarity index 99% rename from server/classes/EventTimer.js rename to server/src/classes/EventTimer.js index 16280541d..911ea8e58 100644 --- a/server/classes/EventTimer.js +++ b/server/src/classes/EventTimer.js @@ -1,5 +1,5 @@ -const Timer = require('./Timer'); -const socketIo = require('socket.io'); +import { Timer } from './Timer.js'; +import { Server } from 'socket.io'; /* * EventTimer adds functions specific to APP @@ -9,7 +9,7 @@ const socketIo = require('socket.io'); * */ -class EventTimer extends Timer { +export class EventTimer extends Timer { // AUX DAYMS = 86400000; @@ -57,12 +57,12 @@ class EventTimer extends Timer { numEvents = null; _eventlist = null; - constructor(server, config) { + constructor(httpServer, config) { // call super constructor super(); // initialise socketIO server - this.io = socketIo(server, { + this.io = new Server(httpServer, { cors: { origin: '*', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', @@ -936,5 +936,3 @@ class EventTimer extends Timer { this.loadEvent(this.selectedEventIndex); } } - -module.exports = EventTimer; diff --git a/server/classes/Timer.js b/server/src/classes/Timer.js similarity index 99% rename from server/classes/Timer.js rename to server/src/classes/Timer.js index 915a5dcad..29a5ca793 100644 --- a/server/classes/Timer.js +++ b/server/src/classes/Timer.js @@ -4,7 +4,7 @@ * */ -class Timer { +export class Timer { clock = null; duration = null; current = null; @@ -216,6 +216,4 @@ class Timer { this._finishedAt = null; } } -} - -module.exports = Timer; +} \ No newline at end of file diff --git a/server/src/config/config.js b/server/src/config/config.js new file mode 100644 index 000000000..64ef4ef2b --- /dev/null +++ b/server/src/config/config.js @@ -0,0 +1,15 @@ +export const config = { + timer: { + refresh: 1000, + }, + server: { + port: 4001, + }, + database: { + filename: 'db.json', + tablename: 'events', + }, + osc: { + port: 8888, + }, +}; diff --git a/server/controllers/OscController.js b/server/src/controllers/OscController.js similarity index 93% rename from server/controllers/OscController.js rename to server/src/controllers/OscController.js index e4ddcb983..939ee829b 100644 --- a/server/controllers/OscController.js +++ b/server/src/controllers/OscController.js @@ -1,7 +1,7 @@ -const osc = require('node-osc'); +import { Server } from 'node-osc'; -const initiateOSC = (config) => { - const oscServer = new osc.Server(config.port, '0.0.0.0', () => { +export const initiateOSC = (config) => { + const oscServer = new Server(config.port, '0.0.0.0', () => { console.log(`OSC Server is listening on port ${config.port}`); }); @@ -88,5 +88,3 @@ const initiateOSC = (config) => { } }); }; - -module.exports = { initiateOSC }; diff --git a/server/src/controllers/eventController.js b/server/src/controllers/eventController.js new file mode 100644 index 000000000..938e412b7 --- /dev/null +++ b/server/src/controllers/eventController.js @@ -0,0 +1,26 @@ +// get database +import { db, data } from '../app.js'; + +// Create controller for GET request to 'event' +// Returns ACK message +export const getEvent = async (req, res) => { + res.json(data.event); +}; + +// Create controller for POST request to 'event' +// Returns ACK message +export const postEvent = async (req, res) => { + if (!req.body) { + res.status(400).send('No object found in request'); + return; + } + // TODO: validate data + try { + data.event = { ...data.event, ...req.body }; + await db.write(); + res.sendStatus(200); + } catch (error) { + res.status(400).send(error); + console.log(error); + } +}; diff --git a/server/controllers/eventsController.js b/server/src/controllers/eventsController.js similarity index 73% rename from server/controllers/eventsController.js rename to server/src/controllers/eventsController.js index 5f3973121..bbb2dcb51 100644 --- a/server/controllers/eventsController.js +++ b/server/src/controllers/eventsController.js @@ -1,22 +1,26 @@ // get database -const db = require('../app.js').db; +import { db, data } from '../app.js'; // utils -const customAlphabet = require('nanoid').customAlphabet; +import { customAlphabet } from 'nanoid'; const nanoid = customAlphabet('1234567890abcdef', 4); -const eventDefs = require('../data/eventsDefinition.js'); +import { + event as eventDef, + delay as delayDef, + block as blockDef, +} from '../data/eventsDefinition.js'; function _getEventsCount() { - return db.get('events').size().value(); + return Array.from(data.events).length; } function _pushNew(entry) { - return db.get('events').push(entry).write(); + return data.events.push(entry).write(); } -function _insertAt(entry, index) { +async function _insertAt(entry, index) { // get events - let events = db.get('events').value(); + let events = data.events; let count = events.length; let order = entry.order; @@ -39,15 +43,18 @@ function _insertAt(entry, index) { } // save events - db.set('events', events).write(); + data.events = events; + await db.write(); } -function _removeById(eventId) { - return db.get('events').remove({ id: eventId }).write(); +async function _removeById(eventId) { + data.events = Array.from(data.events).filter((e) => e.id != eventId); + await db.write(); } function getEventEvents() { - return db.get('events').chain().filter({ type: 'event' }).value(); + // return data.events.filter((e) => e.type === 'event'); + return Array.from(data.events).filter((e) => e.type === 'event'); } // Updates timer object @@ -68,21 +75,21 @@ function _deleteTimerId(entryId) { // Create controller for GET request to '/events' // Returns - -exports.eventsGetAll = async (req, res) => { - const results = db.get('events').value(); - res.json(results); +export const eventsGetAll = async (req, res) => { + res.json(data.events); }; // Create controller for GET request to '/events/:eventId' // Returns - -exports.eventsGetById = async (req, res) => { - const e = db.get('events').find({ id: req.params.eventId }).value(); +export const eventsGetById = async (req, res) => { + const e = data.events.find({ id: req.params.eventId }).value(); + console.log('event by id', e); res.json(e); }; // Create controller for POST request to '/events/' // Returns - -exports.eventsPost = async (req, res) => { +export const eventsPost = async (req, res) => { // TODO: Validate event if (!req.body) { res.status(400).send(`No object found in request`); @@ -95,13 +102,13 @@ exports.eventsPost = async (req, res) => { switch (req.body.type) { case 'event': - newEvent = { ...eventDefs.event, ...req.body }; + newEvent = { ...eventDef, ...req.body }; break; case 'delay': - newEvent = { ...eventDefs.delay, ...req.body }; + newEvent = { ...delayDef, ...req.body }; break; case 'block': - newEvent = { ...eventDefs.block, ...req.body }; + newEvent = { ...blockDef, ...req.body }; break; default: @@ -131,7 +138,7 @@ exports.eventsPost = async (req, res) => { // Create controller for PUT request to '/events/' // Returns - -exports.eventsPut = async (req, res) => { +export const eventsPut = async (req, res) => { // no valid params if (!req.body) { res.status(400).send(`No object found`); @@ -145,29 +152,35 @@ exports.eventsPut = async (req, res) => { } try { - db.get('events') - .find({ id: req.body.id }) - .assign({ ...req.body }) - .update('revision', (n) => n + 1) - .write(); + const eventIndex = data.events.findIndex((e) => e.id === req.body.id); + if (eventIndex === -1) { + res.status(400).send(`No Id found found`); + return; + } + + const e = data.events[eventIndex]; + data.events[eventIndex] = { ...e, ...req.body }; + data.events[eventIndex].revision++; + db.write(); // update timer _updateTimersSingle(req.body.id, req.body); res.sendStatus(200); } catch (error) { + console.log(error); res.status(400).send(error); } }; // Create controller for PATCH request to '/events/' // Returns - -exports.eventsPatch = async (req, res) => { +export const eventsPatch = async (req, res) => { // Code is the same as put, call that - this.eventsPut(req, res); + eventsPut(req, res); }; -exports.eventsReorder = async (req, res) => { +export const eventsReorder = async (req, res) => { // TODO: Validate event if (!req.body) { res.status(400).send(`No object found in request`); @@ -177,7 +190,7 @@ exports.eventsReorder = async (req, res) => { const { index, from, to } = req.body; // get events - let events = db.get('events').value(); + let events = data.events; let idx = events.findIndex((e) => e.id === index, from); // Check if item is at given index @@ -194,7 +207,8 @@ exports.eventsReorder = async (req, res) => { events.splice(to, 0, reorderedItem); // save events - db.set('events', events).write(); + data.events = events; + db.write(); // TODO: would it be more efficient to reorder at timer? // update timer @@ -209,7 +223,7 @@ exports.eventsReorder = async (req, res) => { // Create controller for PATCH request to '/events/applydelay/:eventId' // Returns - -exports.eventsApplyDelay = async (req, res) => { +export const eventsApplyDelay = async (req, res) => { // no valid params if (!req.params.eventId) { res.status(400).send(`No id found in request`); @@ -218,7 +232,7 @@ exports.eventsApplyDelay = async (req, res) => { try { // get events - let events = db.get('events').value(); + let events = data.events; // AUX let delayIndex = null; @@ -259,7 +273,8 @@ exports.eventsApplyDelay = async (req, res) => { if (blockIndex) events.splice(blockIndex - 1, 1); // update events - db.set('events', events).write(); + data.events = events; + db.write(); // update timer _updateTimers(); @@ -274,7 +289,7 @@ exports.eventsApplyDelay = async (req, res) => { // Create controller for DELETE request to '/events/:eventId' // Returns - -exports.eventsDelete = async (req, res) => { +export const eventsDelete = async (req, res) => { // no valid params if (!req.params.eventId) { res.status(400).send(`No id found in request`); @@ -298,17 +313,17 @@ exports.eventsDelete = async (req, res) => { // Create controller for DELETE request to '/events/:eventId' // Returns - -exports.eventsDeleteAll = async (req, res) => { +export const eventsDeleteAll = async (req, res) => { try { // set with nothing - db.set('events', []).write(); + data.events = []; + db.write(); // update timer object _updateTimersSingle(); res.sendStatus(201); } catch (error) { - console.log('debug:', error); res.status(400).send(error); } }; diff --git a/server/src/controllers/ontimeController.js b/server/src/controllers/ontimeController.js new file mode 100644 index 000000000..2d74bb73f --- /dev/null +++ b/server/src/controllers/ontimeController.js @@ -0,0 +1,128 @@ +// get database +import { db, data } from '../app.js'; +import fs from 'fs'; +import { + event as eventDef, + delay as delayDef, + block as blockDef, +} from '../data/eventsDefinition.js'; +import { dbModel } from '../data/dataModel.js'; + +function getEventTitle() { + return data.event.title; +} + +async function deleteFile(file) { + // delete a file + fs.unlink(file, (err) => { + if (err) { + console.log(err); + } + }); +} + +// parses version 1 of the data system +async function parsev1(jsonData) { + if ('events' in jsonData) { + let events = []; + let ids = []; + for (const e of jsonData.events) { + if (e.type === 'event') { + // doublecheck unique ids + if (e.id == null || ids.indexOf(e.id) !== -1) continue; + ids.push(e.id); + + // make sure all properties exits + // dont load any extra properties than the ones known + events.push({ + ...eventDef, + title: e.title, + subtitle: e.subtitle, + presenter: e.presenter, + note: e.note, + timeStart: e.timeStart, + timeEnd: e.timeEnd, + isPublic: e.isPublic, + id: e.id, + }); + } else if (e.type === 'delay') { + events.push({ ...delayDef, duration: e.duration }); + } else if (e.type === 'block') { + events.push({ ...blockDef }); + } + } + // write to db + db.data.events = events; + db.write(); + } + + if ('event' in jsonData) { + const e = jsonData.event; + // filter known properties + const event = { + ...dbModel.event, + title: e.title, + url: e.url, + publicInfo: e.publicInfo, + backstageInfo: e.backstageInfo, + }; + + // write to db + db.data.event = event; + db.write(); + } + + // Not handling settings yet + // let settings = {}; + // if ('settings' in jsonData) { + // } +} + +// Create controller for GET request to '/ontime/db' +// Returns - +export const dbDownload = async (req, res) => { + const fileTitle = getEventTitle() || 'ontime events'; + res.download('db.json', `${fileTitle}.json`, (err) => { + if (err) { + res.status(500).send({ + message: 'Could not download the file. ' + err, + }); + } + }); +}; + +// Create controller for POST request to '/ontime/db' +// Returns - +export const dbUpload = async (req, res) => { + if (!req.file) { + res.status(400).send({ message: 'File not found' }); + return; + } + + const file = req.file.path; + if (!fs.existsSync(file)) { + res.status(500).send({ message: 'Upload failed' }); + return; + } + + try { + // get file + let rawdata = fs.readFileSync(file); + let uploadedJson = JSON.parse(rawdata); + + // delete file + deleteFile(file); + + // check version + if (uploadedJson.settings.version === 1) parsev1(uploadedJson); + else { + res.status(400).send({ message: 'Error parsing file, version unknown' }); + return; + } + + res.sendStatus(200); + } catch (error) { + console.log('Error parsing file', error); + res.status(400).send({ message: error }); + } +}; diff --git a/server/controllers/playbackController.js b/server/src/controllers/playbackController.js similarity index 71% rename from server/controllers/playbackController.js rename to server/src/controllers/playbackController.js index 4dcc45cb3..b8a72f5a5 100644 --- a/server/controllers/playbackController.js +++ b/server/src/controllers/playbackController.js @@ -1,64 +1,64 @@ // Create controller for GET request to '/playback' // Returns ACK message -exports.pbGet = async (req, res) => { +export const pbGet = async (req, res) => { res.send(global.timer.playState); }; // Create controller for GET request to '/playback/start' // Starts timer object -exports.pbStart = async (req, res) => { +export const pbStart = async (req, res) => { global.timer.start(); res.sendStatus(200); }; // Create controller for GET request to '/playback/pause' // Pauses timer object -exports.pbPause = async (req, res) => { +export const pbPause = async (req, res) => { global.timer.pause(); res.sendStatus(200); }; // Create controller for GET request to '/playback/stop' // Stops timer object -exports.pbStop = async (req, res) => { +export const pbStop = async (req, res) => { global.timer.stop(); res.sendStatus(200); }; // Create controller for GET request to '/playback/roll' // Sets timer object to roll mode -exports.pbRoll = async (req, res) => { +export const pbRoll = async (req, res) => { global.timer.roll(); res.sendStatus(501); }; // Create controller for GET request to '/playback/previous' // Sets timer object to roll mode -exports.pbPrevious = async (req, res) => { +export const pbPrevious = async (req, res) => { global.timer.previous(); res.sendStatus(200); }; // Create controller for GET request to '/playback/next' // Sets timer object to roll mode -exports.pbNext = async (req, res) => { +export const pbNext = async (req, res) => { global.timer.next(); res.sendStatus(200); }; // Create controller for GET request to '/playback/unload' // Unloads any events -exports.pbUnload = async (req, res) => { +export const pbUnload = async (req, res) => { global.timer.unload(); - console.log('debug: unload called') + console.log('debug: unload called'); res.sendStatus(200); }; // Create controller for GET request to '/playback/reload' // Reloads current event -exports.pbReload = async (req, res) => { +export const pbReload = async (req, res) => { global.timer.reload(); - console.log('debug: reload called') + console.log('debug: reload called'); res.sendStatus(200); }; diff --git a/server/data/dataModel.js b/server/src/data/dataModel.js similarity index 73% rename from server/data/dataModel.js rename to server/src/data/dataModel.js index cc6f67470..39e781b87 100644 --- a/server/data/dataModel.js +++ b/server/src/data/dataModel.js @@ -1,10 +1,10 @@ -const dbModel = { +export const dbModel = { events: [], event: { title: '', url: '', publicInfo: '', - backStageInfo: '', + backstageInfo: '', }, settings: { app: 'ontime', @@ -14,5 +14,3 @@ const dbModel = { lock: false, }, }; - -module.exports = { dbModel }; diff --git a/server/data/eventsDefinition.js b/server/src/data/eventsDefinition.js similarity index 70% rename from server/data/eventsDefinition.js rename to server/src/data/eventsDefinition.js index d98f968f8..f1f326ee8 100644 --- a/server/data/eventsDefinition.js +++ b/server/src/data/eventsDefinition.js @@ -1,4 +1,4 @@ -const event = { +export const event = { title: '', subtitle: '', presenter: '', @@ -10,14 +10,12 @@ const event = { revision: 0, }; -const delay = { +export const delay = { duration: 0, type: 'delay', revision: 0, }; -const block = { +export const block = { type: 'block', }; - -module.exports = { event, delay, block }; diff --git a/server/src/routes/eventRouter.js b/server/src/routes/eventRouter.js new file mode 100644 index 000000000..9b929f95d --- /dev/null +++ b/server/src/routes/eventRouter.js @@ -0,0 +1,11 @@ +import express from 'express'; +export const router = express.Router(); + +// import event controller +import { getEvent, postEvent } from '../controllers/eventController.js'; + +// create route between controller and 'GET /event' endpoint +router.get('/', getEvent); + +// create route between controller and 'POST /event' endpoint +router.post('/', postEvent); diff --git a/server/src/routes/eventsRouter.js b/server/src/routes/eventsRouter.js new file mode 100644 index 000000000..9ea280c45 --- /dev/null +++ b/server/src/routes/eventsRouter.js @@ -0,0 +1,42 @@ +import express from 'express'; +export const router = express.Router(); + +// import events controller +import { + eventsGetAll, + eventsGetById, + eventsPost, + eventsPut, + eventsPatch, + eventsReorder, + eventsApplyDelay, + eventsDeleteAll, + eventsDelete, +} from '../controllers/eventsController.js'; + +// create route between controller and '/events/' endpoint +router.get('/', eventsGetAll); + +// create route between controller and '/events/:eventId' endpoint +router.get('/:eventId', eventsGetById); + +// create route between controller and '/events/' endpoint +router.post('/', eventsPost); + +// create route between controller and '/events/' endpoint +router.put('/', eventsPut); + +// create route between controller and '/events/' endpoint +router.patch('/', eventsPatch); + +// create route between controller and '/events/reorder' endpoint +router.patch('/reorder/', eventsReorder); + +// create route between controller and '/events/applydelay/:eventId' endpoint +router.patch('/applydelay/:eventId', eventsApplyDelay); + +// create route between controller and '/events/all' endpoint +router.delete('/all', eventsDeleteAll); + +// create route between controller and '/events/:eventId' endpoint +router.delete('/:eventId', eventsDelete); diff --git a/server/src/routes/ontimeRouter.js b/server/src/routes/ontimeRouter.js new file mode 100644 index 000000000..e5b5e5670 --- /dev/null +++ b/server/src/routes/ontimeRouter.js @@ -0,0 +1,11 @@ +import express from 'express'; +import uploadJson from '../utils/upload.js'; +export const router = express.Router(); + +import { dbDownload, dbUpload } from '../controllers/ontimeController.js'; + +// create route between controller and '/ontime/db' endpoint +router.get('/db', dbDownload); + +// create route between controller and '/ontime/db' endpoint +router.post('/db', uploadJson, dbUpload); diff --git a/server/routes/playbackRouter.js b/server/src/routes/playbackRouter.js similarity index 92% rename from server/routes/playbackRouter.js rename to server/src/routes/playbackRouter.js index e767a3156..3b71b9c86 100644 --- a/server/routes/playbackRouter.js +++ b/server/src/routes/playbackRouter.js @@ -1,5 +1,5 @@ -const express = require('express'); -const router = express.Router(); +import express from 'express'; +export const router = express.Router(); // import event controller const playbackController = require('../controllers/playbackController'); @@ -30,5 +30,3 @@ router.get('/unload', playbackController.pbUnload); // create route between controller and '/playback/reload' endpoint router.get('/reload', playbackController.pbReload); - -module.exports = router; diff --git a/server/src/utils/upload.js b/server/src/utils/upload.js new file mode 100644 index 000000000..e489ac26f --- /dev/null +++ b/server/src/utils/upload.js @@ -0,0 +1,23 @@ +import multer from 'multer'; + +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, 'uploads/'); + }, + filename: function (req, file, cb) { + cb(null, Date.now() + '--' + file.originalname); + }, +}); + +// filter only json +const filterJson = (req, file, cb) => { + if (file.mimetype.includes('application/json')) { + cb(null, true); + } else { + cb(null, false); + } +}; + +const uploadJson = multer({ storage: storage, fileFilter: filterJson }); + +export default uploadJson.single('jsondb'); diff --git a/server/yarn.lock b/server/yarn.lock index 9778a0c2f..ba9e9c7b2 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -30,6 +30,11 @@ accepts@~1.3.4, accepts@~1.3.7: mime-types "~2.1.24" negotiator "0.6.2" +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -66,6 +71,19 @@ body-parser@1.19.0, body-parser@~1.19.0: raw-body "2.4.0" type-is "~1.6.17" +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + bytes@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" @@ -76,6 +94,16 @@ component-emitter@~1.3.0: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + content-disposition@0.5.3: version "0.5.3" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" @@ -103,6 +131,11 @@ cookie@~0.4.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + cors@~2.8.5: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" @@ -140,6 +173,14 @@ destroy@~1.0.4: resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -253,11 +294,6 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -graceful-fs@^4.1.3: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - http-errors@1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" @@ -292,7 +328,7 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -inherits@2.0.4: +inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -302,26 +338,22 @@ ipaddr.js@1.9.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== -is-promise@^2.1.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" - integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= -lodash@4: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -lowdb@^1.0.0: +isarray@~1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064" - integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ== + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +lowdb@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-2.1.0.tgz#c8063e228b5ab3e082ece90e0512537ecb6e1e2a" + integrity sha512-F4Go8/V37gAidTR3c5poyjprOpZSDNSLJVOmI0ny4D4q9rC37OkBhlzX0bqj7LZlT3UIj4FchmZrrSw7qY+eGQ== dependencies: - graceful-fs "^4.1.3" - is-promise "^2.1.0" - lodash "4" - pify "^3.0.0" - steno "^0.4.1" + steno "^1.0.0" media-typer@0.3.0: version "0.3.0" @@ -355,6 +387,18 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -370,6 +414,20 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +multer@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" + integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.1" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + nanoid@^3.1.22: version "3.1.22" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" @@ -387,12 +445,12 @@ node-osc@6.0.2: dependencies: osc-min "^1.1.1" -object-assign@^4: +object-assign@^4, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -on-finished@~2.3.0: +on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -446,10 +504,10 @@ pause@0.0.1: resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10= -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== proxy-addr@~2.0.5: version "2.0.6" @@ -484,7 +542,30 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" -safe-buffer@5.1.2: +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.2.2: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -567,19 +648,34 @@ socket.io@^4.0.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -steno@^0.4.1: - version "0.4.4" - resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz#071105bdfc286e6615c0403c27e9d7b5dcb855cb" - integrity sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs= +steno@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/steno/-/steno-1.0.0.tgz#475e32c6066ec9760229eaaf1550601764fbecba" + integrity sha512-C/KgCvEa1yWnpHmaPjAXrz1yWxh6hs+HvhqqPa71euaQmNi1wr4+WFo57VQxjKKuFl2KqS7gtlrN0oxj2noQLw== + +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: - graceful-fs "^4.1.3" + safe-buffer "~5.1.0" toidentifier@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -587,6 +683,11 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + uid-safe@~2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" @@ -599,6 +700,11 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" @@ -613,3 +719,8 @@ ws@~7.4.2: version "7.4.4" resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==