summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json78
-rw-r--r--package.json2
-rw-r--r--src/cdn/routes/attachments.ts44
-rw-r--r--src/cdn/util/FileStorage.ts2
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);