New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@uppy/core

Package Overview
Dependencies
Maintainers
6
Versions
147
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@uppy/core - npm Package Compare versions

Comparing version 3.1.2 to 3.2.0

8

CHANGELOG.md
# @uppy/core
## 3.2.0
Released: 2023-04-18
Included in: Uppy v3.8.0
- @uppy/core: improve performance of validating & uploading files (Mikael Finstad / #4402)
- @uppy/core,@uppy/locales,@uppy/provider-views: User feedback adding recursive folders take 2 (Mikael Finstad / #4399)
## 3.1.2

@@ -4,0 +12,0 @@

4

lib/locale.js

@@ -50,2 +50,3 @@ export default {

emptyFolderAdded: 'No files were added from empty folder',
addedNumFiles: 'Added %{numFiles} file(s)',
folderAlreadyAdded: 'The folder "%{folder}" was already added',

@@ -55,4 +56,5 @@ folderAdded: {

1: 'Added %{smart_count} files from %{folder}'
}
},
additionalRestrictionsFailed: '%{count} additional restrictions were not fulfilled'
}
};

@@ -15,5 +15,11 @@ /* eslint-disable max-classes-per-file, class-methods-use-this */

class RestrictionError extends Error {
constructor() {
super(...arguments);
constructor(message, _temp) {
let {
isUserFacing = true,
file
} = _temp === void 0 ? {} : _temp;
super(message);
this.isRestriction = true;
this.isUserFacing = isUserFacing;
if (file != null) this.file = file; // only some restriction errors are related to a particular file
}

@@ -36,17 +42,15 @@

};
}
} // Because these operations are slow, we cannot run them for every file (if we are adding multiple files)
validate(file, files) {
validateAggregateRestrictions(existingFiles, addingFiles) {
const {
maxFileSize,
minFileSize,
maxTotalFileSize,
maxNumberOfFiles,
allowedFileTypes
maxNumberOfFiles
} = this.getOpts().restrictions;
if (maxNumberOfFiles) {
const nonGhostFiles = files.filter(f => !f.isGhost);
const nonGhostFiles = existingFiles.filter(f => !f.isGhost);
if (nonGhostFiles.length + 1 > maxNumberOfFiles) {
if (nonGhostFiles.length + addingFiles.length > maxNumberOfFiles) {
throw new RestrictionError(`${this.i18n('youCanOnlyUploadX', {

@@ -58,2 +62,28 @@ smart_count: maxNumberOfFiles

if (maxTotalFileSize) {
let totalFilesSize = existingFiles.reduce((total, f) => total + f.size, 0);
for (const addingFile of addingFiles) {
if (addingFile.size != null) {
// We can't check maxTotalFileSize if the size is unknown.
totalFilesSize += addingFile.size;
if (totalFilesSize > maxTotalFileSize) {
throw new RestrictionError(this.i18n('exceedsSize', {
size: prettierBytes(maxTotalFileSize),
file: addingFile.name
}));
}
}
}
}
}
validateSingleFile(file) {
const {
maxFileSize,
minFileSize,
allowedFileTypes
} = this.getOpts().restrictions;
if (allowedFileTypes) {

@@ -79,16 +109,6 @@ const isCorrectFileType = allowedFileTypes.some(type => {

types: allowedFileTypesString
}));
}), {
file
});
}
} // We can't check maxTotalFileSize if the size is unknown.
if (maxTotalFileSize && file.size != null) {
const totalFilesSize = files.reduce((total, f) => total + f.size, file.size);
if (totalFilesSize > maxTotalFileSize) {
throw new RestrictionError(this.i18n('exceedsSize', {
size: prettierBytes(maxTotalFileSize),
file: file.name
}));
}
} // We can't check maxFileSize if the size is unknown.

@@ -101,3 +121,5 @@

file: file.name
}));
}), {
file
});
} // We can't check minFileSize if the size is unknown.

@@ -109,6 +131,15 @@

size: prettierBytes(minFileSize)
}));
}), {
file
});
}
}
validate(existingFiles, addingFiles) {
addingFiles.forEach(addingFile => {
this.validateSingleFile(addingFile);
});
this.validateAggregateRestrictions(existingFiles, addingFiles);
}
validateMinNumberOfFiles(files) {

@@ -115,0 +146,0 @@ const {

@@ -25,3 +25,3 @@ let _Symbol$for, _Symbol$for2;

const packageJson = {
"version": "3.1.2"
"version": "3.2.0"
};

@@ -57,6 +57,8 @@ import locale from './locale.js';

var _checkAndCreateFileStateObject = /*#__PURE__*/_classPrivateFieldLooseKey("checkAndCreateFileStateObject");
var _transformFile = /*#__PURE__*/_classPrivateFieldLooseKey("transformFile");
var _startIfAutoProceed = /*#__PURE__*/_classPrivateFieldLooseKey("startIfAutoProceed");
var _checkAndUpdateFileState = /*#__PURE__*/_classPrivateFieldLooseKey("checkAndUpdateFileState");
var _addListeners = /*#__PURE__*/_classPrivateFieldLooseKey("addListeners");

@@ -101,7 +103,10 @@

});
Object.defineProperty(this, _checkAndUpdateFileState, {
value: _checkAndUpdateFileState2
});
Object.defineProperty(this, _startIfAutoProceed, {
value: _startIfAutoProceed2
});
Object.defineProperty(this, _checkAndCreateFileStateObject, {
value: _checkAndCreateFileStateObject2
Object.defineProperty(this, _transformFile, {
value: _transformFile2
});

@@ -282,2 +287,16 @@ Object.defineProperty(this, _assertNewUploadAllowed, {

}
patchFilesState(filesWithNewState) {
const existingFilesState = this.getState().files;
this.setState({
files: { ...existingFilesState,
...Object.fromEntries(Object.entries(filesWithNewState).map(_ref => {
let [fileID, newFileState] = _ref;
return [fileID, { ...existingFilesState[fileID],
...newFileState
}];
}))
}
});
}
/**

@@ -293,8 +312,4 @@ * Shorthand to set state for a specific file.

this.setState({
files: { ...this.getState().files,
[fileID]: { ...this.getState().files[fileID],
...state
}
}
this.patchFilesState({
[fileID]: state
});

@@ -447,2 +462,6 @@ }

getFilesByIds(ids) {
return ids.map(id => this.getFile(id));
}
getObjectOfFilesPerState() {

@@ -455,6 +474,6 @@ const {

const files = Object.values(filesObject);
const inProgressFiles = files.filter(_ref => {
const inProgressFiles = files.filter(_ref2 => {
let {
progress
} = _ref;
} = _ref2;
return !progress.uploadComplete && progress.uploadStarted;

@@ -490,3 +509,3 @@ });

* @constructs
* @param { Error } error
* @param { Error[] } errors
* @param { undefined } file

@@ -498,3 +517,2 @@ */

* @param { RestrictionError } error
* @param { UppyFile | undefined } file
*/

@@ -509,3 +527,3 @@

try {
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validate(file, files);
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validate(files, [file]);
} catch (err) {

@@ -531,7 +549,2 @@ return err;

* Create a file state object based on user-provided `addFile()` options.
*
* Note this is extremely side-effectful and should only be done when a file state object
* will be added to state immediately afterward!
*
* The `files` value is passed in because it may be updated by the caller without updating the store.
*/

@@ -552,29 +565,23 @@

const {
files
} = this.getState();
nextFilesState,
validFilesToAdd,
errors
} = _classPrivateFieldLooseBase(this, _checkAndUpdateFileState)[_checkAndUpdateFileState]([file]);
let newFile = _classPrivateFieldLooseBase(this, _checkAndCreateFileStateObject)[_checkAndCreateFileStateObject](files, file); // Users are asked to re-select recovered files without data,
// and to keep the progress, meta and everthing else, we only replace said data
const restrictionErrors = errors.filter(error => error.isRestriction);
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](restrictionErrors);
if (files[newFile.id] && files[newFile.id].isGhost) {
newFile = { ...files[newFile.id],
data: file.data,
isGhost: false
};
this.log(`Replaced the blob in the restored ghost file: ${newFile.name}, ${newFile.id}`);
}
if (errors.length > 0) throw errors[0];
this.setState({
files: { ...files,
[newFile.id]: newFile
}
files: nextFilesState
});
this.emit('file-added', newFile);
this.emit('files-added', [newFile]);
this.log(`Added file: ${newFile.name}, ${newFile.id}, mime type: ${newFile.type}`);
const [firstValidFileToAdd] = validFilesToAdd;
this.emit('file-added', firstValidFileToAdd);
this.emit('files-added', validFilesToAdd);
this.log(`Added file: ${firstValidFileToAdd.name}, ${firstValidFileToAdd.id}, mime type: ${firstValidFileToAdd.type}`);
_classPrivateFieldLooseBase(this, _startIfAutoProceed)[_startIfAutoProceed]();
return newFile.id;
return firstValidFileToAdd.id;
}

@@ -591,56 +598,19 @@ /**

addFiles(fileDescriptors) {
_classPrivateFieldLooseBase(this, _assertNewUploadAllowed)[_assertNewUploadAllowed](); // create a copy of the files object only once
_classPrivateFieldLooseBase(this, _assertNewUploadAllowed)[_assertNewUploadAllowed]();
const {
nextFilesState,
validFilesToAdd,
errors
} = _classPrivateFieldLooseBase(this, _checkAndUpdateFileState)[_checkAndUpdateFileState](fileDescriptors);
const files = { ...this.getState().files
};
const newFiles = [];
const errors = [];
const restrictionErrors = errors.filter(error => error.isRestriction);
for (let i = 0; i < fileDescriptors.length; i++) {
try {
let newFile = _classPrivateFieldLooseBase(this, _checkAndCreateFileStateObject)[_checkAndCreateFileStateObject](files, fileDescriptors[i]); // Users are asked to re-select recovered files without data,
// and to keep the progress, meta and everthing else, we only replace said data
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](restrictionErrors);
const nonRestrictionErrors = errors.filter(error => !error.isRestriction);
if (files[newFile.id] && files[newFile.id].isGhost) {
newFile = { ...files[newFile.id],
data: fileDescriptors[i].data,
isGhost: false
};
this.log(`Replaced blob in a ghost file: ${newFile.name}, ${newFile.id}`);
}
files[newFile.id] = newFile;
newFiles.push(newFile);
} catch (err) {
if (!err.isRestriction) {
errors.push(err);
}
}
}
this.setState({
files
});
newFiles.forEach(newFile => {
this.emit('file-added', newFile);
});
this.emit('files-added', newFiles);
if (newFiles.length > 5) {
this.log(`Added batch of ${newFiles.length} files`);
} else {
Object.keys(newFiles).forEach(fileID => {
this.log(`Added file: ${newFiles[fileID].name}\n id: ${newFiles[fileID].id}\n type: ${newFiles[fileID].type}`);
});
}
if (newFiles.length > 0) {
_classPrivateFieldLooseBase(this, _startIfAutoProceed)[_startIfAutoProceed]();
}
if (errors.length > 0) {
if (nonRestrictionErrors.length > 0) {
let message = 'Multiple errors occurred while adding files:\n';
errors.forEach(subError => {
nonRestrictionErrors.forEach(subError => {
message += `\n * ${subError.message}`;

@@ -650,3 +620,3 @@ });

message: this.i18n('addBulkFilesFailed', {
smart_count: errors.length
smart_count: nonRestrictionErrors.length
}),

@@ -657,9 +627,30 @@ details: message

if (typeof AggregateError === 'function') {
throw new AggregateError(errors, message);
throw new AggregateError(nonRestrictionErrors, message);
} else {
const err = new Error(message);
err.errors = errors;
err.errors = nonRestrictionErrors;
throw err;
}
} // OK, we haven't thrown an error, we can start updating state and emitting events now:
this.setState({
files: nextFilesState
});
validFilesToAdd.forEach(file => {
this.emit('file-added', file);
});
this.emit('files-added', validFilesToAdd);
if (validFilesToAdd.length > 5) {
this.log(`Added batch of ${validFilesToAdd.length} files`);
} else {
Object.values(validFilesToAdd).forEach(file => {
this.log(`Added file: ${file.name}\n id: ${file.id}\n type: ${file.type}`);
});
}
if (validFilesToAdd.length > 0) {
_classPrivateFieldLooseBase(this, _startIfAutoProceed)[_startIfAutoProceed]();
}
}

@@ -1273,3 +1264,3 @@

return Promise.resolve().then(() => _classPrivateFieldLooseBase(this, _restricter)[_restricter].validateMinNumberOfFiles(files)).catch(err => {
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](err);
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit]([err]);

@@ -1313,19 +1304,41 @@ throw err;

function _informAndEmit2(error, file) {
const {
message,
details = ''
} = error;
function _informAndEmit2(errors) {
for (const error of errors) {
const {
file,
isRestriction
} = error;
if (error.isRestriction) {
this.emit('restriction-failed', file, error);
} else {
this.emit('error', error);
if (isRestriction) {
this.emit('restriction-failed', file, error);
} else {
this.emit('error', error);
}
this.log(error, 'warning');
}
this.info({
message,
details
}, 'error', this.opts.infoTimeout);
this.log(error, 'warning');
const userFacingErrors = errors.filter(error => error.isUserFacing); // don't flood the user: only show the first 4 toasts
const maxNumToShow = 4;
const firstErrors = userFacingErrors.slice(0, maxNumToShow);
const additionalErrors = userFacingErrors.slice(maxNumToShow);
firstErrors.forEach(_ref3 => {
let {
message,
details = ''
} = _ref3;
this.info({
message,
details
}, 'error', this.opts.infoTimeout);
});
if (additionalErrors.length > 0) {
this.info({
message: this.i18n('additionalRestrictionsFailed', {
count: additionalErrors.length
})
});
}
}

@@ -1369,5 +1382,7 @@

if (allowNewUpload === false) {
const error = new RestrictionError(this.i18n('noMoreFilesAllowed'));
const error = new RestrictionError(this.i18n('noMoreFilesAllowed'), {
file
});
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](error, file);
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit]([error]);

@@ -1378,16 +1393,12 @@ throw error;

function _checkAndCreateFileStateObject2(files, fileDescriptor) {
function _transformFile2(fileDescriptorOrFile) {
// Uppy expects files in { name, type, size, data } format.
// If the actual File object is passed from input[type=file] or drag-drop,
// we normalize it to match Uppy file object
if (fileDescriptor instanceof File) {
// eslint-disable-next-line no-param-reassign
fileDescriptor = {
name: fileDescriptor.name,
type: fileDescriptor.type,
size: fileDescriptor.size,
data: fileDescriptor
};
}
const fileDescriptor = fileDescriptorOrFile instanceof File ? {
name: fileDescriptorOrFile.name,
type: fileDescriptorOrFile.type,
size: fileDescriptorOrFile.size,
data: fileDescriptorOrFile
} : fileDescriptorOrFile;
const fileType = getFileType(fileDescriptor);

@@ -1398,13 +1409,2 @@ const fileName = getFileName(fileType, fileDescriptor);

const id = getSafeFileId(fileDescriptor);
if (this.checkIfFileAlreadyExists(id)) {
const error = new RestrictionError(this.i18n('noDuplicates', {
fileName
}));
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](error, fileDescriptor);
throw error;
}
const meta = fileDescriptor.meta || {};

@@ -1415,3 +1415,3 @@ meta.name = fileName;

const size = Number.isFinite(fileDescriptor.data.size) ? fileDescriptor.data.size : null;
let newFile = {
return {
source: fileDescriptor.source || '',

@@ -1438,24 +1438,2 @@ id,

};
const onBeforeFileAddedResult = this.opts.onBeforeFileAdded(newFile, files);
if (onBeforeFileAddedResult === false) {
// Don’t show UI info for this error, as it should be done by the developer
const error = new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.');
this.emit('restriction-failed', fileDescriptor, error);
throw error;
} else if (typeof onBeforeFileAddedResult === 'object' && onBeforeFileAddedResult !== null) {
newFile = onBeforeFileAddedResult;
}
try {
const filesArray = Object.keys(files).map(i => files[i]);
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validate(newFile, filesArray);
} catch (err) {
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](err, newFile);
throw err;
}
return newFile;
}

@@ -1476,2 +1454,84 @@

function _checkAndUpdateFileState2(filesToAdd) {
const {
files: existingFiles
} = this.getState(); // create a copy of the files object only once
const nextFilesState = { ...existingFiles
};
const validFilesToAdd = [];
const errors = [];
for (const fileToAdd of filesToAdd) {
try {
var _existingFiles$newFil;
let newFile = _classPrivateFieldLooseBase(this, _transformFile)[_transformFile](fileToAdd); // If a file has been recovered (Golden Retriever), but we were unable to recover its data (probably too large),
// users are asked to re-select these half-recovered files and then this method will be called again.
// In order to keep the progress, meta and everthing else, we keep the existing file,
// but we replace `data`, and we remove `isGhost`, because the file is no longer a ghost now
if ((_existingFiles$newFil = existingFiles[newFile.id]) != null && _existingFiles$newFil.isGhost) {
const {
isGhost,
...existingFileState
} = existingFiles[newFile.id];
newFile = { ...existingFileState,
data: fileToAdd.data
};
this.log(`Replaced the blob in the restored ghost file: ${newFile.name}, ${newFile.id}`);
}
if (this.checkIfFileAlreadyExists(newFile.id)) {
throw new RestrictionError(this.i18n('noDuplicates', {
fileName: newFile.name
}), {
file: fileToAdd
});
}
const onBeforeFileAddedResult = this.opts.onBeforeFileAdded(newFile, nextFilesState);
if (onBeforeFileAddedResult === false) {
// Don’t show UI info for this error, as it should be done by the developer
throw new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.', {
isUserFacing: false,
file: fileToAdd
});
} else if (typeof onBeforeFileAddedResult === 'object' && onBeforeFileAddedResult !== null) {
newFile = onBeforeFileAddedResult;
}
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validateSingleFile(newFile); // need to add it to the new local state immediately, so we can use the state to validate the next files too
nextFilesState[newFile.id] = newFile;
validFilesToAdd.push(newFile);
} catch (err) {
errors.push(err);
}
}
try {
// need to run this separately because it's much more slow, so if we run it inside the for-loop it will be very slow
// when many files are added
_classPrivateFieldLooseBase(this, _restricter)[_restricter].validateAggregateRestrictions(Object.values(existingFiles), validFilesToAdd);
} catch (err) {
errors.push(err); // If we have any aggregate error, don't allow adding this batch
return {
nextFilesState: existingFiles,
validFilesToAdd: [],
errors
};
}
return {
nextFilesState,
validFilesToAdd,
errors
};
}
function _addListeners2() {

@@ -1508,2 +1568,4 @@ /**

const newError = new Error(error.message);
newError.isUserFacing = true; // todo maybe don't do this with all errors?
newError.details = error.message;

@@ -1519,5 +1581,5 @@

_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](newError);
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit]([newError]);
} else {
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit](error);
_classPrivateFieldLooseBase(this, _informAndEmit)[_informAndEmit]([error]);
}

@@ -1549,9 +1611,10 @@ });

});
this.on('upload-started', file => {
if (file == null || !this.getFile(file.id)) {
this.log(`Not setting progress for a file that has been removed: ${file == null ? void 0 : file.id}`);
return;
}
this.setFileState(file.id, {
const onUploadStarted = files => {
const filesFiltered = files.filter(file => {
const exists = file != null && this.getFile(file.id);
if (!exists) this.log(`Not setting progress for a file that has been removed: ${file == null ? void 0 : file.id}`);
return exists;
});
const filesState = Object.fromEntries(filesFiltered.map(file => [file.id, {
progress: {

@@ -1564,3 +1627,12 @@ uploadStarted: Date.now(),

}
}]));
this.patchFilesState(filesState);
};
this.on('upload-start', files => {
files.forEach(file => {
// todo backward compat, remove this event in a next major
this.emit('upload-started', file);
});
onUploadStarted(files);
});

@@ -1726,11 +1798,14 @@ this.on('upload-progress', this.calculateProgress);

async function _runUpload2(uploadID) {
let {
currentUploads
} = this.getState();
let currentUpload = currentUploads[uploadID];
const restoreStep = currentUpload.step || 0;
const getCurrentUpload = () => {
const {
currentUploads
} = this.getState();
return currentUploads[uploadID];
};
let currentUpload = getCurrentUpload();
const steps = [..._classPrivateFieldLooseBase(this, _preProcessors)[_preProcessors], ..._classPrivateFieldLooseBase(this, _uploaders)[_uploaders], ..._classPrivateFieldLooseBase(this, _postProcessors)[_postProcessors]];
try {
for (let step = restoreStep; step < steps.length; step++) {
for (let step = currentUpload.step || 0; step < steps.length; step++) {
if (!currentUpload) {

@@ -1741,16 +1816,17 @@ break;

const fn = steps[step];
const updatedUpload = { ...currentUpload,
step
};
this.setState({
currentUploads: { ...currentUploads,
[uploadID]: updatedUpload
currentUploads: { ...this.getState().currentUploads,
[uploadID]: { ...currentUpload,
step
}
}
}); // TODO give this the `updatedUpload` object as its only parameter maybe?
});
const {
fileIDs
} = currentUpload; // TODO give this the `updatedUpload` object as its only parameter maybe?
// Otherwise when more metadata may be added to the upload this would keep getting more parameters
await fn(updatedUpload.fileIDs, uploadID); // Update currentUpload value in case it was modified asynchronously.
await fn(fileIDs, uploadID); // Update currentUpload value in case it was modified asynchronously.
currentUploads = this.getState().currentUploads;
currentUpload = currentUploads[uploadID];
currentUpload = getCurrentUpload();
}

@@ -1791,4 +1867,3 @@ } catch (err) {

currentUploads = this.getState().currentUploads;
currentUpload = currentUploads[uploadID];
currentUpload = getCurrentUpload();
} // Emit completion events.

@@ -1795,0 +1870,0 @@ // This is in a separate function so that the `currentUploads` variable

{
"name": "@uppy/core",
"description": "Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
"version": "3.1.2",
"version": "3.2.0",
"license": "MIT",

@@ -25,4 +25,4 @@ "main": "lib/index.js",

"@transloadit/prettier-bytes": "0.0.9",
"@uppy/store-default": "^3.0.2",
"@uppy/utils": "^5.2.0",
"@uppy/store-default": "^3.0.3",
"@uppy/utils": "^5.3.0",
"lodash.throttle": "^4.1.1",

@@ -29,0 +29,0 @@ "mime-match": "^1.0.2",

# @uppy/core
<img src="https://uppy.io/images/logos/uppy-dog-head-arrow.svg" width="120" alt="Uppy logo: a superman puppy in a pink suit" align="right">
<img src="https://uppy.io/img/logo.svg" width="120" alt="Uppy logo: a smiling puppy above a pink upwards arrow" align="right">

@@ -5,0 +5,0 @@ [![npm version](https://img.shields.io/npm/v/@uppy/core.svg?style=flat-square)](https://www.npmjs.com/package/@uppy/core)

@@ -54,2 +54,3 @@ export default {

emptyFolderAdded: 'No files were added from empty folder',
addedNumFiles: 'Added %{numFiles} file(s)',
folderAlreadyAdded: 'The folder "%{folder}" was already added',

@@ -60,3 +61,4 @@ folderAdded: {

},
additionalRestrictionsFailed: '%{count} additional restrictions were not fulfilled',
},
}

@@ -16,2 +16,8 @@ /* eslint-disable max-classes-per-file, class-methods-use-this */

class RestrictionError extends Error {
constructor (message, { isUserFacing = true, file } = {}) {
super(message)
this.isUserFacing = isUserFacing
if (file != null) this.file = file // only some restriction errors are related to a particular file
}
isRestriction = true

@@ -34,8 +40,9 @@ }

validate (file, files) {
const { maxFileSize, minFileSize, maxTotalFileSize, maxNumberOfFiles, allowedFileTypes } = this.getOpts().restrictions
// Because these operations are slow, we cannot run them for every file (if we are adding multiple files)
validateAggregateRestrictions (existingFiles, addingFiles) {
const { maxTotalFileSize, maxNumberOfFiles } = this.getOpts().restrictions
if (maxNumberOfFiles) {
const nonGhostFiles = files.filter(f => !f.isGhost)
if (nonGhostFiles.length + 1 > maxNumberOfFiles) {
const nonGhostFiles = existingFiles.filter(f => !f.isGhost)
if (nonGhostFiles.length + addingFiles.length > maxNumberOfFiles) {
throw new RestrictionError(`${this.i18n('youCanOnlyUploadX', { smart_count: maxNumberOfFiles })}`)

@@ -45,2 +52,23 @@ }

if (maxTotalFileSize) {
let totalFilesSize = existingFiles.reduce((total, f) => (total + f.size), 0)
for (const addingFile of addingFiles) {
if (addingFile.size != null) { // We can't check maxTotalFileSize if the size is unknown.
totalFilesSize += addingFile.size
if (totalFilesSize > maxTotalFileSize) {
throw new RestrictionError(this.i18n('exceedsSize', {
size: prettierBytes(maxTotalFileSize),
file: addingFile.name,
}))
}
}
}
}
}
validateSingleFile (file) {
const { maxFileSize, minFileSize, allowedFileTypes } = this.getOpts().restrictions
if (allowedFileTypes) {

@@ -63,18 +91,6 @@ const isCorrectFileType = allowedFileTypes.some((type) => {

const allowedFileTypesString = allowedFileTypes.join(', ')
throw new RestrictionError(this.i18n('youCanOnlyUploadFileTypes', { types: allowedFileTypesString }))
throw new RestrictionError(this.i18n('youCanOnlyUploadFileTypes', { types: allowedFileTypesString }), { file })
}
}
// We can't check maxTotalFileSize if the size is unknown.
if (maxTotalFileSize && file.size != null) {
const totalFilesSize = files.reduce((total, f) => (total + f.size), file.size)
if (totalFilesSize > maxTotalFileSize) {
throw new RestrictionError(this.i18n('exceedsSize', {
size: prettierBytes(maxTotalFileSize),
file: file.name,
}))
}
}
// We can't check maxFileSize if the size is unknown.

@@ -85,3 +101,3 @@ if (maxFileSize && file.size != null && file.size > maxFileSize) {

file: file.name,
}))
}), { file })
}

@@ -93,6 +109,13 @@

size: prettierBytes(minFileSize),
}))
}), { file })
}
}
validate (existingFiles, addingFiles) {
addingFiles.forEach((addingFile) => {
this.validateSingleFile(addingFile)
})
this.validateAggregateRestrictions(existingFiles, addingFiles)
}
validateMinNumberOfFiles (files) {

@@ -99,0 +122,0 @@ const { minNumberOfFiles } = this.getOpts().restrictions

@@ -181,2 +181,19 @@ /* eslint-disable max-classes-per-file */

patchFilesState (filesWithNewState) {
const existingFilesState = this.getState().files
this.setState({
files: {
...existingFilesState,
...Object.fromEntries(Object.entries(filesWithNewState).map(([fileID, newFileState]) => ([
fileID,
{
...existingFilesState[fileID],
...newFileState,
},
]))),
},
})
}
/**

@@ -190,5 +207,3 @@ * Shorthand to set state for a specific file.

this.setState({
files: { ...this.getState().files, [fileID]: { ...this.getState().files[fileID], ...state } },
})
this.patchFilesState({ [fileID]: state })
}

@@ -328,2 +343,6 @@

getFilesByIds (ids) {
return ids.map((id) => this.getFile(id))
}
getObjectOfFilesPerState () {

@@ -368,3 +387,3 @@ const { files: filesObject, totalProgress, error } = this.getState()

* @constructs
* @param { Error } error
* @param { Error[] } errors
* @param { undefined } file

@@ -375,14 +394,28 @@ */

* @param { RestrictionError } error
* @param { UppyFile | undefined } file
*/
#informAndEmit (error, file) {
const { message, details = '' } = error
#informAndEmit (errors) {
for (const error of errors) {
const { file, isRestriction } = error
if (error.isRestriction) {
this.emit('restriction-failed', file, error)
} else {
this.emit('error', error)
if (isRestriction) {
this.emit('restriction-failed', file, error)
} else {
this.emit('error', error)
}
this.log(error, 'warning')
}
this.info({ message, details }, 'error', this.opts.infoTimeout)
this.log(error, 'warning')
const userFacingErrors = errors.filter((error) => error.isUserFacing)
// don't flood the user: only show the first 4 toasts
const maxNumToShow = 4
const firstErrors = userFacingErrors.slice(0, maxNumToShow)
const additionalErrors = userFacingErrors.slice(maxNumToShow)
firstErrors.forEach(({ message, details = '' }) => {
this.info({ message, details }, 'error', this.opts.infoTimeout)
})
if (additionalErrors.length > 0) {
this.info({ message: this.i18n('additionalRestrictionsFailed', { count: additionalErrors.length }) })
}
}

@@ -392,3 +425,3 @@

try {
this.#restricter.validate(file, files)
this.#restricter.validate(files, [file])
} catch (err) {

@@ -426,4 +459,4 @@ return err

if (allowNewUpload === false) {
const error = new RestrictionError(this.i18n('noMoreFilesAllowed'))
this.#informAndEmit(error, file)
const error = new RestrictionError(this.i18n('noMoreFilesAllowed'), { file })
this.#informAndEmit([error])
throw error

@@ -444,21 +477,13 @@ }

* Create a file state object based on user-provided `addFile()` options.
*
* Note this is extremely side-effectful and should only be done when a file state object
* will be added to state immediately afterward!
*
* The `files` value is passed in because it may be updated by the caller without updating the store.
*/
#checkAndCreateFileStateObject (files, fileDescriptor) {
#transformFile (fileDescriptorOrFile) {
// Uppy expects files in { name, type, size, data } format.
// If the actual File object is passed from input[type=file] or drag-drop,
// we normalize it to match Uppy file object
if (fileDescriptor instanceof File) {
// eslint-disable-next-line no-param-reassign
fileDescriptor = {
name: fileDescriptor.name,
type: fileDescriptor.type,
size: fileDescriptor.size,
data: fileDescriptor,
}
}
const fileDescriptor = fileDescriptorOrFile instanceof File ? {
name: fileDescriptorOrFile.name,
type: fileDescriptorOrFile.type,
size: fileDescriptorOrFile.size,
data: fileDescriptorOrFile,
} : fileDescriptorOrFile

@@ -471,8 +496,2 @@ const fileType = getFileType(fileDescriptor)

if (this.checkIfFileAlreadyExists(id)) {
const error = new RestrictionError(this.i18n('noDuplicates', { fileName }))
this.#informAndEmit(error, fileDescriptor)
throw error
}
const meta = fileDescriptor.meta || {}

@@ -485,3 +504,3 @@ meta.name = fileName

let newFile = {
return {
source: fileDescriptor.source || '',

@@ -509,23 +528,2 @@ id,

}
const onBeforeFileAddedResult = this.opts.onBeforeFileAdded(newFile, files)
if (onBeforeFileAddedResult === false) {
// Don’t show UI info for this error, as it should be done by the developer
const error = new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.')
this.emit('restriction-failed', fileDescriptor, error)
throw error
} else if (typeof onBeforeFileAddedResult === 'object' && onBeforeFileAddedResult !== null) {
newFile = onBeforeFileAddedResult
}
try {
const filesArray = Object.keys(files).map(i => files[i])
this.#restricter.validate(newFile, filesArray)
} catch (err) {
this.#informAndEmit(err, newFile)
throw err
}
return newFile
}

@@ -547,2 +545,72 @@

#checkAndUpdateFileState (filesToAdd) {
const { files: existingFiles } = this.getState()
// create a copy of the files object only once
const nextFilesState = { ...existingFiles }
const validFilesToAdd = []
const errors = []
for (const fileToAdd of filesToAdd) {
try {
let newFile = this.#transformFile(fileToAdd)
// If a file has been recovered (Golden Retriever), but we were unable to recover its data (probably too large),
// users are asked to re-select these half-recovered files and then this method will be called again.
// In order to keep the progress, meta and everthing else, we keep the existing file,
// but we replace `data`, and we remove `isGhost`, because the file is no longer a ghost now
if (existingFiles[newFile.id]?.isGhost) {
const { isGhost, ...existingFileState } = existingFiles[newFile.id]
newFile = {
...existingFileState,
data: fileToAdd.data,
}
this.log(`Replaced the blob in the restored ghost file: ${newFile.name}, ${newFile.id}`)
}
if (this.checkIfFileAlreadyExists(newFile.id)) {
throw new RestrictionError(this.i18n('noDuplicates', { fileName: newFile.name }), { file: fileToAdd })
}
const onBeforeFileAddedResult = this.opts.onBeforeFileAdded(newFile, nextFilesState)
if (onBeforeFileAddedResult === false) {
// Don’t show UI info for this error, as it should be done by the developer
throw new RestrictionError('Cannot add the file because onBeforeFileAdded returned false.', { isUserFacing: false, file: fileToAdd })
} else if (typeof onBeforeFileAddedResult === 'object' && onBeforeFileAddedResult !== null) {
newFile = onBeforeFileAddedResult
}
this.#restricter.validateSingleFile(newFile)
// need to add it to the new local state immediately, so we can use the state to validate the next files too
nextFilesState[newFile.id] = newFile
validFilesToAdd.push(newFile)
} catch (err) {
errors.push(err)
}
}
try {
// need to run this separately because it's much more slow, so if we run it inside the for-loop it will be very slow
// when many files are added
this.#restricter.validateAggregateRestrictions(Object.values(existingFiles), validFilesToAdd)
} catch (err) {
errors.push(err)
// If we have any aggregate error, don't allow adding this batch
return {
nextFilesState: existingFiles,
validFilesToAdd: [],
errors,
}
}
return {
nextFilesState,
validFilesToAdd,
errors,
}
}
/**

@@ -559,30 +627,20 @@ * Add a new file to `state.files`. This will run `onBeforeFileAdded`,

const { files } = this.getState()
let newFile = this.#checkAndCreateFileStateObject(files, file)
const { nextFilesState, validFilesToAdd, errors } = this.#checkAndUpdateFileState([file])
// Users are asked to re-select recovered files without data,
// and to keep the progress, meta and everthing else, we only replace said data
if (files[newFile.id] && files[newFile.id].isGhost) {
newFile = {
...files[newFile.id],
data: file.data,
isGhost: false,
}
this.log(`Replaced the blob in the restored ghost file: ${newFile.name}, ${newFile.id}`)
}
const restrictionErrors = errors.filter((error) => error.isRestriction)
this.#informAndEmit(restrictionErrors)
this.setState({
files: {
...files,
[newFile.id]: newFile,
},
})
if (errors.length > 0) throw errors[0]
this.emit('file-added', newFile)
this.emit('files-added', [newFile])
this.log(`Added file: ${newFile.name}, ${newFile.id}, mime type: ${newFile.type}`)
this.setState({ files: nextFilesState })
const [firstValidFileToAdd] = validFilesToAdd
this.emit('file-added', firstValidFileToAdd)
this.emit('files-added', validFilesToAdd)
this.log(`Added file: ${firstValidFileToAdd.name}, ${firstValidFileToAdd.id}, mime type: ${firstValidFileToAdd.type}`)
this.#startIfAutoProceed()
return newFile.id
return firstValidFileToAdd.id
}

@@ -600,51 +658,12 @@

// create a copy of the files object only once
const files = { ...this.getState().files }
const newFiles = []
const errors = []
for (let i = 0; i < fileDescriptors.length; i++) {
try {
let newFile = this.#checkAndCreateFileStateObject(files, fileDescriptors[i])
// Users are asked to re-select recovered files without data,
// and to keep the progress, meta and everthing else, we only replace said data
if (files[newFile.id] && files[newFile.id].isGhost) {
newFile = {
...files[newFile.id],
data: fileDescriptors[i].data,
isGhost: false,
}
this.log(`Replaced blob in a ghost file: ${newFile.name}, ${newFile.id}`)
}
files[newFile.id] = newFile
newFiles.push(newFile)
} catch (err) {
if (!err.isRestriction) {
errors.push(err)
}
}
}
const { nextFilesState, validFilesToAdd, errors } = this.#checkAndUpdateFileState(fileDescriptors)
this.setState({ files })
const restrictionErrors = errors.filter((error) => error.isRestriction)
this.#informAndEmit(restrictionErrors)
newFiles.forEach((newFile) => {
this.emit('file-added', newFile)
})
const nonRestrictionErrors = errors.filter((error) => !error.isRestriction)
this.emit('files-added', newFiles)
if (newFiles.length > 5) {
this.log(`Added batch of ${newFiles.length} files`)
} else {
Object.keys(newFiles).forEach(fileID => {
this.log(`Added file: ${newFiles[fileID].name}\n id: ${newFiles[fileID].id}\n type: ${newFiles[fileID].type}`)
})
}
if (newFiles.length > 0) {
this.#startIfAutoProceed()
}
if (errors.length > 0) {
if (nonRestrictionErrors.length > 0) {
let message = 'Multiple errors occurred while adding files:\n'
errors.forEach((subError) => {
nonRestrictionErrors.forEach((subError) => {
message += `\n * ${subError.message}`

@@ -654,3 +673,3 @@ })

this.info({
message: this.i18n('addBulkFilesFailed', { smart_count: errors.length }),
message: this.i18n('addBulkFilesFailed', { smart_count: nonRestrictionErrors.length }),
details: message,

@@ -660,9 +679,31 @@ }, 'error', this.opts.infoTimeout)

if (typeof AggregateError === 'function') {
throw new AggregateError(errors, message)
throw new AggregateError(nonRestrictionErrors, message)
} else {
const err = new Error(message)
err.errors = errors
err.errors = nonRestrictionErrors
throw err
}
}
// OK, we haven't thrown an error, we can start updating state and emitting events now:
this.setState({ files: nextFilesState })
validFilesToAdd.forEach((file) => {
this.emit('file-added', file)
})
this.emit('files-added', validFilesToAdd)
if (validFilesToAdd.length > 5) {
this.log(`Added batch of ${validFilesToAdd.length} files`)
} else {
Object.values(validFilesToAdd).forEach((file) => {
this.log(`Added file: ${file.name}\n id: ${file.id}\n type: ${file.type}`)
})
}
if (validFilesToAdd.length > 0) {
this.#startIfAutoProceed()
}
}

@@ -984,2 +1025,3 @@

const newError = new Error(error.message)
newError.isUserFacing = true // todo maybe don't do this with all errors?
newError.details = error.message

@@ -990,5 +1032,5 @@ if (error.details) {

newError.message = this.i18n('failedToUpload', { file: file?.name })
this.#informAndEmit(newError)
this.#informAndEmit([newError])
} else {
this.#informAndEmit(error)
this.#informAndEmit([error])
}

@@ -1014,16 +1056,31 @@ })

this.on('upload-started', (file) => {
if (file == null || !this.getFile(file.id)) {
this.log(`Not setting progress for a file that has been removed: ${file?.id}`)
return
}
this.setFileState(file.id, {
progress: {
uploadStarted: Date.now(),
uploadComplete: false,
percentage: 0,
bytesUploaded: 0,
bytesTotal: file.size,
const onUploadStarted = (files) => {
const filesFiltered = files.filter((file) => {
const exists = (file != null && this.getFile(file.id))
if (!exists) this.log(`Not setting progress for a file that has been removed: ${file?.id}`)
return exists
})
const filesState = Object.fromEntries(filesFiltered.map((file) => ([
file.id,
{
progress: {
uploadStarted: Date.now(),
uploadComplete: false,
percentage: 0,
bytesUploaded: 0,
bytesTotal: file.size,
},
},
])))
this.patchFilesState(filesState)
}
this.on('upload-start', (files) => {
files.forEach((file) => {
// todo backward compat, remove this event in a next major
this.emit('upload-started', file)
})
onUploadStarted(files)
})

@@ -1437,6 +1494,9 @@

async #runUpload (uploadID) {
let { currentUploads } = this.getState()
let currentUpload = currentUploads[uploadID]
const restoreStep = currentUpload.step || 0
const getCurrentUpload = () => {
const { currentUploads } = this.getState()
return currentUploads[uploadID]
}
let currentUpload = getCurrentUpload()
const steps = [

@@ -1448,3 +1508,3 @@ ...this.#preProcessors,

try {
for (let step = restoreStep; step < steps.length; step++) {
for (let step = currentUpload.step || 0; step < steps.length; step++) {
if (!currentUpload) {

@@ -1455,21 +1515,20 @@ break

const updatedUpload = {
...currentUpload,
step,
}
this.setState({
currentUploads: {
...currentUploads,
[uploadID]: updatedUpload,
...this.getState().currentUploads,
[uploadID]: {
...currentUpload,
step,
},
},
})
const { fileIDs } = currentUpload
// TODO give this the `updatedUpload` object as its only parameter maybe?
// Otherwise when more metadata may be added to the upload this would keep getting more parameters
await fn(updatedUpload.fileIDs, uploadID)
await fn(fileIDs, uploadID)
// Update currentUpload value in case it was modified asynchronously.
currentUploads = this.getState().currentUploads
currentUpload = currentUploads[uploadID]
currentUpload = getCurrentUpload()
}

@@ -1506,4 +1565,3 @@ } catch (err) {

// Update currentUpload value in case it was modified asynchronously.
currentUploads = this.getState().currentUploads
currentUpload = currentUploads[uploadID]
currentUpload = getCurrentUpload()
}

@@ -1557,3 +1615,3 @@ // Emit completion events.

.catch((err) => {
this.#informAndEmit(err)
this.#informAndEmit([err])
throw err

@@ -1560,0 +1618,0 @@ })

@@ -39,6 +39,8 @@ /* eslint-disable */

| 'emptyFolderAdded'
| 'addedNumFiles'
| 'folderAlreadyAdded'
| 'folderAdded'
| 'additionalRestrictionsFailed'
>
export default CoreLocale

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc