Skip to content

Commit a4a3a2d

Browse files
committed
feat(demo): add deno deploy ingest endpoint
1 parent 5c79e6a commit a4a3a2d

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"exclude": [
99
".vscode",
1010
"tests",
11+
"docs",
1112
"coverage",
1213
"node_modules",
1314
"AGENTS.md"

docs/ingest/main.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { dhash } from "../../mod.ts";
2+
3+
const MAX_BYTES = 10 * 1024 * 1024;
4+
5+
function json(data: unknown, status = 200): Response {
6+
return new Response(JSON.stringify(data, null, 2) + "\n", {
7+
status,
8+
headers: {
9+
"content-type": "application/json; charset=utf-8",
10+
"cache-control": "no-store",
11+
"x-content-type-options": "nosniff",
12+
},
13+
});
14+
}
15+
16+
function html(body: string, status = 200): Response {
17+
return new Response(body, {
18+
status,
19+
headers: {
20+
"content-type": "text/html; charset=utf-8",
21+
"cache-control": "no-store",
22+
"x-content-type-options": "nosniff",
23+
},
24+
});
25+
}
26+
27+
const INDEX_HTML = `<!doctype html>
28+
<html lang="en">
29+
<head>
30+
<meta charset="utf-8" />
31+
<meta name="viewport" content="width=device-width, initial-scale=1" />
32+
<title>dHash ingest (Deno Deploy)</title>
33+
<style>
34+
:root { color-scheme: light; }
35+
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; margin: 24px; }
36+
.card { max-width: 760px; border: 1px solid #ddd; border-radius: 12px; padding: 16px; }
37+
h1 { margin: 0 0 8px 0; font-size: 20px; }
38+
p { margin: 8px 0; line-height: 1.4; }
39+
input { margin: 8px 0; }
40+
button { padding: 8px 12px; border-radius: 10px; border: 1px solid #bbb; background: #f7f7f7; cursor: pointer; }
41+
button:disabled { opacity: 0.6; cursor: not-allowed; }
42+
pre { background: #0b1020; color: #e6e6e6; padding: 12px; border-radius: 10px; overflow: auto; }
43+
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
44+
.muted { color: #555; font-size: 12px; }
45+
a { color: inherit; }
46+
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
47+
</style>
48+
</head>
49+
<body>
50+
<div class="card">
51+
<h1>dHash ingest endpoint</h1>
52+
<p class="muted">
53+
Upload an image, get back the 64-bit dHash computed by <code>@claudiu-ceia/dhash</code>.
54+
This is a minimal test to see whether <code>sharp</code> works on Deno Deploy.
55+
</p>
56+
57+
<div class="row">
58+
<input id="file" type="file" accept="image/*" />
59+
<button id="go" disabled>Compute hash</button>
60+
<span id="status" class="muted"></span>
61+
</div>
62+
63+
<p class="muted">
64+
POST raw bytes to <code>/hash</code> (max 10MB). Example:
65+
<code>curl -X POST $HOST/hash --data-binary @img.png</code>
66+
</p>
67+
68+
<pre id="out">{ "hash": "..." }</pre>
69+
<p class="muted">
70+
Repo: <a href="https://github.com/ClaudiuCeia/dhash" target="_blank" rel="noreferrer">github.com/ClaudiuCeia/dhash</a>
71+
| JSR: <a href="https://jsr.io/@claudiu-ceia/dhash" target="_blank" rel="noreferrer">jsr.io/@claudiu-ceia/dhash</a>
72+
</p>
73+
</div>
74+
75+
<script type="module">
76+
const fileEl = document.getElementById("file");
77+
const goEl = document.getElementById("go");
78+
const outEl = document.getElementById("out");
79+
const statusEl = document.getElementById("status");
80+
81+
function setStatus(s) { statusEl.textContent = s; }
82+
83+
fileEl.addEventListener("change", () => {
84+
goEl.disabled = !(fileEl.files && fileEl.files.length === 1);
85+
outEl.textContent = '{ "hash": "..." }';
86+
setStatus("");
87+
});
88+
89+
goEl.addEventListener("click", async () => {
90+
const f = fileEl.files && fileEl.files[0];
91+
if (!f) return;
92+
if (f.size > ${MAX_BYTES}) {
93+
outEl.textContent = JSON.stringify({ error: "File too large (max 10MB)." }, null, 2);
94+
return;
95+
}
96+
97+
goEl.disabled = true;
98+
setStatus("Uploading...");
99+
try {
100+
const res = await fetch("/hash", {
101+
method: "POST",
102+
headers: { "content-type": f.type || "application/octet-stream" },
103+
body: await f.arrayBuffer(),
104+
});
105+
const text = await res.text();
106+
outEl.textContent = text || String(res.status);
107+
setStatus(res.ok ? "OK" : ("HTTP " + res.status));
108+
} catch (e) {
109+
outEl.textContent = JSON.stringify({ error: String(e) }, null, 2);
110+
setStatus("Failed");
111+
} finally {
112+
goEl.disabled = false;
113+
}
114+
});
115+
</script>
116+
</body>
117+
</html>
118+
`;
119+
120+
Deno.serve(async (req) => {
121+
const url = new URL(req.url);
122+
const path = url.pathname;
123+
124+
if (req.method === "GET" && path === "/") return html(INDEX_HTML);
125+
126+
if (req.method === "POST" && path === "/hash") {
127+
const lenHeader = req.headers.get("content-length");
128+
if (lenHeader) {
129+
const len = Number(lenHeader);
130+
if (Number.isFinite(len) && len > MAX_BYTES) {
131+
return json({ error: "Payload too large (max 10MB)." }, 413);
132+
}
133+
}
134+
135+
const buf = await req.arrayBuffer();
136+
if (buf.byteLength === 0) {
137+
return json({ error: "Empty request body." }, 400);
138+
}
139+
if (buf.byteLength > MAX_BYTES) {
140+
return json({ error: "Payload too large (max 10MB)." }, 413);
141+
}
142+
143+
try {
144+
const hash = await dhash(new Uint8Array(buf));
145+
return json({ hash });
146+
} catch (err) {
147+
const detail = err instanceof Error
148+
? (err.stack ?? err.message)
149+
: String(err);
150+
return json(
151+
{
152+
error: "Failed to compute hash.",
153+
detail,
154+
hint:
155+
"If this is running on Deno Deploy, sharp/npm native addon initialization may not be supported.",
156+
},
157+
500,
158+
);
159+
}
160+
}
161+
162+
return json({ error: "Not found." }, 404);
163+
});

0 commit comments

Comments
 (0)