From e3970ad394ffdbfb0ac05b59eb299041070b9733 Mon Sep 17 00:00:00 2001 From: subnub Date: Sat, 29 Jun 2024 13:39:11 -0400 Subject: [PATCH] started video thumbnails --- .../ChunkService/FileSystemService.ts | 17 +- .../ChunkService/utils/createThumbnailFS.ts | 40 ++-- .../utils/createVideoThumbnailFS.ts | 190 ++++++++++++++++++ .../ChunkService/utils/getBusboyData.ts | 1 - .../utils/tempCreateVideoThumbnailFS.ts | 152 ++++++++++++++ backend/utils/imageChecker.ts | 38 ++-- package.json | 3 +- 7 files changed, 387 insertions(+), 54 deletions(-) create mode 100644 backend/services/ChunkService/utils/createVideoThumbnailFS.ts create mode 100644 backend/services/ChunkService/utils/tempCreateVideoThumbnailFS.ts diff --git a/backend/services/ChunkService/FileSystemService.ts b/backend/services/ChunkService/FileSystemService.ts index fa72392..d318c35 100644 --- a/backend/services/ChunkService/FileSystemService.ts +++ b/backend/services/ChunkService/FileSystemService.ts @@ -31,6 +31,7 @@ import ForbiddenError from "../../utils/ForbiddenError"; import { ObjectId } from "mongodb"; import FolderService from "../FolderService"; import MongoFileService from "../FileService"; +import createVideoThumbnailFS from "./utils/createVideoThumbnailFS"; const dbUtilsFile = new DbUtilFile(); const dbUtilsFolder = new DbUtilFolder(); @@ -107,8 +108,6 @@ class FileSystemService implements ChunkInterface { const date = new Date(); const encryptedFileSize = await getFileSize(metadata.filePath); - console.log("filtnamed", filename); - const currentFile = new File({ filename, uploadDate: date.toISOString(), @@ -120,11 +119,19 @@ class FileSystemService implements ChunkInterface { await addToStoageSize(user, size, personalFile); - console.log("current file", currentFile); - const imageCheck = imageChecker(currentFile.filename); + const videoCheck = videoChecker(currentFile.filename); - if (currentFile.length < 15728640 && imageCheck) { + if (videoCheck) { + console.log("is vidoe"); + const updatedFile = await createVideoThumbnailFS( + currentFile, + filename, + user + ); + + return updatedFile; + } else if (currentFile.length < 15728640 && imageCheck) { const updatedFile = await createThumbnailAny(currentFile, filename, user); return updatedFile; diff --git a/backend/services/ChunkService/utils/createThumbnailFS.ts b/backend/services/ChunkService/utils/createThumbnailFS.ts index f8a60ac..4f863bd 100644 --- a/backend/services/ChunkService/utils/createThumbnailFS.ts +++ b/backend/services/ChunkService/utils/createThumbnailFS.ts @@ -8,6 +8,7 @@ import fs from "fs"; import uuid from "uuid"; import env from "../../../enviroment/env"; import { ObjectId } from "mongodb"; +import File from "../../../models/file"; const conn = mongoose.connection; @@ -82,32 +83,27 @@ const createThumbnailFS = ( if (!file._id) { return reject(); } - const getUpdatedFile = await conn.db - .collection("fs.files") - .findOneAndUpdate( - { _id: new ObjectId(file._id) }, - { - $set: { - "metadata.hasThumbnail": true, - "metadata.thumbnailID": thumbnailModel._id, - }, - } - ); - if (!getUpdatedFile) { + const updateFileResponse = await File.updateOne( + { _id: new ObjectId(file._id), "metadata.owner": user._id }, + { + $set: { + "metadata.hasThumbnail": true, + "metadata.thumbnailID": thumbnailModel._id, + }, + } + ); + if (updateFileResponse.modifiedCount === 0) { return reject(); } - let updatedFile: FileInterface = getUpdatedFile.value; - updatedFile = { - ...updatedFile, - metadata: { - ...updatedFile.metadata, - hasThumbnail: true, - thumbnailID: thumbnailModel._id, - }, - } as FileInterface; + const updatedFile = await File.findById({ + _id: new ObjectId(file._id), + "metadata.owner": user._id, + }); - resolve(updatedFile); + if (!updatedFile) return reject(); + + resolve(updatedFile?.toObject()); }); } catch (e) { console.log("Thumbnail error", e); diff --git a/backend/services/ChunkService/utils/createVideoThumbnailFS.ts b/backend/services/ChunkService/utils/createVideoThumbnailFS.ts new file mode 100644 index 0000000..f5ddd90 --- /dev/null +++ b/backend/services/ChunkService/utils/createVideoThumbnailFS.ts @@ -0,0 +1,190 @@ +import mongoose from "../../../db/mongoose"; +import crypto from "crypto"; +import Thumbnail from "../../../models/thumbnail"; +import sharp from "sharp"; +import { FileInterface } from "../../../models/file"; +import { UserInterface } from "../../../models/user"; +import fs from "fs"; +import uuid from "uuid"; +import env from "../../../enviroment/env"; +import { ObjectId } from "mongodb"; +import File from "../../../models/file"; +import ffmpeg from "fluent-ffmpeg"; +import tempCreateVideoThumbnailFS from "./tempCreateVideoThumbnailFS"; + +const createVideoThumbnailFS = ( + file: FileInterface, + filename: string, + user: UserInterface +) => { + return new Promise(async (resolve, reject) => { + const password = user.getEncryptionKey(); + + let CIPHER_KEY = crypto.createHash("sha256").update(password!).digest(); + + const thumbnailFilename = uuid.v4(); + + const readStream = fs.createReadStream(file.metadata.filePath!); + console.log("env", env); + const writeStream = fs.createWriteStream( + env.fsDirectory + thumbnailFilename + ); + const decipher = crypto.createDecipheriv( + "aes256", + CIPHER_KEY, + file.metadata.IV + ); + + const thumbnailIV = crypto.randomBytes(16); + + const thumbnailCipher = crypto.createCipheriv( + "aes256", + CIPHER_KEY, + thumbnailIV + ); + + const decryptedReadStream = readStream.pipe(decipher); + + // const encryptedWriteStream = thumbnailCipher + // .pipe(writeStream) + // .on("finish", async () => { + // const thumbnailModel = new Thumbnail({ + // name: filename, + // owner: user._id, + // IV: thumbnailIV, + // path: env.fsDirectory + thumbnailFilename, + // }); + + // await thumbnailModel.save(); + // if (!file._id) { + // return reject(); + // } + // const updateFileResponse = await File.updateOne( + // { _id: new ObjectId(file._id), "metadata.owner": user._id }, + // { + // $set: { + // "metadata.hasThumbnail": true, + // "metadata.thumbnailID": thumbnailModel._id, + // }, + // } + // ); + // if (updateFileResponse.modifiedCount === 0) { + // return reject(); + // } + + // const updatedFile = await File.findById({ + // _id: new ObjectId(file._id), + // "metadata.owner": user._id, + // }); + + // if (!updatedFile) return reject(); + + // resolve(updatedFile?.toObject()); + // }); + + ffmpeg(decryptedReadStream, { + timeout: 60, + }) + .seek(1) + .format("image2pipe") + .outputOptions([ + "-f image2pipe", + "-vframes 1", + "-vf scale='if(gt(iw,ih),600,-1):if(gt(ih,iw),600,-1)'", + ]) + .on("start", (command) => { + /** + * log + */ + }) + .on("end", async () => { + console.log("end"); + const thumbnailModel = new Thumbnail({ + name: filename, + owner: user._id, + IV: thumbnailIV, + path: env.fsDirectory + thumbnailFilename, + }); + + await thumbnailModel.save(); + if (!file._id) { + return reject(); + } + const updateFileResponse = await File.updateOne( + { _id: new ObjectId(file._id), "metadata.owner": user._id }, + { + $set: { + "metadata.hasThumbnail": true, + "metadata.thumbnailID": thumbnailModel._id, + }, + } + ); + if (updateFileResponse.modifiedCount === 0) { + return reject(); + } + + const updatedFile = await File.findById({ + _id: new ObjectId(file._id), + "metadata.owner": user._id, + }); + + if (!updatedFile) return reject(); + + resolve(updatedFile?.toObject()); + }) + .on("error", async (err, _, stderr) => { + console.log("thumbnail error attempting temp directory fix"); + const updatedFile = await tempCreateVideoThumbnailFS( + file, + filename, + user + ); + resolve(updatedFile); + /** + * log + */ + }) + .pipe(thumbnailCipher) + .pipe(writeStream, { end: true }); + + // ffmpeg() + // .input(readStream.pipe(decipher)) + // .seekInput("00:00:01.00") + // .outputFormat("image2") + // .pipe(writeStream, { end: true }) + // .on("end", async () => { + // const thumbnailModel = new Thumbnail({ + // name: filename, + // owner: user._id, + // IV: thumbnailIV, + // path: env.fsDirectory + thumbnailFilename, + // }); + + // await thumbnailModel.save(); + // resolve(file); + // }) + // .on("error", (e) => { + // console.log("error", e); + // reject(e); + // }); + + // ffmpeg() + // .input(readStream.pipe(decipher)) + // .size("300x?") + // .outputOptions("-frames:v 1") + // .on("end", async () => { + // const thumbnailModel = new Thumbnail({ + // name: filename, + // owner: user._id, + // IV: thumbnailIV, + // path: env.fsDirectory + thumbnailFilename, + // }); + + // await thumbnailModel.save(); + // resolve(file); + // }) + // .output(writeStream); + }); +}; + +export default createVideoThumbnailFS; diff --git a/backend/services/ChunkService/utils/getBusboyData.ts b/backend/services/ChunkService/utils/getBusboyData.ts index bd6a2fd..9e50586 100644 --- a/backend/services/ChunkService/utils/getBusboyData.ts +++ b/backend/services/ChunkService/utils/getBusboyData.ts @@ -13,7 +13,6 @@ const getBusboyData = (busboy: any) => { const formData = new Map(); busboy.on("field", (field: any, val: any) => { - console.log("field", field, val, typeof val); // ????? why does it make undefined a string? if (typeof val !== "string" || val !== "undefined") { formData.set(field, val); diff --git a/backend/services/ChunkService/utils/tempCreateVideoThumbnailFS.ts b/backend/services/ChunkService/utils/tempCreateVideoThumbnailFS.ts new file mode 100644 index 0000000..602e618 --- /dev/null +++ b/backend/services/ChunkService/utils/tempCreateVideoThumbnailFS.ts @@ -0,0 +1,152 @@ +import mongoose from "../../../db/mongoose"; +import crypto from "crypto"; +import Thumbnail from "../../../models/thumbnail"; +import sharp from "sharp"; +import { FileInterface } from "../../../models/file"; +import { UserInterface } from "../../../models/user"; +import fs from "fs"; +import uuid from "uuid"; +import env from "../../../enviroment/env"; +import { ObjectId } from "mongodb"; +import File from "../../../models/file"; +import ffmpeg from "fluent-ffmpeg"; + +const tempCreateVideoThumbnailFS = ( + file: FileInterface, + filename: string, + user: UserInterface +) => { + return new Promise(async (resolve, reject) => { + const password = user.getEncryptionKey(); + + let CIPHER_KEY = crypto.createHash("sha256").update(password!).digest(); + + const thumbnailFilename = uuid.v4(); + + const readStream = fs.createReadStream(file.metadata.filePath!); + console.log("env", env); + const writeStream = fs.createWriteStream( + env.fsDirectory + thumbnailFilename + ); + const tempWriteStream = fs.createWriteStream( + env.fsDirectory + "temp/" + thumbnailFilename + ); + const decipher = crypto.createDecipheriv( + "aes256", + CIPHER_KEY, + file.metadata.IV + ); + + const thumbnailIV = crypto.randomBytes(16); + + const thumbnailCipher = crypto.createCipheriv( + "aes256", + CIPHER_KEY, + thumbnailIV + ); + + const decryptedReadStream = readStream.pipe(decipher); + + decryptedReadStream.pipe(tempWriteStream, { end: true }); + + ffmpeg(env.fsDirectory + "temp/" + thumbnailFilename, { + timeout: 60, + }) + .seek(1) + .format("image2pipe") + .outputOptions([ + "-f image2pipe", + "-vframes 1", + "-vf scale='if(gt(iw,ih),600,-1):if(gt(ih,iw),600,-1)'", + ]) + .on("start", (command) => { + /** + * log + */ + }) + .on("end", async () => { + console.log("end"); + const thumbnailModel = new Thumbnail({ + name: filename, + owner: user._id, + IV: thumbnailIV, + path: env.fsDirectory + thumbnailFilename, + }); + + await thumbnailModel.save(); + if (!file._id) { + return reject(); + } + const updateFileResponse = await File.updateOne( + { _id: new ObjectId(file._id), "metadata.owner": user._id }, + { + $set: { + "metadata.hasThumbnail": true, + "metadata.thumbnailID": thumbnailModel._id, + }, + } + ); + if (updateFileResponse.modifiedCount === 0) { + return reject(); + } + + const updatedFile = await File.findById({ + _id: new ObjectId(file._id), + "metadata.owner": user._id, + }); + + if (!updatedFile) return reject(); + + resolve(updatedFile?.toObject()); + }) + .on("error", (err, _, stderr) => { + console.log("error", err, stderr); + resolve(file); + /** + * log + */ + }) + .pipe(thumbnailCipher) + .pipe(writeStream, { end: true }); + + // ffmpeg() + // .input(readStream.pipe(decipher)) + // .seekInput("00:00:01.00") + // .outputFormat("image2") + // .pipe(writeStream, { end: true }) + // .on("end", async () => { + // const thumbnailModel = new Thumbnail({ + // name: filename, + // owner: user._id, + // IV: thumbnailIV, + // path: env.fsDirectory + thumbnailFilename, + // }); + + // await thumbnailModel.save(); + // resolve(file); + // }) + // .on("error", (e) => { + // console.log("error", e); + // reject(e); + // }); + + // ffmpeg() + // .input(readStream.pipe(decipher)) + // .size("300x?") + // .outputOptions("-frames:v 1") + // .on("end", async () => { + // const thumbnailModel = new Thumbnail({ + // name: filename, + // owner: user._id, + // IV: thumbnailIV, + // path: env.fsDirectory + thumbnailFilename, + // }); + + // await thumbnailModel.save(); + // resolve(file); + // }) + // .output(writeStream); + }); +}; + +export default tempCreateVideoThumbnailFS; diff --git a/backend/utils/imageChecker.ts b/backend/utils/imageChecker.ts index 9c6c62e..5fa3b44 100644 --- a/backend/utils/imageChecker.ts +++ b/backend/utils/imageChecker.ts @@ -1,31 +1,19 @@ -const imageExtList = [ - "jpeg", - "jpg", - "png", - "gif", - "svg", - "tiff", - "bmp" -] +const imageExtList = ["jpeg", "jpg", "png", "gif", "svg", "tiff", "bmp"]; -const imageChecker = (filename: string) => { +const imageChecker = (filename: string) => { + if (filename.length < 1 || !filename.includes(".")) { + return false; + } - if (filename.length < 1 || !filename.includes(".")) { + const extSplit = filename.split("."); - return false; - } + if (extSplit.length <= 1) { + return false; + } - const extSplit = filename.split("."); + const ext = extSplit[extSplit.length - 1]; - if (extSplit.length <= 1) { - - return false; - } + return imageExtList.includes(ext.toLowerCase()); +}; - const ext = extSplit[extSplit.length - 1]; - - return imageExtList.includes(ext.toLowerCase()); - -} - -export default imageChecker; \ No newline at end of file +export default imageChecker; diff --git a/package.json b/package.json index b7d83f2..d0d1220 100755 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "diskusage": "^1.1.3", "dotenv": "^8.2.0", "express": "^4.19.2", - "fluent-ffmpeg": "^2.1.2", + "fluent-ffmpeg": "^2.1.3", "font-awesome": "^4.7.0", "helmet": "^3.21.2", "history": "^4.10.1", @@ -117,6 +117,7 @@ "vite": "^5.2.13" }, "devDependencies": { + "@types/fluent-ffmpeg": "^2.1.24", "@types/lodash": "^4.17.5", "@typescript-eslint/eslint-plugin": "^2.20.0", "@typescript-eslint/parser": "^2.20.0",