@uppy/core
Version:
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:
126 lines (125 loc) • 5.13 kB
JavaScript
import prettierBytes from '@transloadit/prettier-bytes';
// @ts-ignore untyped
import match from 'mime-match';
const defaultOptions = {
maxFileSize: null,
minFileSize: null,
maxTotalFileSize: null,
maxNumberOfFiles: null,
minNumberOfFiles: null,
allowedFileTypes: null,
requiredMetaFields: [],
};
class RestrictionError extends Error {
isUserFacing;
file;
constructor(message, opts) {
super(message);
this.isUserFacing = opts?.isUserFacing ?? true;
if (opts?.file) {
this.file = opts.file; // only some restriction errors are related to a particular file
}
}
isRestriction = true;
}
class Restricter {
getI18n;
getOpts;
constructor(getOpts, getI18n) {
this.getI18n = getI18n;
this.getOpts = () => {
const opts = getOpts();
if (opts.restrictions?.allowedFileTypes != null &&
!Array.isArray(opts.restrictions.allowedFileTypes)) {
throw new TypeError('`restrictions.allowedFileTypes` must be an array');
}
return opts;
};
}
// 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 = existingFiles.filter((f) => !f.isGhost);
if (nonGhostFiles.length + addingFiles.length > maxNumberOfFiles) {
throw new RestrictionError(`${this.getI18n()('youCanOnlyUploadX', {
smart_count: maxNumberOfFiles,
})}`);
}
}
if (maxTotalFileSize) {
const totalFilesSize = [...existingFiles, ...addingFiles].reduce((total, f) => total + (f.size ?? 0), 0);
if (totalFilesSize > maxTotalFileSize) {
throw new RestrictionError(this.getI18n()('aggregateExceedsSize', {
sizeAllowed: prettierBytes(maxTotalFileSize),
size: prettierBytes(totalFilesSize),
}));
}
}
}
validateSingleFile(file) {
const { maxFileSize, minFileSize, allowedFileTypes } = this.getOpts().restrictions;
if (allowedFileTypes) {
const isCorrectFileType = allowedFileTypes.some((type) => {
// check if this is a mime-type
if (type.includes('/')) {
if (!file.type)
return false;
return match(file.type.replace(/;.*?$/, ''), type);
}
// otherwise this is likely an extension
if (type[0] === '.' && file.extension) {
return file.extension.toLowerCase() === type.slice(1).toLowerCase();
}
return false;
});
if (!isCorrectFileType) {
const allowedFileTypesString = allowedFileTypes.join(', ');
throw new RestrictionError(this.getI18n()('youCanOnlyUploadFileTypes', {
types: allowedFileTypesString,
}), { file });
}
}
// We can't check maxFileSize if the size is unknown.
if (maxFileSize && file.size != null && file.size > maxFileSize) {
throw new RestrictionError(this.getI18n()('exceedsSize', {
size: prettierBytes(maxFileSize),
file: file.name ?? this.getI18n()('unnamed'),
}), { file });
}
// We can't check minFileSize if the size is unknown.
if (minFileSize && file.size != null && file.size < minFileSize) {
throw new RestrictionError(this.getI18n()('inferiorSize', {
size: prettierBytes(minFileSize),
}), { file });
}
}
validate(existingFiles, addingFiles) {
addingFiles.forEach((addingFile) => {
this.validateSingleFile(addingFile);
});
this.validateAggregateRestrictions(existingFiles, addingFiles);
}
validateMinNumberOfFiles(files) {
const { minNumberOfFiles } = this.getOpts().restrictions;
if (minNumberOfFiles && Object.keys(files).length < minNumberOfFiles) {
throw new RestrictionError(this.getI18n()('youHaveToAtLeastSelectX', {
smart_count: minNumberOfFiles,
}));
}
}
getMissingRequiredMetaFields(file) {
const error = new RestrictionError(this.getI18n()('missingRequiredMetaFieldOnFile', {
fileName: file.name ?? this.getI18n()('unnamed'),
}));
const { requiredMetaFields } = this.getOpts().restrictions;
const missingFields = [];
for (const field of requiredMetaFields) {
if (!Object.hasOwn(file.meta, field) || file.meta[field] === '') {
missingFields.push(field);
}
}
return { missingFields, error };
}
}
export { Restricter, defaultOptions, RestrictionError };