KV multiple image upload - Tutorial
GET
The get should be done with KV bindings (Fast, 30 ms), in arrayBuffer mode
const value = await KV.get(k, 'arrayBuffer');
PUT
You have 2 possibilities :
- Use the KV API to put the file in KV (Extremely slow, 1.25 s)
- Use a formData parser to get the file, then use the KV bindings to put the file in KV (Relatively fast, 50-60 ms)
Problem : Response.formData() does not work
Unfortunately, handling multipart/form-data with binary files like images by using response.formData() does not work in Cloudflare Workers. The response goes through a utf-8 transformation which corrupts the files and make them unreadable. In addition, you lose the filename information, which sometimes is important.
Solution : Custom formData parser
I wrote a custom formData parser in JavaScript to do the job. It does not respect the standard API, but it does the job roughtly. It handles binary files like images, text and json files, and simple form values, even those on multiple lines. That should be enough for most usages. Feel free to alter to match your needs.
–
Try to copy / paste this JS code snippet in your Chrome console:
const main = async () => {
const o = {};
const o2 = {};
const body = new FormData();
o['50'] = 'data:image/webp;base64,UklGRlIFAABXRUJQVlA4IEYFAABQFQCdASoyAEsAPm00k0ekIyGhKBM7uIANiUAYP/E47JItebRUci3F6WNuxzqDNRcX/tDNVx79R2oi5iO83GSwtvS2WD6Bnz2+ODh34MAr9LxRRyWtOohZTGCza2fg8hcMAupIMZFRya5Y5SxM/oHw2ru86zpogeNUnSnxSo+zCLDLAt/pdPzrJD6E+pqwUUgucIQH7GGEJ5SBBi4qBoNvHaTKRacqewuLQUe5FQLWJHuAAAD+6Zb5OwaddpMri4OFtes/WvWfyLjN9Qbs3jrGT3MN4n4XAbnfwxPnr3Pb/qVSTRKhv9/f2VsPYTKQM//0Vl9d9tP98L+anGlXQAQ3qY6ZgkF2ZOug/WzSo0E9pEMudHwl/ZbJHWin04CCt0w4CTv4LnKBwt4pgWBfSbVhXvSB3bRFmSjNx5/9zDsAT2Yqfg1Ej2loJbdE1kWvYTS3BVKu3v8QWmd165fF3y5irwahactojWUcBJpH/XWfCxIQIGmVlhaq8YyUEMRBcBtcGTFXvzedJPMHb5EtxAYKs5zmV7RB0PYuIo2OAADq138R+SIcWNKE/jeHR7/8zZHwY/xOp/o0cLYsIaGeBnyAIu/2xzGa4Py1amVSIgYXyx4M4B1Ayp8f0GiUTMuMY0LWyPXckGTx8Vw2mGDAP923mFg/csGg/YC3RJ4pcH0lFR66DKRTFsIZYP2SylCOy3HSBXpSE5Bfqot+8SVrBE/RztjSKOxAQj6shwJ7L1Vzc28P2XhZNwikHIiZPxrOTi/w2V4/060iyoEnS+yzGL5CqDYwdjGXW/4r4pqC2wvfM7eS3UYQteQwdHjAQMTLBEDHJbFz/X5hN5he+drNy8+OuyS+ohKnwaADuW6nVrzDAsjFrunm3leKWHpBW8Hueto1OSRHMVOX/CDZGfNSoPHHuPxxeDy7jHnBGICmqLBB3wd/2lM4ytiVtZFTTgeLhuMk+5n1FIj4WjZocRR2/vKRa84xqy178t0NqHuMRCzaJ280aDpYxvI7924uLt8xx6zxB3BVdCkUc80laqT9h/zJeMNZvcl7rbkDC7aKgwp7N6uN8HDD5ZFMgMDYTvyd+3BaQQ06q4GhDIIW1yRJu/7YeLd2QfAehniYuESI1+06uLouzLCDSUtPRSpTlEkAVDsjWDbnGdz56+kMMmf+TLV0gn7/VJ/1cJi010IsakalaKFYOsJz+iYVrOB2dLHEbBbzZKnY48uMiF/MZjR/2WZ0t6fvo/WGOe/x9fwJP+YDSe/pJwfLBZ/ezri+nDmbOaxNkyKunyo5gGzBV3wLNsxnU774eXfzFn9kTsuK+yD0CEdIIBmdq7ZxOfLnHFTfElSqRd/RHzw8j6XVWjl7EVF4yNbrtksNE9OD3pXijW/qzf4rgWLdVoYRZsZzCMYu/Ixe6zAgeGVfyT3AyvPG/U3x7ebntr4/5yPiG+tptQyWm4H3Wll6Qmlc+ZNXUEk/u9b4WudYwEG3Zs1Ki66YtZ64L6zM9NcpRODsYslzHjmibGkWK7KxUOXmh3r+Y4srhwCBpFdVbEFHEg8x/4AZnJyU8zqSZu104Ilsn+9N5UbUYGYGdFFfFP0NiLo0Keuei4qL1q4G2RbYplYj10dXzi7FWGRNCr56Z9xaEPBLRCPEwj/svxhQSxD79yZ1vdLE4HQvHaLv4L0FjuOwFCFvZ31f/ZE1XTLVg2tgM12CqqD8I+TCvcHea3JOgGMggxUyR2L1/Hzt2yiWjNGUs3nw2CK6TdbZPVmHRE2C5W+NbQBUfR40Tty7bmupuzufkMTt0ACSRGbAAAA=';
o['100'] = 'data:image/webp;base64,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';
o['test.txt'] = 'data:text/plain;base64,YWFh'; //contains : 'aaa'
o['test.json'] = 'data:application/json;base64,eyJKb2huIjoiRG9lIn0='; //contains : '{"John":"Doe"}'
for (const k in o) { body.append(k, await (await fetch(o[k])).blob(), k); }
o2['1'] = JSON.stringify({ name: 'Alice' });
o2['2'] = JSON.stringify({ name: 'Bob' });
o2['x'] = 'yyy';
o2['u'] = "v\nv2";
for (const k in o2) { body.append(k, o2[k]); }
const res = new Response(body);
//console.log(JSON.stringify(Object.fromEntries(res.headers)));
//console.log(await res.clone().text());
const event = { request: res, response: res }; // Fake event, fake request
console.time('formData');
const fd = await formData(event); // Returns an array [{name,value,filename,content-type}]
console.timeEnd('formData');
console.log(fd);
if (!fd) { return; }
const z = fd[1];
const blob = new Blob([new Uint8Array(z.value, 0, z.value.byteLength)]);
//const blob = new Blob(z.value);
const url = URL.createObjectURL(blob);
document.querySelector('body').insertAdjacentHTML('beforeend', `<img src='${url}' width=50>`);
//URL.revokeObjectURL(url);
}
main();
const formData = async (event) => {
// content-type headers must be valid and offer a boundary
const boundary = await getBoundary(event);
if (!boundary) { return null; }
// Payload size must be under 1000kB
const ab = await event.request.arrayBuffer();
const max = ab.byteLength;
if (!await lessThan1000kB(max)) { return null; }
// Start to parse
const t = new TextDecoder();
const fd = Array();
let o = Object();
let i = 0;
//console.log({ ab, i, max });
while (i < max) {
i = await getPart(ab, i, max, boundary, t, o);
if (!o || !o.name) { break; }
fd.push(o);
o = Object();
}
return fd;
}
const lessThan1000kB = async (max) => {
if (Math.ceil(max / 1000) > 1000) { return false; }
return true;
}
const getBoundary = async (event) => {
const ct = event.request.headers.get('content-type');
return `--${(ct.match(/^multipart\/form\-data; boundary=(.+)$/))?.[1]}`;
}
const getPart = async (ab, start, max, boundary, t, o) => {
let s = '';
let i = start;
//console.log('A');
i = await readString(boundary, i, ab, t, max);
if (i === max) { return max; }
//console.log('B');
if (await eof(i, ab, t, max)) { return max; }
if (i === max) { return max; }
//console.log('C');
i = await readNewLine(i, ab, t, max);
if (i === max) { return max; }
//console.log('D');
i = await readString('Content-Disposition: form-data; name="', i, ab, t, max);
if (i === max) { return max; }
//console.log('E');
i = await captureUntil(['"'], i, ab, t, max, o, 'name');
if (i === max) { return max; }
//console.log('F');
if (!o.name) { o = null; return max; }
if (i === max) { return max; }
//console.log('G');
let x;
if ((x = await readValue("\"\n\n", boundary, i, ab, t, max, o))) { return x; }
if ((x = await readValue("\"\r\n\r\n", boundary, i, ab, t, max, o))) { return x; }
return readFile(boundary, i, ab, t, max, o);
}
// Depending on carriage return prefix,
// Try to read a simple value (implicit text/plain)
const readValue = async (s, boundary, i, ab, t, max, o) => {
if (!await checkString(s, i, ab, t, max)) { return false; }
//console.log('I');
i = await readString(s, i, ab, t, max);
//console.log('I2');
i = await captureUntil([/*`${"\r\n"}${boundary}`, */`${"\n"}${boundary}`], i, ab, t, max, o, 'value');
//console.log('I3');
i -= boundary.length - 1;
return i;
}
// Read a file, like text, json or images
const readFile = async (boundary, i, ab, t, max, o) => {
//console.log('J');
i = await readString('"; filename="', i, ab, t, max);
if (i === max) { return max; }
//console.log('K');
i = await captureUntil(['"'], i, ab, t, max, o, 'filename');
if (i === max) { return max; }
//console.log('L');
i = await readString('"', i, ab, t, max);
i = await readNewLine(i, ab, t, max);
if (i === max) { return max; }
//console.log('M');
i = await readString('Content-Type: ', i, ab, t, max);
if (i === max) { return max; }
//console.log('N');
i = await captureUntil(["\r", "\n"], i, ab, t, max, o, 'content-type');
if (i === max) { return max; }
//console.log('O');
if (!o['content-type']) { o = null; return max; }
if (i === max) { return max; }
//console.log('P');
i = await readNewLine(i, ab, t, max);
if (i === max) { return max; }
//console.log('Q');
i = await readNewLine(i, ab, t, max);
if (i === max) { return max; }
//console.log('R');
i = await captureUntil([`${"\r\n"}${boundary}`, `${"\n"}${boundary}`], i, ab, t, max, o, 'value', o['content-type']);
if (i === max) { return max; }
//console.log('S');
i -= boundary.length - 1;
return i;
}
const charAt = async (i, ab, t) => {
return t.decode(ab.slice(i, i + 1));
}
// Check if end of file is found
// Meaningful only when used directly after readString(boundary)
const eof = async (i, ab, t, max) => {
if (i + 1 === max) { return false; }
const s = t.decode(ab.slice(i, i + 2));
return s === '--';
}
// Try to read \n
// If \r is found, second chance is given
// If max reached, or if \n not found, return max to stop the machine
const readNewLine = async (i, ab, t, max) => {
if (await charAt(i, ab, t) === "\r") { if (++i === max) { return max; } }
if (await charAt(i, ab, t) === "\n") { return ++i; }
return max;
}
const checkString = async (v, i, ab, t, max) => {
return readString(v, i, ab, t, max, true)
}
// Try to read v
// If max reached, or if v not found, return max
const readString = async (v, i, ab, t, max, check = false) => {
const start = i;
let j = 0;
const len = v.length;
while (i < max && j < len) {
++i;
++j;
}
const s = t.decode(ab.slice(start, i));
//console.log('expected : v :', v);
//console.log(`${check ? 'check' : 'read '}${' : s :'}`, s);
if (s === v) { return check ? true : i; }
return check ? false : max;
}
// Try to capture, until we get one the values inside a (Ex : \r or \n)
const captureUntil = async (a, i, ab, t, max, o, prop, contentType = 'text/plain') => {
return readUntil(a, i, ab, t, max, true, o, prop, contentType);
}
// Try to read, until we get one the values inside a (Ex : \r or \n)
const readUntil = async (a, i, ab, t, max, capture = false, o = Object(), prop = '_', contentType = 'text/plain') => {
const start = i;
// Until end
while (i < max) {
//console.log(i, await charAt(i, ab, t));
// For each token
for (const v of a) {
const end = Math.max(start, i - v.length);
// Extract ending text, at the size of the token
const tmp = t.decode(ab.slice(end, i));
// If extracted ending text not equals to the token, do nothing
if (tmp !== v) { continue; }
// Capture data
if (capture) { await captureData(ab, start, end, t, o, prop, contentType); }
// Return current position
--i;
//console.log('readUntilOut1', i);
return i;
}
// Go to next char
++i;
}
// console.log('readUntilOut2', i);
return i;
}
const captureData = async (ab, start, end, t, o, prop, contentType) => {
//console.log('capture', prop, contentType);
//console.log(start, end);
const sub = ab.slice(start, end);
await captureJson(sub, t, o, prop, contentType);
await captureTextAsJson(sub, t, o, prop, contentType);
await captureText(sub, t, o, prop, contentType);
await captureBlob(sub, o, prop, contentType);
}
const captureJson = async (sub, t, o, prop, contentType) => {
if (contentType !== 'application/json') { return; }
let s = t.decode(sub);
//console.log('captureJson', s);
o[prop] = JSON.parse(s);
}
const captureTextAsJson = async (sub, t, o, prop, contentType) => {
const s = t.decode(sub);
if (contentType !== 'text/plain' || s[0] !== '{') { return; }
//console.log('captureTextAsJson1');
let o2;
//try { o2 = JSON.parse(s); } catch (e) { }
try { o2 = JSON.parse(s); } catch (e) { }
if (!o2) { return; }
o[prop] = o2;
o['content-type'] = 'application/json';
//console.log('captureTextAsJson2', s);
}
const captureText = async (sub, t, o, prop, contentType) => {
//console.log('captureText1', contentType);
if (contentType !== 'text/plain' ||
o['content-type'] && o['content-type'] !== 'text/plain') { return; }
let s = t.decode(sub);
//console.log('captureText2', s);
o[prop] = s;
}
const captureBlob = async (sub, o, prop, contentType) => {
if (contentType === 'text/plain' || contentType === 'application/json') { return; }
//console.log('captureBlob');
o[prop] = sub;
}
It should display :
0: {name: "50", filename: "50", content-type: "image/webp", value: ArrayBuffer(1370)}
1: {name: "100", filename: "100", content-type: "image/webp", value: ArrayBuffer(3414)}
2: {name: "test.txt", filename: "test.txt", content-type: "text/plain", value: "aaa"}
3: {name: "test.json", filename: "test.json", content-type: "application/json", value: {…}}
4: {name: "1", value: {…}, content-type: "application/json"}
5: {name: "2", value: {…}, content-type: "application/json"}
6: {name: "x", value: "yyy"}
7: {name: "u", value: "v↵v2"}
… indicating the form data has been successfully parsed, in 39 ms.
Now, with KV bindings you just have to :
- PUT the value into KV
- GET the value from KV using the arrayBuffer mode
And voilà !