From ebeaf3f5494360852dfe16dae81e0f926e08efb4 Mon Sep 17 00:00:00 2001 From: trivernis Date: Thu, 23 Jan 2020 15:41:06 +0100 Subject: [PATCH] Change video format to webm - Change default allowed filesize to 10mb - Change video format for posts to webm --- config/default.toml | 2 +- src/lib/UploadManager.ts | 43 ++++++++++++--------- src/lib/models/Post.ts | 16 ++++++++ src/routes/UploadRoute.ts | 52 ++++++++++++++++++++++++-- src/routes/graphql/MutationResolver.ts | 16 +------- src/routes/graphql/schema.graphql | 2 +- 6 files changed, 94 insertions(+), 37 deletions(-) diff --git a/config/default.toml b/config/default.toml index 14e8a6a..d66d813 100644 --- a/config/default.toml +++ b/config/default.toml @@ -64,7 +64,7 @@ maxQueryComplexity = 5000 imageFormat = "png" # the max file size for uploading in bytes -maxFileSize = 5_242_880 +maxFileSize = 10_485_760 # Configuration for the api rate limit [api.rateLimit] diff --git a/src/lib/UploadManager.ts b/src/lib/UploadManager.ts index a12d68c..9b7280c 100644 --- a/src/lib/UploadManager.ts +++ b/src/lib/UploadManager.ts @@ -10,6 +10,7 @@ import globals from "./globals"; const toArray = require("stream-to-array"); +const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path; const dataDirName = "data"; interface IUploadConfirmation { @@ -49,6 +50,7 @@ export class UploadManager { constructor() { this.dataDir = path.join(globals.getPublicDir(), dataDirName); + ffmpeg.setFfmpegPath(ffmpegPath); } /** @@ -79,6 +81,7 @@ export class UploadManager { const filePath = path.join(this.dataDir, fileBasename); let image = sharp(data) .resize(width, height, { + background: "#00000000", fit, }) .normalise(); @@ -103,23 +106,29 @@ export class UploadManager { * @param width */ public async processAndStoreVideo(data: Buffer, width: number = 720): Promise { - return new Promise(async (resolve) => { - const fileBasename = UploadManager.getCrypticFileName() + ".mp4"; - await fsx.ensureDir(this.dataDir); - const filePath = path.join(this.dataDir, fileBasename); - const videoFileStream = new ReadableStreamBuffer({ - chunkSize: 2048, - frequency: 10, - }); - videoFileStream.put(data); - const video = ffmpeg(videoFileStream); - video - .on("end", () => { - resolve(`/${dataDirName}/${fileBasename}`); - }) - .size(`${width}x?`) - .toFormat("libx264") - .output(filePath); + return new Promise(async (resolve, reject) => { + try { + const fileBasename = UploadManager.getCrypticFileName() + ".webm"; + await fsx.ensureDir(this.dataDir); + const filePath = path.join(this.dataDir, fileBasename); + const tempFile = filePath + ".tmp"; + await fsx.writeFile(tempFile, data); + const video = ffmpeg(tempFile); + video + .size(`${width}x?`) + .toFormat("webm") + .on("end", async () => { + await fsx.unlink(tempFile); + resolve(`/${dataDirName}/${fileBasename}`); + }) + .on("error", async (err) => { + await fsx.unlink(tempFile); + reject(err); + }) + .save(filePath); + } catch (err) { + reject(err); + } }); } diff --git a/src/lib/models/Post.ts b/src/lib/models/Post.ts index 8d60709..621f35c 100644 --- a/src/lib/models/Post.ts +++ b/src/lib/models/Post.ts @@ -106,6 +106,22 @@ export class Post extends Model { return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length; } + /** + * Returns the media description object of the post + */ + public get media() { + const url = this.getDataValue("mediaUrl"); + if (url) { + const type = url.endsWith(".webm") ? "VIDEO" : "IMAGE"; + return { + type, + url, + }; + } else { + return null; + } + } + /** * Toggles the vote of the user. * @param userId diff --git a/src/routes/UploadRoute.ts b/src/routes/UploadRoute.ts index 1797dae..ff0911f 100644 --- a/src/routes/UploadRoute.ts +++ b/src/routes/UploadRoute.ts @@ -9,11 +9,11 @@ import * as fsx from "fs-extra"; import * as status from "http-status"; import * as path from "path"; import globals from "../lib/globals"; -import {Group, User} from "../lib/models"; +import {Group, Post, User} from "../lib/models"; +import {is} from "../lib/regex"; import Route from "../lib/Route"; import {UploadManager} from "../lib/UploadManager"; -const ffmpegPath = require("@ffmpeg-installer/ffmpeg").path; const dataDirName = "data"; interface IUploadConfirmation { @@ -59,7 +59,6 @@ export class UploadRoute extends Route { super(); this.router = Router(); this.dataDir = path.join(this.publicPath, dataDirName); - ffmpeg.setFfmpegPath(ffmpegPath); this.uploadManager = new UploadManager(); } @@ -80,6 +79,8 @@ export class UploadRoute extends Route { uploadConfirmation = await this.uploadProfilePicture(req); } else if (req.files.groupPicture) { uploadConfirmation = await this.uploadGroupPicture(req); + } else if (req.files.postMedia) { + uploadConfirmation = await this.uploadPostMedia(req); } else { res.status(status.BAD_REQUEST); uploadConfirmation = { @@ -187,4 +188,49 @@ export class UploadRoute extends Route { success, }; } + + /** + * Uploads a media file for a post + * @param request + */ + private async uploadPostMedia(request: any) { + let error: string; + let fileName: string; + let success = false; + const postId = request.body.postId; + const postMedia = request.files.postMedia as UploadedFile; + if (postId) { + try { + const post = await Post.findByPk(postId); + if (post.authorId === request.session.userId) { + if (is.image(postMedia.mimetype)) { + fileName = await this.uploadManager.processAndStoreImage(postMedia.data, 1080, 720, "contain"); + } else if (is.video(postMedia.mimetype)) { + fileName = await this.uploadManager.processAndStoreVideo(postMedia.data, 1080); + } else { + error = "Wrong type of file provided"; + } + if (fileName) { + post.mediaUrl = fileName; + await post.save(); + success = true; + } + } else { + error = "You are not the author of the post"; + } + } catch (err) { + error = err.message; + globals.logger.error(err.message); + globals.logger.debug(err.stack); + } + } else { + error = "No post Id provided"; + } + + return { + error, + fileName, + success, + }; + } } diff --git a/src/routes/graphql/MutationResolver.ts b/src/routes/graphql/MutationResolver.ts index 696c2a5..edf8f60 100644 --- a/src/routes/graphql/MutationResolver.ts +++ b/src/routes/graphql/MutationResolver.ts @@ -145,27 +145,13 @@ export class MutationResolver extends BaseResolver { * @param activityId * @param request */ - public async createPost({content, activityId, file}: { content: string, activityId?: number, file: FileUpload }, + public async createPost({content, activityId}: { content: string, activityId?: number}, request: any): Promise { this.ensureLoggedIn(request); if (content.length > 2048) { throw new GraphQLError("Content too long."); } const post = await dataaccess.createPost(content, request.session.userId, activityId); - if (file) { - let fileUrl: string; - if (is.video(file.mimetype)) { - const fileBuffer = await this.uploadManager.streamToBuffer(file.createReadStream()); - fileUrl = await this.uploadManager.processAndStoreVideo(fileBuffer); - } else if (is.image(file.mimetype)) { - const fileBuffer = await this.uploadManager.streamToBuffer(file.createReadStream()); - fileUrl = await this.uploadManager.processAndStoreImage(fileBuffer); - } else { - throw new InvalidFileError(file.mimetype); - } - post.mediaUrl = fileUrl; - await post.save(); - } globals.internalEmitter.emit(InternalEvents.GQLPOSTCREATE, post); return post; } diff --git a/src/routes/graphql/schema.graphql b/src/routes/graphql/schema.graphql index f8239af..41b009a 100644 --- a/src/routes/graphql/schema.graphql +++ b/src/routes/graphql/schema.graphql @@ -85,7 +85,7 @@ type Mutation { sendMessage(chatId: ID!, content: String!): ChatMessage "create a post that can belong to an activity" - createPost(content: String!, activityId: ID, file: Upload): Post! + createPost(content: String!, activityId: ID): Post! "delete the post for a given post id" deletePost(postId: ID!): Boolean!