mirror of
https://github.com/diced/zipline.git
synced 2026-01-07 10:50:41 -08:00
309 lines
9.8 KiB
TypeScript
309 lines
9.8 KiB
TypeScript
import {
|
|
Button,
|
|
Collapse,
|
|
Group,
|
|
NumberInput,
|
|
PasswordInput,
|
|
Progress,
|
|
Select,
|
|
Title,
|
|
Tooltip,
|
|
} from '@mantine/core';
|
|
import { randomId, useClipboard } from '@mantine/hooks';
|
|
import { useModals } from '@mantine/modals';
|
|
import { showNotification, updateNotification } from '@mantine/notifications';
|
|
import Dropzone from 'components/dropzone/Dropzone';
|
|
import FileDropzone from 'components/dropzone/DropzoneFile';
|
|
import { ClockIcon, CrossIcon, UploadIcon } from 'components/icons';
|
|
import { invalidateFiles } from 'lib/queries/files';
|
|
import { userSelector } from 'lib/recoil/user';
|
|
import { expireReadToDate, randomChars } from 'lib/utils/client';
|
|
import { useEffect, useState } from 'react';
|
|
import { useRecoilValue } from 'recoil';
|
|
import showFilesModal from './showFilesModal';
|
|
import useUploadOptions from './useUploadOptions';
|
|
|
|
export default function File({ chunks: chunks_config }) {
|
|
const clipboard = useClipboard();
|
|
const modals = useModals();
|
|
const user = useRecoilValue(userSelector);
|
|
|
|
const [files, setFiles] = useState([]);
|
|
const [progress, setProgress] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const [options, setOpened, OptionsModal] = useUploadOptions();
|
|
|
|
useEffect(() => {
|
|
window.addEventListener('paste', (e: ClipboardEvent) => {
|
|
const item = Array.from(e.clipboardData.items).find((x) => /^image/.test(x.type));
|
|
const file = item.getAsFile();
|
|
setFiles([...files, file]);
|
|
showNotification({
|
|
title: 'Image imported from clipboard',
|
|
message: '',
|
|
});
|
|
});
|
|
});
|
|
|
|
const handleChunkedFiles = async (expires_at: Date, toChunkFiles: File[]) => {
|
|
for (let i = 0; i !== toChunkFiles.length; ++i) {
|
|
const file = toChunkFiles[i];
|
|
const identifier = randomChars(4);
|
|
|
|
const nChunks = Math.ceil(file.size / chunks_config.chunks_size);
|
|
const chunks: {
|
|
blob: Blob;
|
|
start: number;
|
|
end: number;
|
|
}[] = [];
|
|
|
|
for (let j = 0; j !== nChunks; ++j) {
|
|
const chunk = file.slice(j * chunks_config.chunks_size, (j + 1) * chunks_config.chunks_size);
|
|
chunks.push({
|
|
blob: chunk,
|
|
start: j * chunks_config.chunks_size,
|
|
end: (j + 1) * chunks_config.chunks_size,
|
|
});
|
|
}
|
|
|
|
let ready = true;
|
|
for (let j = 0; j !== chunks.length; ++j) {
|
|
while (!ready) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
|
|
// if last chunk send notif that it will take a while
|
|
if (j === chunks.length - 1) {
|
|
updateNotification({
|
|
id: 'upload-chunked',
|
|
title: 'Finalizing partial upload',
|
|
message: 'This may take a while...',
|
|
icon: <ClockIcon />,
|
|
color: 'yellow',
|
|
autoClose: false,
|
|
});
|
|
}
|
|
|
|
const body = new FormData();
|
|
body.append('file', chunks[j].blob);
|
|
|
|
setLoading(true);
|
|
const req = new XMLHttpRequest();
|
|
|
|
req.addEventListener(
|
|
'load',
|
|
(e) => {
|
|
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
|
|
const json = JSON.parse(e.target.response);
|
|
|
|
if (json.error === undefined) {
|
|
setProgress(Math.round((j / chunks.length) * 100));
|
|
updateNotification({
|
|
id: 'upload-chunked',
|
|
title: `Uploading chunk ${j + 1}/${chunks.length} Successful`,
|
|
message: '',
|
|
color: 'green',
|
|
icon: <UploadIcon />,
|
|
autoClose: false,
|
|
});
|
|
|
|
if (j === chunks.length - 1) {
|
|
updateNotification({
|
|
id: 'upload-chunked',
|
|
title: 'Upload Successful',
|
|
message: '',
|
|
color: 'green',
|
|
icon: <UploadIcon />,
|
|
});
|
|
showFilesModal(clipboard, modals, json.files);
|
|
invalidateFiles();
|
|
setFiles([]);
|
|
setProgress(100);
|
|
|
|
setTimeout(() => setProgress(0), 1000);
|
|
|
|
clipboard.copy(json.files[0]);
|
|
}
|
|
|
|
ready = true;
|
|
} else {
|
|
updateNotification({
|
|
id: 'upload-chunked',
|
|
title: `Uploading chunk ${j + 1}/${chunks.length} Failed`,
|
|
message: json.error,
|
|
color: 'red',
|
|
icon: <CrossIcon />,
|
|
autoClose: false,
|
|
});
|
|
ready = false;
|
|
}
|
|
},
|
|
false
|
|
);
|
|
|
|
req.open('POST', '/api/upload');
|
|
req.setRequestHeader('Authorization', user.token);
|
|
req.setRequestHeader('Content-Range', `bytes ${chunks[j].start}-${chunks[j].end}/${file.size}`);
|
|
req.setRequestHeader('X-Zipline-Partial-FileName', file.name);
|
|
req.setRequestHeader('X-Zipline-Partial-MimeType', file.type);
|
|
req.setRequestHeader('X-Zipline-Partial-Identifier', identifier);
|
|
req.setRequestHeader('X-Zipline-Partial-LastChunk', j === chunks.length - 1 ? 'true' : 'false');
|
|
options.expires !== 'never' && req.setRequestHeader('Expires-At', 'date=' + expires_at.toISOString());
|
|
options.password.trim() !== '' && req.setRequestHeader('Password', options.password);
|
|
options.maxViews &&
|
|
options.maxViews !== 0 &&
|
|
req.setRequestHeader('Max-Views', String(options.maxViews));
|
|
options.compression !== 'none' &&
|
|
req.setRequestHeader('Image-Compression-Percent', options.compression);
|
|
options.embedded && req.setRequestHeader('Embed', 'true');
|
|
options.zeroWidth && req.setRequestHeader('Zws', 'true');
|
|
options.format !== 'default' && req.setRequestHeader('Format', options.format);
|
|
|
|
req.send(body);
|
|
|
|
ready = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleUpload = async () => {
|
|
const expires_at = options.expires === 'never' ? null : expireReadToDate(options.expires);
|
|
|
|
setProgress(0);
|
|
setLoading(true);
|
|
const body = new FormData();
|
|
const toChunkFiles = [];
|
|
|
|
for (let i = 0; i !== files.length; ++i) {
|
|
const file = files[i];
|
|
if (file.size >= chunks_config.max_size) {
|
|
toChunkFiles.push(file);
|
|
} else {
|
|
body.append('file', files[i]);
|
|
}
|
|
}
|
|
|
|
const bodyLength = body.getAll('file').length;
|
|
|
|
if (bodyLength === 0 && toChunkFiles.length) {
|
|
showNotification({
|
|
id: 'upload-chunked',
|
|
title: 'Uploading chunked files',
|
|
message: '',
|
|
loading: true,
|
|
autoClose: false,
|
|
});
|
|
|
|
return handleChunkedFiles(expires_at, toChunkFiles);
|
|
}
|
|
|
|
showNotification({
|
|
id: 'upload',
|
|
title: 'Uploading files...',
|
|
message: '',
|
|
loading: true,
|
|
autoClose: false,
|
|
});
|
|
|
|
const req = new XMLHttpRequest();
|
|
req.upload.addEventListener('progress', (e) => {
|
|
if (e.lengthComputable) {
|
|
setProgress(Math.round((e.loaded / e.total) * 100));
|
|
}
|
|
});
|
|
|
|
req.addEventListener(
|
|
'load',
|
|
(e) => {
|
|
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
|
|
const json = JSON.parse(e.target.response);
|
|
setLoading(false);
|
|
|
|
if (!json.error) {
|
|
updateNotification({
|
|
id: 'upload',
|
|
title: 'Upload Successful',
|
|
message: '',
|
|
color: 'green',
|
|
icon: <UploadIcon />,
|
|
});
|
|
showFilesModal(clipboard, modals, json.files);
|
|
setFiles([]);
|
|
invalidateFiles();
|
|
|
|
if (toChunkFiles.length) {
|
|
showNotification({
|
|
id: 'upload-chunked',
|
|
title: 'Uploading chunked files',
|
|
message: '',
|
|
loading: true,
|
|
autoClose: false,
|
|
});
|
|
|
|
return handleChunkedFiles(expires_at, toChunkFiles);
|
|
}
|
|
} else {
|
|
updateNotification({
|
|
id: 'upload',
|
|
title: 'Upload Failed',
|
|
message: json.error,
|
|
color: 'red',
|
|
icon: <CrossIcon />,
|
|
});
|
|
}
|
|
setProgress(0);
|
|
},
|
|
false
|
|
);
|
|
|
|
if (bodyLength !== 0) {
|
|
req.open('POST', '/api/upload');
|
|
req.setRequestHeader('Authorization', user.token);
|
|
options.expires !== 'never' && req.setRequestHeader('Expires-At', 'date=' + expires_at.toISOString());
|
|
options.password.trim() !== '' && req.setRequestHeader('Password', options.password);
|
|
options.maxViews &&
|
|
options.maxViews !== 0 &&
|
|
req.setRequestHeader('Max-Views', String(options.maxViews));
|
|
options.compression !== 'none' &&
|
|
req.setRequestHeader('Image-Compression-Percent', options.compression);
|
|
options.embedded && req.setRequestHeader('Embed', 'true');
|
|
options.zeroWidth && req.setRequestHeader('Zws', 'true');
|
|
options.format !== 'default' && req.setRequestHeader('Format', options.format);
|
|
|
|
req.send(body);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<OptionsModal />
|
|
<Title mb='md'>Upload Files</Title>
|
|
|
|
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
|
|
<Group position='center' spacing='md'>
|
|
{files.map((file) => (
|
|
<FileDropzone key={randomId()} file={file} />
|
|
))}
|
|
</Group>
|
|
</Dropzone>
|
|
|
|
<Collapse in={progress !== 0}>
|
|
{progress !== 0 && <Progress mt='md' value={progress} animate />}
|
|
</Collapse>
|
|
|
|
<Group position='right' mt='md'>
|
|
<Button onClick={() => setOpened(true)} variant='outline'>
|
|
Options
|
|
</Button>
|
|
<Button onClick={() => setFiles([])} color='red' variant='outline'>
|
|
Clear Files
|
|
</Button>
|
|
<Button leftIcon={<UploadIcon />} onClick={handleUpload} disabled={files.length === 0 ? true : false}>
|
|
Upload
|
|
</Button>
|
|
</Group>
|
|
</>
|
|
);
|
|
}
|