Files
speckle-server/packages/frontend-2/lib/core/helpers/file.ts
T
Kristaps Fabians Geikins b02a07e2b6 feat: Frontend 2.0 MVP
2023-05-08 10:47:01 +03:00

123 lines
3.4 KiB
TypeScript

import { difference, intersection } from 'lodash-es'
import { Nullable } from '@speckle/shared'
import md5 from '~~/lib/common/helpers/md5'
import { BaseError } from '~~/lib/core/errors/base'
export type FileTypeSpecifier = UniqueFileTypeSpecifier | `.${string}`
export enum UniqueFileTypeSpecifier {
AnyAudio = 'audio/*',
AnyVideo = 'video/*',
AnyImage = 'image/*'
}
/**
* Validate if file has the allowed type. While we could also test for MIME types
* not in UniqueFileTypeSpecifier, this function is meant to be equivalent to the
* 'accept' attribute, which only allows for extensions or UniqueFileTypeSpecifier
* values.
* @param file
* @param allowedTypes The file must have one of these types
* @returns True if valid, Error object if not
*/
export function validateFileType(
file: File,
allowedTypes: FileTypeSpecifier[]
): true | Error {
// Check one of the unique file type specifiers first
const allowedUniqueTypes = intersection(
Object.values(UniqueFileTypeSpecifier),
allowedTypes
)
for (const allowedUniqueType of allowedUniqueTypes) {
switch (allowedUniqueType) {
case UniqueFileTypeSpecifier.AnyAudio:
if (file.type.startsWith('audio')) return true
break
case UniqueFileTypeSpecifier.AnyImage:
if (file.type.startsWith('image')) return true
break
case UniqueFileTypeSpecifier.AnyVideo:
if (file.type.startsWith('video')) return true
break
}
}
// Check file extensions
const allowedExtensions = difference(allowedTypes, allowedUniqueTypes)
const fileExt = resolveFileExtension(file.name)
if (!fileExt) return new MissingFileExtensionError()
for (const allowedExtension of allowedExtensions) {
if (allowedExtension === fileExt) return true
}
return new ForbiddenFileTypeError()
}
/**
* Resolve file extension (with leading dot)
*/
export function resolveFileExtension(fileName: string): Nullable<FileTypeSpecifier> {
const ext = fileName.split('.').pop() || null
return ext ? `.${ext}` : null
}
/**
* Check if string is a FileTypeSpecifier
*/
export function isFileTypeSpecifier(type: string): type is FileTypeSpecifier {
return (
type.startsWith('.') ||
Object.values(UniqueFileTypeSpecifier as Record<string, string>).includes(type)
)
}
/**
* Create a human readable file size string from the numeric size in bytes
*/
export function prettyFileSize(sizeInBytes: number): string {
const removeTrailingZeroes = (fileSize: number) =>
parseFloat(fileSize.toFixed(2)).toString()
if (sizeInBytes < 1024) {
return `${sizeInBytes}bytes`
}
const kbSize = sizeInBytes / 1024
if (kbSize < 1024) {
return `${removeTrailingZeroes(kbSize)}KB`
}
const mbSize = kbSize / 1024
if (mbSize < 1024) {
return `${removeTrailingZeroes(mbSize)}MB`
}
const gbSize = mbSize / 1024
return `${removeTrailingZeroes(gbSize)}GB`
}
/**
* Generate an ID that uniquely identifies a specific file. The same file
* will always have the same ID.
*/
export function generateFileId(file: File): string {
const importantData = {
name: file.name,
lastModified: file.lastModified,
size: file.size,
type: file.type
}
return md5(JSON.stringify(importantData))
}
export class MissingFileExtensionError extends BaseError {
static defaultMessage = 'The selected file has a missing extension'
}
export class ForbiddenFileTypeError extends BaseError {
static defaultMessage = 'The selected file type is forbidden'
}