ff7fd7e123
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?)
168 lines
5.3 KiB
JavaScript
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;
|
|
} |