Last active
July 17, 2024 03:47
-
-
Save dcollien/76d17f69afe748afad7ff3a15ff9a08a to your computer and use it in GitHub Desktop.
Parse multi-part formdata in the browser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Multipart = { | |
parse: (function() { | |
function Parser(arraybuf, boundary) { | |
this.array = arraybuf; | |
this.token = null; | |
this.current = null; | |
this.i = 0; | |
this.boundary = boundary; | |
} | |
Parser.prototype.skipPastNextBoundary = function() { | |
var boundaryIndex = 0; | |
var isBoundary = false; | |
while (!isBoundary) { | |
if (this.next() === null) { | |
return false; | |
} | |
if (this.current === this.boundary[boundaryIndex]) { | |
boundaryIndex++; | |
if (boundaryIndex === this.boundary.length) { | |
isBoundary = true; | |
} | |
} else { | |
boundaryIndex = 0; | |
} | |
} | |
return true; | |
} | |
Parser.prototype.parseHeader = function() { | |
var header = ''; | |
var _this = this; | |
var skipUntilNextLine = function() { | |
header += _this.next(); | |
while (_this.current !== '\n' && _this.current !== null) { | |
header += _this.next(); | |
} | |
if (_this.current === null) { | |
return null; | |
} | |
}; | |
var hasSkippedHeader = false; | |
while (!hasSkippedHeader) { | |
skipUntilNextLine(); | |
header += this.next(); | |
if (this.current === '\r') { | |
header += this.next(); // skip | |
} | |
if (this.current === '\n') { | |
hasSkippedHeader = true; | |
} else if (this.current === null) { | |
return null; | |
} | |
} | |
return header; | |
} | |
Parser.prototype.next = function() { | |
if (this.i >= this.array.byteLength) { | |
this.current = null; | |
return null; | |
} | |
this.current = String.fromCharCode(this.array[this.i]); | |
this.i++; | |
return this.current; | |
} | |
function buf2String(buf) { | |
var string = ''; | |
buf.forEach(function (byte) { | |
string += String.fromCharCode(byte); | |
}); | |
return string; | |
} | |
function processSections(arraybuf, sections) { | |
for (var i = 0; i !== sections.length; ++i) { | |
var section = sections[i]; | |
if (section.header['content-type'] === 'text/plain') { | |
section.text = buf2String(arraybuf.slice(section.bodyStart, section.end)); | |
} else { | |
var imgData = arraybuf.slice(section.bodyStart, section.end); | |
section.file = new Blob([imgData], { | |
type: section.header['content-type'] | |
}); | |
var fileNameMatching = (/\bfilename\=\"([^\"]*)\"/g).exec(section.header['content-disposition']) || []; | |
section.fileName = fileNameMatching[1] || ''; | |
} | |
var matching = (/\bname\=\"([^\"]*)\"/g).exec(section.header['content-disposition']) || []; | |
section.name = matching[1] || ''; | |
delete section.headerStart; | |
delete section.bodyStart; | |
delete section.end; | |
} | |
return sections; | |
} | |
function multiparts(arraybuf, boundary) { | |
boundary = '--' + boundary; | |
var parser = new Parser(arraybuf, boundary); | |
var sections = []; | |
while (parser.skipPastNextBoundary()) { | |
var header = parser.parseHeader(); | |
if (header !== null) { | |
var headerLength = header.length; | |
var headerParts = header.trim().split('\n'); | |
var headerObj = {}; | |
for (var i = 0; i !== headerParts.length; ++i) { | |
var parts = headerParts[i].split(':'); | |
headerObj[parts[0].trim().toLowerCase()] = (parts[1] || '').trim(); | |
} | |
sections.push({ | |
'bodyStart': parser.i, | |
'header': headerObj, | |
'headerStart': parser.i - headerLength | |
}); | |
} | |
} | |
// add dummy section for end | |
sections.push({ | |
'headerStart': arraybuf.byteLength - 2 // 2 hyphens at end | |
}); | |
for (var i = 0; i !== sections.length - 1; ++i) { | |
sections[i].end = sections[i+1].headerStart - boundary.length; | |
if (String.fromCharCode(arraybuf[sections[i].end]) === '\r' || '\n') { | |
sections[i].end -= 1; | |
} | |
if (String.fromCharCode(arraybuf[sections[i].end]) === '\r' || '\n') { | |
sections[i].end -= 1; | |
} | |
} | |
// remove dummy section | |
sections.pop(); | |
sections = processSections(arraybuf, sections); | |
return sections; | |
} | |
return multiparts; | |
})() | |
}; |
Very helpful snippet. If you have access to fetch()
it should be possible to use text()
to get the raw multipart/form-data content with \r\n
included
{
var formdata = new FormData();
var dirname = "web-directory";
formdata.append(dirname, new Blob(["123"], {
type: "text/plain"
}), `${dirname}/file.txt`);
formdata.append(dirname, new Blob(["src"], {
type: "text/plain"
}), `${dirname}/src/file.txt`);
var body = await new Response(formdata).text();
console.log(body);
const boundary = body.slice(2, body.indexOf('\r\n'))
console.log(boundary)
new Response(body, {
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`
}
})
.formData()
.then(formData => {
console.log([...formData])
})
}
@Finesse Another way to do this when payload
is a TypedArray
(or ArrayBuffer
)
let ab = new Uint8Array(await response.clone().arrayBuffer());
let boundary = ab.subarray(2, ab.indexOf(13) + 1);
let archive = await new Response(ab, {
headers: {
"Content-Type": `multipart/form-data; boundary=${new TextDecoder().decode(boundary)}`,
},
})
.formData()
.then((data) => {
console.log([...data]);
return data;
}).catch((e) => {
console.warn(e);
});
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@guest271314 You are right, I've amended my code snippet