Files
lamp ff7fd7e123 update
make reliable fetch and prevent crash

add stop.bat using pid file although exit hook does not work (how to do graceful stop on windows?)
2026-05-08 14:26:39 -07:00

168 lines
5.3 KiB
JavaScript

console.log(`started @ ${new Date().toISOString()}`);
import { encode } from "blurhash";
import mime from 'mime/lite';
import sharp from "sharp";
import exitHook from 'exit-hook';
import { execFileSync, spawnSync } from "child_process";
import { watch, existsSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
import { join } from "path";
import { access_token, homeserver, folder2room } from "./config.js";
exitHook(signal => {
console.log(`stopped @ ${new Date().toISOString()} signal ${signal}`);
unlinkSync("pid.txt");
});
writeFileSync("pid.txt", String(process.pid));
async function fetch(url, options) {
while (!res?.ok) {
try {
console.debug("fetch", url);
var res = await global.fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}, ${await res.text()}`);
} catch (error) {
console.error("fetch:", error.message);
}
await new Promise(r => setTimeout(r, 30000));
}
return res;
}
//todo use win32 api
function fileIdToPath(fileId) {
var out = execFileSync("fsutil", ["file", "queryFileNameById", "C:", fileId]).toString().trim();
var path = out.slice(out.indexOf("\\\\"));
//console.debug(`${fileId} = ${path}`);
return path;
}
function filePathToId(filePath) {
var out = execFileSync("fsutil", ["file", "queryFileId", filePath]).toString().trim();
var id = BigInt(out.slice(out.indexOf("0x")));
//console.debug(`${filePath} = ${id}`);
return id;
}
folder2room.forEach((roomId, folderId) => {
console.log(`loading ids for ${roomId}`);
var folderPath = fileIdToPath(folderId);
var existingFiles = readdirSync(folderPath).map(fileName => filePathToId(join(folderPath, fileName)));
console.log(`${existingFiles.length} files`);
watch(folderPath, async (eventType, fileName) => {
try {
console.log(eventType, folderPath, fileName);
if (eventType != "rename") return;
if (fileName.endsWith(".crdownload")) return;
var filePath = join(folderPath, fileName);
if (!existsSync(filePath)) return;
var fileId = filePathToId(filePath);
if (existingFiles.includes(fileId)) return;
existingFiles.push(fileId);
var mimetype = mime.getType(filePath) || "application/octet-stream";
await postImageOnMatrix(roomId, filePath, fileName, mimetype);
} catch (error) {
console.error(error.stack);
}
});
});
console.log("ready");
async function postImageOnMatrix(roomId, filePath, fileName, mimetype) {
console.log(`Posting ${filePath} to room ${roomId}`);
var file = readFileSync(filePath);
if (mimetype.startsWith("image/")) {
var {data, info} = await sharp(file).ensureAlpha().raw().toBuffer({resolveWithObject: true});
var blurhash = encode(
new Uint8ClampedArray(data),
info.width,
info.height,
// https://github.com/element-hq/element-web/blob/7dbffb348dfe8fbd83c9ea4bff9f37292b0979bb/src/workers/blurhash.worker.ts#L29-L31
info.width >= info.height ? 4 : 3,
info.height >= info.width ? 4 : 3,
);
} else if (mimetype.startsWith("video/")) {
let {stdout, stderr} = spawnSync("ffmpeg", ["-i", filePath, "-frames:v", "1", "-f", "webp", "-c:v", "libwebp", "-"]);
stderr = stderr.toString();
console.debug(stderr);
var duration = stderr.match(/Duration: (\d{2}):(\d{2}):(\d{2})\.(\d{2})/);
if (duration) {
duration = parseInt(duration[1]) * 3600 + parseInt(duration[2]) * 60 + parseInt(duration[3]) + parseInt(duration[4]) / 100;
duration = duration * 1000;
console.debug(`Duration: ${duration} ms`);
}
var {data, info} = await sharp(stdout).ensureAlpha().raw().toBuffer({resolveWithObject: true});
var thumbnail_info = {
w: info.width,
h: info.height,
mimetype: "image/webp",
size: stdout.length,
}
var blurhash = encode(
new Uint8ClampedArray(data),
info.width,
info.height,
// https://github.com/element-hq/element-web/blob/7dbffb348dfe8fbd83c9ea4bff9f37292b0979bb/src/workers/blurhash.worker.ts#L29-L31
info.width >= info.height ? 4 : 3,
info.height >= info.width ? 4 : 3,
);
var thumbnail_url = await uploadFile(fileName + ".webp", stdout, "image/webp");
} else if (mimetype.startsWith("audio/")) {
// todo get duration
}
var content_uri = await uploadFile(fileName, file, mimetype);
var txnId = Math.random();
var res = await fetch(`${homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/m.room.message/${txnId}`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"msgtype": mimetype.startsWith("image/") ? "m.image" : mimetype.startsWith("video/") ? "m.video" : mimetype.startsWith("audio/") ? "m.audio" : "m.file",
"body": fileName,
"info": {
"xyg.amorgan.blurhash": blurhash,
"mimetype": mimetype,
"w": info?.width,
"h": info?.height,
"size": file.length,
thumbnail_info,
thumbnail_url,
duration
},
"url": content_uri,
})
});
var {event_id} = await res.json();
console.log(event_id);
//todo store mapping
}
async function uploadFile(fileName, file, mimetype) {
var res = await fetch(`${homeserver}/_matrix/media/v3/upload?filename=${encodeURIComponent(fileName)}`, {
method: "POST",
headers: {
"Authorization": `Bearer ${access_token}`,
"Content-Type": mimetype,
},
body: file
});
var {content_uri} = await res.json();
console.debug(content_uri);
return content_uri;
}