edited-imagekit-uppy-plugin
Version:
A plugin for Uppy, which allows you to upload files directly to ImageKit.io media library.
451 lines (401 loc) • 12 kB
JavaScript
import { Plugin } from '@uppy/core';
import settle from '@uppy/utils/lib/settle';
import { Provider, RequestClient, Socket } from '@uppy/companion-client';
import emitSocketProgress from '@uppy/utils/lib/emitSocketProgress';
import getSocketHost from '@uppy/utils/lib/getSocketHost';
import ProgressTimeout from '@uppy/utils/lib/ProgressTimeout';
import EventTracker from '@uppy/utils/lib/EventTracker';
import NetworkError from '@uppy/utils/lib/NetworkError';
import RateLimitedQueue from '@uppy/utils/lib/RateLimitedQueue';
import isNetworkError from '@uppy/utils/lib/isNetworkError';
function buildResponseError(error, xhr) {
// No error message
if (!error) error = new Error('Upload error');
// Got an error message string
if (typeof error === 'string') error = new Error(error);
// Got something else
if (!(error instanceof Error)) {
error = Object.assign(new Error('Upload error'), { data: error });
}
if (xhr) {
if (isNetworkError(xhr)) {
error = new NetworkError(error, xhr);
return error;
}
error.request = xhr;
}
return error;
}
class ImageKitUppyPlugin extends Plugin {
constructor(uppy, opts) {
super(uppy, opts);
this.id = opts.id || 'ImageKit';
this.type = 'uploader';
this.title = 'ImageKit.io';
this.handleUpload = this.handleUpload.bind(this);
this.uploadEndpoint =
opts.uploadEndpoint || 'https://upload.imagekit.io/api/v1/files/upload';
if (!opts.authenticationEndpoint) {
throw new Error('authenticationEndpoint is missing');
}
if (!opts.publicKey) {
throw new Error('publicKey is missing');
}
// Simultaneous upload limiting is shared across all uploads with this plugin.
// __queue is for internal Uppy use only!
if (this.opts.__queue instanceof RateLimitedQueue) {
this.requests = this.opts.__queue;
} else {
this.requests = new RateLimitedQueue(this.opts.limit);
}
const defaultOptions = {
timeout: 30 * 1000,
};
this.opts = { ...defaultOptions, ...opts };
this.uploaderEvents = Object.create(null);
}
onFileRemove(fileID, cb) {
this.uploaderEvents[fileID].on('file-removed', (file) => {
if (fileID === file.id) cb(file.id);
});
}
onRetry(fileID, cb) {
this.uploaderEvents[fileID].on('upload-retry', (targetFileID) => {
if (fileID === targetFileID) {
cb();
}
});
}
onRetryAll(fileID, cb) {
this.uploaderEvents[fileID].on('retry-all', (filesToRetry) => {
if (!this.uppy.getFile(fileID)) return;
cb();
});
}
onCancelAll(fileID, cb) {
this.uploaderEvents[fileID].on('cancel-all', () => {
if (!this.uppy.getFile(fileID)) return;
cb();
});
}
handleUpload(fileIDs) {
if (fileIDs.length === 0) {
this.uppy.log('[ImageKit] No files to upload!');
return Promise.resolve();
}
this.uppy.log('[ImageKit] Uploading...');
const files = fileIDs.map((fileID) => this.uppy.getFile(fileID));
return this.uploadFiles(files).then(() => null);
}
uploadFiles(files) {
const promises = files.map((file, i) => {
const current = parseInt(i, 10) + 1;
const total = files.length;
if (file.error) {
return Promise.reject(new Error(file.error));
} else {
return new Promise((resolve, reject) => {
const queuedRequest = this.requests.run(() => {
this.upload(file, current, total)
.then((res) => {
queuedRequest.done();
resolve(res);
})
.catch((err) => {
queuedRequest.done();
reject(err);
});
});
});
}
});
return settle(promises);
}
_generateSignatureToken() {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.timeout = 60000;
var url = this.opts.authenticationEndpoint;
if (url.indexOf('?') === -1) {
url += `?t=${Math.random().toString()}`;
} else {
url += `&t=${Math.random().toString()}`;
}
xhr.open('GET', url);
xhr.ontimeout = function (e) {
reject([
'The authenticationEndpoint you provided timed out in 60 seconds',
xhr,
]);
};
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
try {
var body = JSON.parse(xhr.responseText);
var obj = {
signature: body.signature,
expire: body.expire,
token: body.token,
};
resolve(obj);
} catch (ex) {
reject([ex, xhr]);
}
} else {
try {
var error = JSON.parse(xhr.responseText);
reject([error, xhr]);
} catch (ex) {
reject([ex, xhr]);
}
}
});
xhr.send();
});
}
_uploadDirectly(formData, file, timeout) {
return new Promise((resolve, reject) => {
var uploadFileXHR = new XMLHttpRequest();
this.uppy.emit('upload-started', file);
this.uploaderEvents[file.id] = new EventTracker(this.uppy);
const timer = new ProgressTimeout(timeout, () => {
uploadFileXHR.abort();
// queuedRequest.done()
const error = new Error('timedOut', {
seconds: Math.ceil(timeout / 1000),
});
reject([error, uploadFileXHR]);
});
const id = file.id || '';
uploadFileXHR.upload.addEventListener('loadstart', (ev) => {
this.uppy.log(`[ImageKit] ${id} started`);
});
uploadFileXHR.upload.addEventListener('progress', (ev) => {
this.uppy.log(`[ImageKit] ${id} progress: ${ev.loaded} / ${ev.total}`);
// Begin checking for timeouts when progress starts, instead of loading,
// to avoid timing out requests on browser concurrency queue
timer.progress();
if (ev.lengthComputable) {
this.uppy.emit('upload-progress', file, {
uploader: this,
bytesUploaded: ev.loaded,
bytesTotal: ev.total,
});
}
});
uploadFileXHR.addEventListener('error', (ev) => {
this.uppy.log(`[ImageKit] ${id} errored`);
timer.done();
// queuedRequest.done()
if (this.uploaderEvents[file.id]) {
this.uploaderEvents[file.id].remove();
this.uploaderEvents[file.id] = null;
}
return reject([new Error('Upload error'), uploadFileXHR]);
});
uploadFileXHR.open('POST', this.uploadEndpoint);
uploadFileXHR.addEventListener('load', () => {
this.uppy.log(`[ImageKit] ${id} finished`);
timer.done();
// queuedRequest.done()
if (this.uploaderEvents[file.id]) {
this.uploaderEvents[file.id].remove();
this.uploaderEvents[file.id] = null;
}
if (uploadFileXHR.status === 200) {
try {
var uploadResponse = JSON.parse(uploadFileXHR.responseText);
resolve(uploadResponse);
} catch (ex) {
reject([ex, uploadFileXHR]);
}
} else if (uploadFileXHR.status !== 200) {
try {
var error = JSON.parse(uploadFileXHR.responseText);
if (error.message) reject([error.message, uploadFileXHR]);
else reject([error, uploadFileXHR]);
} catch (ex) {
reject([ex, uploadFileXHR]);
}
}
});
uploadFileXHR.send(formData);
});
}
_uploadRemote(formData, file, timeout) {
var metadata = {};
for (var pair of formData.entries()) {
// exclude file
if (pair[0] != 'file') {
metadata[pair[0]] = pair[1];
}
}
delete formData.file;
return new Promise((resolve, reject) => {
this.uppy.emit('upload-started', file);
const Client = file.remote.providerOptions.provider
? Provider
: RequestClient;
const client = new Client(this.uppy, file.remote.providerOptions);
client
.post(file.remote.url, {
...file.remote.body,
endpoint: this.uploadEndpoint,
size: file.data.size,
httpMethod: 'POST',
metadata: metadata,
useFormData: formData,
fieldname: 'file',
})
.then((res) => {
const token = res.token;
const host = getSocketHost(file.remote.companionUrl);
const socket = new Socket({
target: `${host}/api/${token}`,
autoOpen: false,
});
this.uploaderEvents[file.id] = new EventTracker(this.uppy);
this.onFileRemove(file.id, () => {
socket.send('pause', {});
queuedRequest.abort();
resolve(`upload ${file.id} was removed`);
});
this.onCancelAll(file.id, () => {
socket.send('pause', {});
queuedRequest.abort();
resolve(`upload ${file.id} was canceled`);
});
this.onRetry(file.id, () => {
socket.send('pause', {});
socket.send('resume', {});
});
this.onRetryAll(file.id, () => {
socket.send('pause', {});
socket.send('resume', {});
});
socket.on('progress', (progressData) =>
emitSocketProgress(this, progressData, file)
);
socket.on('success', (data) => {
var uploadResponse = JSON.parse(data.response.responseText);
queuedRequest.done();
if (this.uploaderEvents[file.id]) {
this.uploaderEvents[file.id].remove();
this.uploaderEvents[file.id] = null;
}
return resolve(uploadResponse);
});
socket.on('error', (errData) => {
queuedRequest.done();
if (this.uploaderEvents[file.id]) {
this.uploaderEvents[file.id].remove();
this.uploaderEvents[file.id] = null;
}
try {
var error = JSON.parse(errData.responseText);
if (error.message) {
return reject([error.message]);
} else {
return reject([error]);
}
} catch (ex) {
return reject([ex, errData]);
}
});
const queuedRequest = this.requests.run(() => {
socket.open();
if (file.isPaused) {
socket.send('pause', {});
}
return () => socket.close();
});
})
.catch((err) => {
reject([err]);
});
});
}
upload(file, current, total) {
return new Promise((resolve, reject) => {
this.uppy.log(`uploading ${current} of ${total}`);
var formData = new FormData();
const metaFields = Object.keys(file.meta);
metaFields.map((key) => {
/* if (key === 'name') {
formData.append('fileName', file.meta.name.toString());
return;
} */
if (
this.opts.metaFields &&
this.opts.metaFields.length &&
this.opts.metaFields.indexOf(key) == -1
) {
return;
}
var value = file.meta[key];
if (value !== null && typeof value !== 'undefined') {
// We need to pass false values as string
if (
['tags', 'responseFields'].indexOf(key) !== -1 &&
Array.isArray(value)
) {
value = value.join(',');
}
formData.append(key, value.toString()); // Always pass value as string
}
});
if (!formData.get('fileName') || !formData.get('fileName').trim()) {
formData.set('fileName', file.name);
}
formData.append('publicKey', this.opts.publicKey);
formData.append('file', file.data);
if (this.opts.createdBy) {
if (
!this.opts.createdBy.userId ||
!this.opts.createdBy.userId.trim().length <= 0
) {
this.uppy.log(`Missing or invalid userId`);
this.uppy.emit(
'upload-error',
file,
buildResponseError(`Missing or invalid userId`)
);
return reject(file);
}
formData.append('createdBy', JSON.stringify(this.opts.createdBy));
}
this._generateSignatureToken()
.then(({ signature, token, expire }) => {
formData.append('signature', signature);
formData.append('expire', expire);
formData.append('token', token);
if (file.remote) {
return this._uploadRemote(formData, file, this.opts.timeout);
} else {
return this._uploadDirectly(formData, file, this.opts.timeout);
}
})
.then((uploadResponse) => {
this.uppy.emit('upload-success', file, {
status: 200,
body: uploadResponse,
uploadURL: uploadResponse.url,
});
return resolve(file);
})
.catch((data) => {
var error = data[0];
var xhr = data[1];
this.uppy.emit('upload-error', file, buildResponseError(error, xhr));
reject(file);
});
});
}
install() {
this.uppy.addUploader(this.handleUpload);
}
uninstall() {
this.uppy.removeUploader(this.handleUpload);
}
}
export default ImageKitUppyPlugin;