19
common/resources/client/uploads/utils/convert-to-bytes.ts
Executable file
19
common/resources/client/uploads/utils/convert-to-bytes.ts
Executable file
@@ -0,0 +1,19 @@
|
||||
export type SpaceUnit = 'KB' | 'MB' | 'GB' | 'TB' | 'PB';
|
||||
|
||||
export function convertToBytes(value: number, unit: SpaceUnit): number {
|
||||
if (value == null) return 0;
|
||||
switch (unit) {
|
||||
case 'KB':
|
||||
return value * 1024;
|
||||
case 'MB':
|
||||
return value * 1024 ** 2;
|
||||
case 'GB':
|
||||
return value * 1024 ** 3;
|
||||
case 'TB':
|
||||
return value * 1024 ** 4;
|
||||
case 'PB':
|
||||
return value * 1024 ** 5;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
49
common/resources/client/uploads/utils/create-upload-input.ts
Executable file
49
common/resources/client/uploads/utils/create-upload-input.ts
Executable file
@@ -0,0 +1,49 @@
|
||||
import {UploadInputConfig} from '../types/upload-input-config';
|
||||
|
||||
export function createUploadInput(
|
||||
config: UploadInputConfig = {}
|
||||
): HTMLInputElement {
|
||||
const old = document.querySelector('#hidden-file-upload-input');
|
||||
if (old) old.remove();
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = config.multiple ?? false;
|
||||
input.classList.add('hidden');
|
||||
input.style.display = 'none';
|
||||
input.style.visibility = 'hidden';
|
||||
input.id = 'hidden-file-upload-input';
|
||||
|
||||
input.accept = buildUploadInputAccept(config);
|
||||
|
||||
if (config.directory) {
|
||||
input.webkitdirectory = true;
|
||||
}
|
||||
|
||||
document.body.appendChild(input);
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
export interface UploadAccentProps {
|
||||
extensions?: string[];
|
||||
types?: string[];
|
||||
}
|
||||
export function buildUploadInputAccept({
|
||||
extensions = [],
|
||||
types = [],
|
||||
}: UploadAccentProps): string {
|
||||
const accept = [];
|
||||
if (extensions?.length) {
|
||||
extensions = extensions.map(e => {
|
||||
return e.startsWith('.') ? e : `.${e}`;
|
||||
});
|
||||
accept.push(extensions.join(','));
|
||||
}
|
||||
|
||||
if (types?.length) {
|
||||
accept.push(types.join(','));
|
||||
}
|
||||
|
||||
return accept.join(',');
|
||||
}
|
||||
8
common/resources/client/uploads/utils/download-file-from-url.ts
Executable file
8
common/resources/client/uploads/utils/download-file-from-url.ts
Executable file
@@ -0,0 +1,8 @@
|
||||
export function downloadFileFromUrl(url: string, name?: string) {
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
if (name) link.download = name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
4
common/resources/client/uploads/utils/extension-from-filename.ts
Executable file
4
common/resources/client/uploads/utils/extension-from-filename.ts
Executable file
@@ -0,0 +1,4 @@
|
||||
export function extensionFromFilename(fullFileName: string): string {
|
||||
const re = /(?:\.([^.]+))?$/;
|
||||
return re.exec(fullFileName)?.[1] || '';
|
||||
}
|
||||
81
common/resources/client/uploads/utils/get-dropped-files.ts
Executable file
81
common/resources/client/uploads/utils/get-dropped-files.ts
Executable file
@@ -0,0 +1,81 @@
|
||||
import {UploadedFile} from '../uploaded-file';
|
||||
|
||||
export async function getDroppedFiles(
|
||||
dataTransfer: DataTransfer
|
||||
): Promise<UploadedFile[]> {
|
||||
let files: UploadedFile[] = [];
|
||||
|
||||
if (dataTransfer.items?.[0] && 'webkitGetAsEntry' in dataTransfer.items[0]) {
|
||||
// need to make a copy if transfer items and get entry here, it
|
||||
// might not be available anymore in subsequent loop iterations
|
||||
const entries = [...dataTransfer.items].map(item => {
|
||||
return item.webkitGetAsEntry();
|
||||
});
|
||||
for (const entry of entries) {
|
||||
if (entry && !entry.isDirectory) {
|
||||
files.push(await filesystemEntryToFile(entry as FileSystemFileEntry));
|
||||
} else if (entry) {
|
||||
files = [
|
||||
...files,
|
||||
...(await readDirRecursive(entry as FileSystemDirectoryEntry)),
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files = [...dataTransfer.files].map(f => new UploadedFile(f));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function filesystemEntryToFile(
|
||||
entry: FileSystemFileEntry
|
||||
): Promise<UploadedFile> {
|
||||
return new Promise(resolve => {
|
||||
entry.file(file => {
|
||||
resolve(new UploadedFile(file, entry.fullPath));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function readDirRecursive(
|
||||
entry: FileSystemDirectoryEntry,
|
||||
files: UploadedFile[] = []
|
||||
) {
|
||||
const entries = await readEntries(entry);
|
||||
|
||||
for (const childEntry of entries) {
|
||||
if (childEntry.isDirectory) {
|
||||
await readDirRecursive(childEntry as FileSystemDirectoryEntry, files);
|
||||
} else {
|
||||
files.push(
|
||||
await filesystemEntryToFile(childEntry as FileSystemFileEntry)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function readEntries(
|
||||
dir: FileSystemDirectoryEntry
|
||||
): Promise<FileSystemEntry[]> {
|
||||
return new Promise(resolve => {
|
||||
drainDirReader(dir.createReader(), resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function drainDirReader(
|
||||
reader: FileSystemDirectoryReader,
|
||||
resolve: (value: FileSystemEntry[]) => void,
|
||||
allEntries: FileSystemEntry[] = []
|
||||
) {
|
||||
// directory reader needs to be called repeatedly until it returns an empty array
|
||||
reader.readEntries(entries => {
|
||||
if (entries.length) {
|
||||
allEntries = [...allEntries, ...entries];
|
||||
drainDirReader(reader, resolve, allEntries);
|
||||
} else {
|
||||
resolve(allEntries);
|
||||
}
|
||||
});
|
||||
}
|
||||
30
common/resources/client/uploads/utils/get-file-mime.ts
Executable file
30
common/resources/client/uploads/utils/get-file-mime.ts
Executable file
@@ -0,0 +1,30 @@
|
||||
import {extensionFromFilename} from './extension-from-filename';
|
||||
|
||||
export function getFileMime(file: File): string {
|
||||
const extensionsToMime: Record<string, string> = {
|
||||
md: 'text/markdown',
|
||||
markdown: 'text/markdown',
|
||||
mp4: 'video/mp4',
|
||||
mp3: 'audio/mp3',
|
||||
svg: 'image/svg+xml',
|
||||
jpg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
yaml: 'text/yaml',
|
||||
yml: 'text/yaml',
|
||||
};
|
||||
|
||||
const fileExtension = file.name ? extensionFromFilename(file.name) : null;
|
||||
|
||||
// check if mime type is set in the file object
|
||||
if (file.type) {
|
||||
return file.type;
|
||||
}
|
||||
|
||||
// see if we can map extension to a mime type
|
||||
if (fileExtension && fileExtension in extensionsToMime) {
|
||||
return extensionsToMime[fileExtension];
|
||||
}
|
||||
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
31
common/resources/client/uploads/utils/open-upload-window.ts
Executable file
31
common/resources/client/uploads/utils/open-upload-window.ts
Executable file
@@ -0,0 +1,31 @@
|
||||
import {UploadInputConfig} from '../types/upload-input-config';
|
||||
import {UploadedFile} from '../uploaded-file';
|
||||
import {createUploadInput} from './create-upload-input';
|
||||
|
||||
/**
|
||||
* Open browser dialog for uploading files and
|
||||
* resolve promise with uploaded files.
|
||||
*/
|
||||
export function openUploadWindow(
|
||||
config: UploadInputConfig = {}
|
||||
): Promise<UploadedFile[]> {
|
||||
return new Promise(resolve => {
|
||||
const input = createUploadInput(config);
|
||||
|
||||
input.onchange = e => {
|
||||
const fileList = (e.target as HTMLInputElement).files;
|
||||
if (!fileList) {
|
||||
return resolve([]);
|
||||
}
|
||||
|
||||
const uploads = Array.from(fileList)
|
||||
.filter(f => f.name !== '.DS_Store')
|
||||
.map(file => new UploadedFile(file));
|
||||
resolve(uploads);
|
||||
input.remove();
|
||||
};
|
||||
|
||||
document.body.appendChild(input);
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
30
common/resources/client/uploads/utils/pretty-bytes.ts
Executable file
30
common/resources/client/uploads/utils/pretty-bytes.ts
Executable file
@@ -0,0 +1,30 @@
|
||||
// Adapted from https://github.com/Flet/prettier-bytes/
|
||||
// Changing 1000 bytes to 1024, so we can keep uppercase KB vs kB
|
||||
// ISC License (c) Dan Flettre https://github.com/Flet/prettier-bytes/blob/master/LICENSE
|
||||
export function prettyBytes(num?: number, fractionDigits = 1): string {
|
||||
if (num == null || Number.isNaN(num)) return '';
|
||||
const neg = num < 0;
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
if (neg) {
|
||||
num = -num;
|
||||
}
|
||||
|
||||
if (num < 1) {
|
||||
return `${(neg ? '-' : '') + num} B`;
|
||||
}
|
||||
|
||||
const exponent = Math.min(
|
||||
Math.floor(Math.log(num) / Math.log(1024)),
|
||||
units.length - 1
|
||||
);
|
||||
num = Number(num / Math.pow(1024, exponent));
|
||||
const unit = units[exponent];
|
||||
|
||||
if (num >= 10 || num % 1 === 0) {
|
||||
// Do not show decimals when the number is two-digit, or if the number has no
|
||||
// decimal component.
|
||||
return `${(neg ? '-' : '') + num.toFixed(0)} ${unit}`;
|
||||
}
|
||||
return `${(neg ? '-' : '') + num.toFixed(fractionDigits)} ${unit}`;
|
||||
}
|
||||
1
common/resources/client/uploads/utils/space-units.ts
Executable file
1
common/resources/client/uploads/utils/space-units.ts
Executable file
@@ -0,0 +1 @@
|
||||
export const spaceUnits = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
Reference in New Issue
Block a user