192 lines
5.8 KiB
TypeScript
192 lines
5.8 KiB
TypeScript
import Router from "@koa/router";
|
|
import { hasShape, predicates } from "@sealcode/ts-predicates";
|
|
import { spawn } from "child_process";
|
|
import Koa from "koa";
|
|
import mount from "koa-mount";
|
|
import serve from "koa-static";
|
|
import qs from "qs";
|
|
import { Readable } from "stream";
|
|
import { v4 as uuid } from "uuid";
|
|
import { DOCKER_ARGS, IMAGE_NAME, VOLUME_MOUNT } from "./docker-args";
|
|
import { requests } from "./memory";
|
|
import ScreenshotRequest from "./request";
|
|
|
|
const router = new Router();
|
|
|
|
// response
|
|
const app = new Koa();
|
|
const Static = new Koa();
|
|
Static.use(serve("./static"));
|
|
|
|
app.use(mount("/static", Static));
|
|
|
|
function attach(docker_id: string, output_stream: Readable) {
|
|
// to prevent browser timeout
|
|
const interval = setInterval(() => output_stream.push("<span></span>"), 500);
|
|
const task = spawn("docker", ["logs", "-f", docker_id]);
|
|
task.stdout.on("data", (d: Buffer) => {
|
|
output_stream.push(d);
|
|
console.log("DATA!", d.toString());
|
|
});
|
|
task.stderr.on("data", (d: Buffer) => {
|
|
/* output_stream.push(d); */
|
|
console.log("STDERR!", d.toString());
|
|
});
|
|
task.stdout.on("error", (d) => {
|
|
output_stream.push(d);
|
|
});
|
|
task.on("close", () => {
|
|
output_stream.push("</pre>");
|
|
output_stream.push(/* HTML */ `<script>
|
|
clearInterval(window.interval);
|
|
</script>`);
|
|
clearInterval(interval);
|
|
output_stream.push(null);
|
|
});
|
|
}
|
|
|
|
router.get("/", async (ctx) => {
|
|
ctx.body = /* HTML */ `<!DOCTYPE html>
|
|
<html>
|
|
<body>
|
|
<form onsubmit="formSubmit(event)">
|
|
<label for="url_input">URL:</label>
|
|
<input type="text" name="url" id="url_input" />
|
|
<br />
|
|
<label for="domains">Domeny (oddzielone przecinkami):</label>
|
|
<input
|
|
type="text"
|
|
name="domains"
|
|
id="domains"
|
|
style="width: calc(100vw - 30%)"
|
|
value="doubleclick.net,facebook.com"
|
|
/>
|
|
<br />
|
|
<input type="submit" />
|
|
</form>
|
|
<code><pre id="output"></pre></code>
|
|
<code><pre id="stdout"></pre></code>
|
|
</body>
|
|
<script>
|
|
async function sleep(time) {
|
|
return new Promise((resolve) => setTimeout(resolve, time));
|
|
}
|
|
async function formSubmit(e) {
|
|
e.preventDefault();
|
|
let response = { status: "sending first request..." };
|
|
const url = \`/api/requests?url=\${url_input.value}&\${domains.value
|
|
.split(",")
|
|
.map((d) => "domains[]=" + d)
|
|
.join("&")}\`;
|
|
const { id } = await (await fetch(url, { method: "post" })).json();
|
|
do {
|
|
response = await (await fetch(\`/api/requests/\${id}\`)).json();
|
|
output.innerHTML = JSON.stringify(response, null, " ").replace(
|
|
/\\/(static|api)\\/.*(.png|.jpg|all-screenshots|v=[0-9]+)/g,
|
|
'<a href="$&">$&</a>'
|
|
);
|
|
stdout.innerHTML = response.output.replace(
|
|
/\\/(static|api)\\/.*(.png|.jpg|all-screenshots)/g,
|
|
'<a href="$&">$&</a>'
|
|
);
|
|
await sleep(1000);
|
|
} while (response.status !== "finished");
|
|
}
|
|
</script>
|
|
</html>`;
|
|
});
|
|
|
|
router.get("/preview", async (ctx) => {
|
|
const response = new Readable({
|
|
read() {
|
|
/*noop */
|
|
},
|
|
});
|
|
// stream data
|
|
ctx.response.set("content-type", "txt/html");
|
|
ctx.type = "html"; // <-- THIS is the important step!
|
|
ctx.body = response;
|
|
response.push("<!doctype html>");
|
|
const id = uuid();
|
|
response.push(
|
|
`<img id="preview" width="1080" height="608" src="/static/${id}/preview.png?id=0"/><br/>`
|
|
);
|
|
response.push(/* HTML */ `<script>
|
|
window.interval = setInterval(() => (preview.src = preview.src + "0"), 500);
|
|
</script>`);
|
|
const params = qs.parse(ctx.querystring);
|
|
if (!hasShape({ url: predicates.string }, params)) {
|
|
throw new Error("MISSING URL PARAM");
|
|
}
|
|
response.push(`Got request to screenshot ${params.url}<pre>`);
|
|
let docker_id = "";
|
|
if (!params.url) {
|
|
ctx.body = "specify url!";
|
|
return;
|
|
}
|
|
const starter = spawn(
|
|
"docker",
|
|
[
|
|
...DOCKER_ARGS,
|
|
`{"url": "${params.url}", "third_party_domains": ["hotjar.com", "cookielaw.org", "facebook.com", "gemius.pl"]}`,
|
|
id,
|
|
],
|
|
{ cwd: process.cwd() }
|
|
);
|
|
starter.stdout.on("data", (data: Buffer) => {
|
|
docker_id += data.toString().replace(/\n/g, "");
|
|
});
|
|
|
|
starter.on("close", () => {
|
|
response.push("spawned " + docker_id);
|
|
attach(docker_id, ctx.body);
|
|
});
|
|
});
|
|
|
|
router.post("/api/requests", async (ctx) => {
|
|
const params = qs.parse(ctx.querystring);
|
|
if (!hasShape({ url: predicates.string }, params)) {
|
|
ctx.body = "Specify url";
|
|
ctx.status = 422;
|
|
return;
|
|
}
|
|
if (!hasShape({ domains: predicates.array(predicates.string) }, params)) {
|
|
ctx.body = "Specify domains as an array of strings";
|
|
ctx.status = 422;
|
|
return;
|
|
}
|
|
if (!Array.isArray(params.domains)) {
|
|
ctx.body = "'domains' should be an array of strings";
|
|
ctx.status = 422;
|
|
return;
|
|
}
|
|
const request = new ScreenshotRequest(params.url, params.domains);
|
|
ctx.status = 303;
|
|
ctx.redirect(`/api/requests/${request.id}`);
|
|
});
|
|
|
|
router.get("/api/requests/:id", async (ctx) => {
|
|
const request = requests[ctx.params.id];
|
|
if (!request) {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
ctx.body = await request.getJSON();
|
|
});
|
|
|
|
router.get("/api/requests/:id/all-screenshots", async (ctx) => {
|
|
const request = requests[ctx.params.id];
|
|
if (!request || request.status != "finished") {
|
|
ctx.status = 404;
|
|
return;
|
|
}
|
|
ctx.attachment(`${request.url.replace(/\W/g, "_")}_all-screenshots.zip`);
|
|
ctx.response.set("content-type", "application/zip");
|
|
ctx.body = request.getZIP();
|
|
});
|
|
|
|
app.use(router.routes()).use(router.allowedMethods());
|
|
const port = 3000;
|
|
app.listen(port);
|
|
console.log(`server started on port ${port}`);
|