-
Notifications
You must be signed in to change notification settings - Fork 282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature request: Option to pass file metadata on file stream itself #356
Comments
Hey, thanks for the thought. Just to better understand the idea – you need to pass And I'm unfamiliar with |
I probably should have provided a concrete example of my issue! My primary concern is not with passing data to For instance, when using the Basically, when uploading a file, all you have access to is this: const BoxSDK = require('box-node-sdk');
const sdk = new BoxSDK({ clientID: CLIENT_ID, clientSecret: CLIENT_SECRET });
const boxClient = sdk.getBasicClient(USER_ACCESS_TOKEN);
// no way to pass in file meta here.
boxClient.files.uploadFile(parentFolder.id, fileName, fileStream, callback); That goes through a couple layers until it finally reaches the internal wrapper around This means while something like the following work: boxClient.files.uploadFile(parentFolder.id, fs.createReadStream(somePath), callback);
boxClient.files.uploadFile(parentFolder.id, request(otherResource), callback); This does not: // readstream metadata is stuck back on the readstream. There is nothing on the passthrough stream.
boxClient.files.uploadFile(parentFolder.id, fs.createReadStream(somePath).pipe(new PassThrough()), callback); Or, more specific to my case, using // koa 2 router handler.
async function handleUpload(ctx) {
const busboy = new Busboy(ctx.req.headers);
const uploadPromises = [];
busboy.on('file', (fieldname, file, filename) => {
uploadPromises.push(new Promise((resolve, reject) => {
// `file` is a stream.
// `boxClient` here is attached to the current request context.
ctx.boxClient.files.uploadFile('0', file, (error, uploadList) => {
if (error) reject(error);
else resolve(uploadList);
});
}));
});
const busboyDone = new Promise((resolve, reject) => {
busboy.once('end', resolve);
busboy.once('error', reject);
});
ctx.req.pipe(busboy);
try {
await busboyDone;
await Promise.all(uploadPromises);
ctx.status = 200;
ctx.body = ':)';
}
catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = ':(';
}
} In this case, the file will fail to upload because the In my specific case, I got around this by making the client send file metadata before the file itself and then using the above trickery to make // koa 2 router handler.
async function handleUpload(ctx) {
const busboy = new Busboy(ctx.req.headers);
const uploadPromises = [];
const fileMetas = {};
// Check fields for any named `(fileFieldname).meta`
busboy.on('field', (fieldname, val) => {
if (/\.meta$/.test(fieldname)) {
const fileFieldname = fieldname.replace(/\.meta$/, '');
// size, name, type from the browser File object.
fileMetas[fileFieldname] = JSON.parse(val);
}
});
busboy.on('file', (fieldname, file, filename) => {
uploadPromises.push(new Promise((resolve, reject) => {
// Correlate data from `(fileFieldname).meta` with `(fileFieldname)`
if (!fileMetas[fieldname]) {
// no meta? dump file.
file.resume();
return reject(new Error(`No file meta for ${fieldname}`));
}
// fake it til ya make it.
file.httpVersion = '1.0';
file.headers = { 'content-length': fileMetas[fieldname].size };
file.name = filename;
// `file` is a stream.
// `boxClient` here is attached to the current request context.
ctx.boxClient.files.uploadFile('0', file, (error, uploadList) => {
if (error) reject(error);
else resolve(uploadList);
});
}));
});
const busboyDone = new Promise((resolve, reject) => {
busboy.once('end', resolve);
busboy.once('error', reject);
});
ctx.req.pipe(busboy);
try {
await busboyDone;
await Promise.all(uploadPromises);
ctx.status = 200;
ctx.body = ':)';
}
catch (error) {
console.error(error);
ctx.status = 500;
ctx.body = ':(';
}
}
Hope that explains the sort of situations I'm concerned with! |
Wow. :) Thanks for the detailed explanation. I'm still dizzy from the shear amount of new information :) Let me know to boil it down. Passing stream works when they're passed directly (i.e. fs-stream, request, http-stream), but doesn't work when they're being transform by some intermediary, like And your solution is to stick extra properties on the passed stream, like And if you have Transfer-Encoding chunked per multipart block, Or did I miss something? Thank you. |
Thanks for bearing with me here! Passing a stream directly from an fs-stream, Examples of those streams which do not work include:
My solution was to attach that missing data by making it look kiiiinda like an IncomingMessage with the I can't even use The solution that I would like to have is some obviously named property that I could attach to any stream that Which is to say, currently I have to do this to pass the data down through whatever library I'm using: // Make it look kinda like an http.IncomingMessage... except the path, which looks like an fs.ReadStream.
myStream.httpVersion = '1.0'; // value technically doesn't matter, but might as well use a valid value.
myStream.headers = { 'content-length': myStreamKnownLength };
myStream.name = myStreamFileName; And I would like to do this: // Obvious name makes what is happening here plain to see.
myStream._formDataOptions = {
filename: myStreamFileName,
knownLength: myStreamKnownLength,
}; So that things like handling data from Busboy or retaining metadata from an fs ReadStream after Transforms or Duplexes is doable in a known and documented way rather than in a way that takes advantage of the internals of I hope that's a bit clearer! |
Oh, I think I got it. So you just need more simple way to augment already pre-messed up streams. |
Exactly! And, that's actually a very good question. I suppose something like I suppose another option along this vein is to just attach So, from that, I think |
@joedski let me start by saying that this issue has been a TLDR for me, but I did skim it. I think the idea is a good one. I personally think attaching the properties directly to the stream is the best for a couple reasons.
If we were to go this route though the only allowed fields should be name, size and type like you mentioned earlier in order to match the browser side api. My dream is to eventually use the browsers file api isomorphically, similar to this and I think this is a good step toward that. I think it's something we could add to feature parity issue, what's your thoughts @alexindigo? |
I had concerns about possible name collisions, but looking at a fresh instance of a PassThrough stream didn't show any preexisting props named Isomorphism is certainly a good argument. I myself prefer things be neatly namespaced (in as much as underscores are 'neat') but ultimately any documented, fixed, supported API will do for me. |
As an extra vote for this: just spent 2 hours trying to figure out request/request#2499. Currently form-data is broken for anything other than the 3 supported stream types (fs readstreams, request streams and httpIncoming) when really all it needs to know is the file size. I ended up doing something similar to whats described here: adding fake httpVersion and content-length headers, and that worked. A standard way to specify the size for arbitrary stream-compatible objects would really be the way to go. |
Request doesn't currently have any logic to do
transfer-encoding: chunked
. It currently tries to always send acontent-length
header, butform-data
cannot just pull that number out of thin air without processing the whole stream.Ordinarily, this would be solved by passing the
knownLength
file option, either toform-data
directly or torequest
, however this is not possible when using libraries that themselves userequest
, such asbox-node-sdk
.Currently, I am working around this by taking advantage of
form-data
's automatic detection ofcontent-length
headers onIncomingMessage
instances and its use of thename
prop (presumably from BrowserFile
objects) to derive the mime-type, though this also means I'm having the client-side code send the file meta along side the file content itself.However, it would be nice if there were a more sanctioned way to do this, something like:
I'm open to doing up a PR if the idea presented here is acceptable.
The text was updated successfully, but these errors were encountered: