diff options
author | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2022-10-04 19:12:41 +1100 |
---|---|---|
committer | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2022-10-04 19:22:50 +1100 |
commit | a3209fb7dcc251abd3196af836605fde9a018d3a (patch) | |
tree | 359063c4e858c34296ae23f7067af60b7f703215 | |
parent | capture exception in sentry for embed processing (diff) | |
download | server-a3209fb7dcc251abd3196af836605fde9a018d3a.tar.xz |
Video attachment support!
-rw-r--r-- | package-lock.json | 78 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/cdn/routes/attachments.ts | 44 | ||||
-rw-r--r-- | src/cdn/util/FileStorage.ts | 2 |
4 files changed, 118 insertions, 8 deletions
diff --git a/package-lock.json b/package-lock.json index 5f7abc85..4cba0900 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "exif-be-gone": "^1.3.1", "fast-zlib": "^2.0.1", "file-type": "16.5", + "fluent-ffmpeg": "^2.1.2", "form-data": "^4.0.0", "i18next": "^21.9.2", "i18next-http-middleware": "^3.2.1", @@ -50,6 +51,7 @@ "@types/amqplib": "^0.8.2", "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", + "@types/fluent-ffmpeg": "^2.1.20", "@types/i18next-node-fs-backend": "^2.1.1", "@types/json-bigint": "^1.0.1", "@types/jsonwebtoken": "^8.5.9", @@ -1659,6 +1661,15 @@ "@types/range-parser": "*" } }, + "node_modules/@types/fluent-ffmpeg": { + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.20.tgz", + "integrity": "sha512-B+OvhCdJ3LgEq2PhvWNOiB/EfwnXLElfMCgc4Z1K5zXgSfo9I6uGKwR/lqmNPFQuebNnes7re3gqkV77SyypLg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/i18next-node-fs-backend": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/i18next-node-fs-backend/-/i18next-node-fs-backend-2.1.1.tgz", @@ -2115,6 +2126,11 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3204,6 +3220,29 @@ "node": ">= 0.8" } }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", + "dependencies": { + "async": ">=0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fluent-ffmpeg/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3740,8 +3779,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -7561,6 +7599,15 @@ "@types/range-parser": "*" } }, + "@types/fluent-ffmpeg": { + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.20.tgz", + "integrity": "sha512-B+OvhCdJ3LgEq2PhvWNOiB/EfwnXLElfMCgc4Z1K5zXgSfo9I6uGKwR/lqmNPFQuebNnes7re3gqkV77SyypLg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/i18next-node-fs-backend": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@types/i18next-node-fs-backend/-/i18next-node-fs-backend-2.1.1.tgz", @@ -7947,6 +7994,11 @@ "tslib": "^2.0.1" } }, + "async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -8763,6 +8815,25 @@ "unpipe": "~1.0.0" } }, + "fluent-ffmpeg": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz", + "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==", + "requires": { + "async": ">=0.2.9", + "which": "^1.1.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -9160,8 +9231,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "js-yaml": { "version": "4.1.0", diff --git a/package.json b/package.json index 303324f5..37a21647 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/amqplib": "^0.8.2", "@types/bcrypt": "^5.0.0", "@types/cookie-parser": "^1.4.3", + "@types/fluent-ffmpeg": "^2.1.20", "@types/i18next-node-fs-backend": "^2.1.1", "@types/json-bigint": "^1.0.1", "@types/jsonwebtoken": "^8.5.9", @@ -58,6 +59,7 @@ "exif-be-gone": "^1.3.1", "fast-zlib": "^2.0.1", "file-type": "16.5", + "fluent-ffmpeg": "^2.1.2", "form-data": "^4.0.0", "i18next": "^21.9.2", "i18next-http-middleware": "^3.2.1", diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts index 2a1b6f09..9bd256aa 100644 --- a/src/cdn/routes/attachments.ts +++ b/src/cdn/routes/attachments.ts @@ -5,6 +5,9 @@ import FileType from "file-type"; import { HTTPError } from "lambert-server"; import { multer } from "../util/multer"; import imageSize from "image-size"; +import ffmpeg from "fluent-ffmpeg"; +import Path from "path"; +import { Duplex, Readable, Transform, Writable } from "stream"; const router = Router(); @@ -15,6 +18,14 @@ const SANITIZED_CONTENT_TYPE = [ "application/xhtml+xml", ]; +const probe = (file: string): Promise<ffmpeg.FfprobeData> => new Promise((resolve, reject) => { + ffmpeg.setFfprobePath(process.env.FFPROBE_PATH as string); + ffmpeg.ffprobe(file, (err, data) => { + if (err) return reject(err); + return resolve(data); + }); +}); + router.post( "/:channel_id", multer.single("file"), @@ -44,6 +55,13 @@ router.post( height = dimensions.height; } } + else if (mimetype.includes("video") && process.env.FFPROBE_PATH) { + const root = process.env.STORAGE_LOCATION || "../"; // hmm, stolen from FileStorage + const out = await probe(Path.join(root, path)); + const stream = out.streams[0]; // hmm + width = stream.width; + height = stream.height; + } const file = { id, @@ -63,10 +81,10 @@ router.get( "/:channel_id/:id/:filename", async (req: Request, res: Response) => { const { channel_id, id, filename } = req.params; + const { format } = req.query; - const file = await storage.get( - `attachments/${channel_id}/${id}/${filename}`, - ); + const path = `attachments/${channel_id}/${id}/${filename}`; + let file = await storage.get(path); if (!file) throw new HTTPError("File not found"); const type = await FileType.fromBuffer(file); let content_type = type?.mime || "application/octet-stream"; @@ -75,6 +93,26 @@ router.get( content_type = "application/octet-stream"; } + // lol, super gross + if (content_type.includes("video") && format == "jpeg" && process.env.FFMPEG_PATH) { + const promise = (): Promise<Buffer> => new Promise((resolve, reject) => { + ffmpeg.setFfmpegPath(process.env.FFMPEG_PATH as string); + const out: any[] = []; + const cmd = ffmpeg(Readable.from(file as Buffer)) + .format("mjpeg") + .frames(1) + .on("end", () => resolve(Buffer.concat(out))) + .on("error", (err) => reject(err)) + const stream = cmd.pipe(); + stream.on("data", (data) => { + out.push(data) + }); + }); + const res = await promise(); + file = res; + content_type = "jpeg"; + } + res.set("Content-Type", content_type); res.set("Cache-Control", "public, max-age=31536000"); diff --git a/src/cdn/util/FileStorage.ts b/src/cdn/util/FileStorage.ts index 0e31a50e..9386663f 100644 --- a/src/cdn/util/FileStorage.ts +++ b/src/cdn/util/FileStorage.ts @@ -35,7 +35,7 @@ export class FileStorage implements Storage { async set(path: string, value: any) { path = getPath(path); - if (!fs.existsSync(dirname(path))) fs.mkdirSync(dirname(path)); + if (!fs.existsSync(dirname(path))) fs.mkdirSync(dirname(path), { recursive: true }); value = Readable.from(value); const cleaned_file = fs.createWriteStream(path); |