Skip to content
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: file upload allows buffer and streams #389

Merged
merged 1 commit into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ export class StreamClient<

upload(
url: string,
uri: string | File | NodeJS.ReadStream,
uri: string | File | Buffer | NodeJS.ReadStream,
name?: string,
contentType?: string,
onUploadProgress?: OnUploadProgress,
Expand Down
4 changes: 2 additions & 2 deletions src/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ export class StreamFileStore {
// param. If you don't then Android will refuse to perform the upload
/**
* upload a File instance or a readable stream of data
* @param {File|NodeJS.ReadStream|string} uri - File object or stream or URI
* @param {File|Buffer|NodeJS.ReadStream|string} uri - File object or stream or URI
* @param {string} [name] - file name
* @param {string} [contentType] - mime-type
* @param {function} [onUploadProgress] - browser only, Function that is called with upload progress
* @return {Promise<FileUploadAPIResponse>}
*/
upload(
uri: string | File | NodeJS.ReadStream,
uri: string | File | Buffer | NodeJS.ReadStream,
name?: string,
contentType?: string,
onUploadProgress?: OnUploadProgress,
Expand Down
4 changes: 2 additions & 2 deletions src/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ export class StreamImageStore {
// param. If you don't then Android will refuse to perform the upload
/**
* upload an Image File instance or a readable stream of data
* @param {File|NodeJS.ReadStream|string} uri - File object or stream or URI
* @param {File|Buffer|NodeJS.ReadStream|string} uri - File object or stream or URI
* @param {string} [name] - file name
* @param {string} [contentType] - mime-type
* @param {function} [onUploadProgress] - browser only, Function that is called with upload progress
* @return {Promise<FileUploadAPIResponse>}
*/
upload(
uri: string | File | NodeJS.ReadStream,
uri: string | File | Buffer | NodeJS.ReadStream,
name?: string,
contentType?: string,
onUploadProgress?: OnUploadProgress,
Expand Down
45 changes: 33 additions & 12 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import FormData from 'form-data';
import FormData, { AppendOptions } from 'form-data';

import { FeedError } from './errors';

Expand Down Expand Up @@ -31,8 +31,27 @@ function rfc3986(str: string) {
return str.replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
}

function isReadableStream(obj: NodeJS.ReadStream): obj is NodeJS.ReadStream {
return obj !== null && typeof obj === 'object' && typeof (obj as NodeJS.ReadStream)._read === 'function';
function isReadableStream(obj: unknown): obj is NodeJS.ReadStream {
return (
obj !== null &&
typeof obj === 'object' &&
((obj as NodeJS.ReadStream).readable || typeof (obj as NodeJS.ReadStream)._read === 'function')
);
}

function isBuffer(obj: unknown): obj is Buffer {
return (
obj != null &&
(obj as Buffer).constructor != null &&
// @ts-expect-error
typeof obj.constructor.isBuffer === 'function' &&
// @ts-expect-error
obj.constructor.isBuffer(obj)
);
}

function isFileWebAPI(uri: unknown): uri is File {
return typeof window !== 'undefined' && 'File' in window && uri instanceof File;
}

/*
Expand All @@ -50,21 +69,23 @@ function validateFeedId(feedId: string) {
return feedId;
}

function addFileToFormData(uri: string | File | NodeJS.ReadStream, name?: string, contentType?: string) {
function addFileToFormData(uri: string | File | Buffer | NodeJS.ReadStream, name?: string, contentType?: string) {
const data = new FormData();

let fileField: File | NodeJS.ReadStream | { name: string; uri: string; type?: string };
const appendOptions: AppendOptions = {};
if (name) appendOptions.filename = name;
if (contentType) appendOptions.contentType = contentType;

if (isReadableStream(uri as NodeJS.ReadStream)) {
fileField = uri as NodeJS.ReadStream;
} else if (uri && uri.toString && uri.toString() === '[object File]') {
fileField = uri as File;
if (isReadableStream(uri) || isBuffer(uri) || isFileWebAPI(uri)) {
data.append('file', uri, appendOptions);
} else {
fileField = { uri: uri as string, name: name || (uri as string).split('/').reverse()[0] };
if (contentType != null) fileField.type = contentType;
data.append('file', {
uri,
name: name || (uri as string).split('/').reverse()[0],
contentType: contentType || undefined,
});
}

data.append('file', fileField);
return data;
}

Expand Down
16 changes: 16 additions & 0 deletions test/integration/cloud/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ describe('Files', () => {
});
});

describe('When alice adds a different type of file stream', () => {
ctx.requestShouldNotError(async () => {
const file = request('http://nodejs.org/images/logo.png');
ctx.response = await ctx.alice.files.upload(file);
ctx.response.should.not.be.empty;
});
});

describe('When alice adds a buffer as a file', () => {
ctx.requestShouldNotError(async () => {
const file = Buffer.from('some string', 'binary');
ctx.response = await ctx.alice.files.upload(file, 'x.txt', 'text/plain');
ctx.response.should.not.be.empty;
});
});

describe('When alice adds a big file', () => {
ctx.requestShouldError(413, async () => {
const file = fs.createReadStream('./test/integration/cloud/toolarge2.jpeg');
Expand Down