Files
f0ckm/src/meta_strip_handler.mjs
2026-04-25 19:51:52 +02:00

94 lines
3.7 KiB
JavaScript

import { promises as fs } from "fs";
import db from "./inc/sql.mjs";
import lib from "./inc/lib.mjs";
import cfg from "./inc/config.mjs";
import queue from "./inc/queue.mjs";
import path from "path";
import { parseMultipart, collectBody } from "./inc/multipart.mjs";
/**
* Strip GPS/location EXIF data from an uploaded image chunk and return the clean bytes.
* POST /api/v2/meta/strip-gps
*/
export const handleMetaStrip = async (req, res) => {
// Manual session lookup
if (!req.session && req.cookies?.session) {
try {
const user = await db`
select "user".id, "user".login, "user".user, "user".admin, "user".is_moderator, "user_sessions".id as sess_id, "user_sessions".csrf_token, "user_options".*
from "user_sessions"
left join "user" on "user".id = "user_sessions".user_id
left join "user_options" on "user_options".user_id = "user_sessions".user_id
where "user_sessions".session = ${lib.sha256(req.cookies.session)}
limit 1
`;
if (user.length > 0) req.session = user[0];
} catch (err) {}
}
if (!req.session) {
res.writeHead(401, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ success: false, msg: 'Unauthorized' }));
}
const csrfToken = req.headers['x-csrf-token'];
if (!csrfToken || csrfToken !== req.session.csrf_token) {
res.writeHead(403, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ success: false, msg: 'Invalid CSRF token' }));
}
try {
const contentType = req.headers['content-type'] || '';
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/);
if (!contentType.includes('multipart/form-data') || !boundaryMatch) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ success: false, msg: 'Invalid content type' }));
}
const boundary = boundaryMatch[1] || boundaryMatch[2];
const body = await collectBody(req, 20 * 1024 * 1024); // 20MB max for full image
const parts = parseMultipart(body, boundary);
const file = parts.file;
if (!file || !file.data) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ success: false, msg: 'No file provided' }));
}
const tmpPath = path.join(cfg.paths.tmp, `strip_gps_${Math.random().toString(36).substring(7)}`);
await fs.writeFile(tmpPath, file.data);
// Strip all GPS and location EXIF tags in-place
await queue.spawn('exiftool', [
'-gps:all=',
'-xmp:gps:all=',
'-iptc:city=',
'-iptc:province-state=',
'-iptc:country-primary-location-name=',
'-iptc:sub-location=',
'-overwrite_original',
tmpPath,
], { quiet: true, ignoreExitCode: true });
// Read the cleaned file and send it back
const cleanData = await fs.readFile(tmpPath);
await fs.unlink(tmpPath).catch(() => {});
const filename = file.filename || 'image';
const mime = file.contentType || 'application/octet-stream';
res.writeHead(200, {
'Content-Type': mime,
'Content-Length': cleanData.length,
'Content-Disposition': `attachment; filename="${filename}"`,
'X-GPS-Stripped': '1',
});
res.end(cleanData);
} catch (err) {
console.error('[META-STRIP-GPS ERROR]', err);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, msg: 'Failed to strip GPS data' }));
}
};