mirror of
https://github.com/diced/zipline.git
synced 2025-12-05 20:40:12 -08:00
283 lines
9.2 KiB
TypeScript
Executable File
283 lines
9.2 KiB
TypeScript
Executable File
import { useConfig } from '@/components/ConfigProvider';
|
|
import { Response } from '@/lib/api/response';
|
|
import { bytes } from '@/lib/bytes';
|
|
import { ErrorBody } from '@/lib/response';
|
|
import { UploadOptionsStore } from '@/lib/store/uploadOptions';
|
|
import { ActionIcon, Anchor, Group, Stack, Table, Text, Tooltip } from '@mantine/core';
|
|
import { useClipboard } from '@mantine/hooks';
|
|
import { modals } from '@mantine/modals';
|
|
import { hideNotification, notifications } from '@mantine/notifications';
|
|
import { IconClipboardCopy, IconExternalLink, IconFileUpload, IconFileXFilled } from '@tabler/icons-react';
|
|
import { Link } from 'react-router-dom';
|
|
|
|
export function filesModal(
|
|
files: Response['/api/upload']['files'],
|
|
{
|
|
clipboard,
|
|
clearEphemeral,
|
|
}: {
|
|
clipboard: ReturnType<typeof useClipboard>;
|
|
clearEphemeral: () => void;
|
|
},
|
|
) {
|
|
const open = (idx: number) => window.open(files[idx].url, '_blank');
|
|
const copy = (idx: number) => {
|
|
clipboard.copy(files[idx].url);
|
|
notifications.show({
|
|
title: 'Copied URL to clipboard',
|
|
message: (
|
|
<Anchor component={Link} to={files[idx].url} target='_blank'>
|
|
{files[idx].url}
|
|
</Anchor>
|
|
),
|
|
color: 'blue',
|
|
icon: <IconClipboardCopy size='1rem' />,
|
|
});
|
|
};
|
|
|
|
modals.open({
|
|
title: 'Uploaded files',
|
|
size: 'auto',
|
|
children: (
|
|
<Table withTableBorder={false} withColumnBorders={false} highlightOnHover horizontalSpacing={'sm'}>
|
|
<Stack>
|
|
{files.map((file, idx) => (
|
|
<Group key={idx} justify='space-between'>
|
|
<Group justify='left'>
|
|
<Anchor component={Link} to={file.url}>
|
|
{file.url}
|
|
</Anchor>
|
|
</Group>
|
|
<Group justify='right'>
|
|
<Tooltip label='Open link in a new tab'>
|
|
<ActionIcon onClick={() => open(idx)} variant='filled' color='primary'>
|
|
<IconExternalLink size='1rem' />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
<Tooltip label='Copy link to clipboard'>
|
|
<ActionIcon onClick={() => copy(idx)} variant='filled' color='primary'>
|
|
<IconClipboardCopy size='1rem' />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
</Group>
|
|
))}
|
|
</Stack>
|
|
</Table>
|
|
),
|
|
});
|
|
|
|
clearEphemeral();
|
|
}
|
|
|
|
export async function uploadPartialFiles(
|
|
files: File[],
|
|
{
|
|
setProgress,
|
|
setLoading,
|
|
setFiles,
|
|
clipboard,
|
|
options,
|
|
ephemeral,
|
|
config,
|
|
folder,
|
|
}: {
|
|
setProgress: (o: { percent: number; remaining: number; speed: number }) => void;
|
|
setLoading: (loading: boolean) => void;
|
|
setFiles: (files: File[]) => void;
|
|
clipboard: ReturnType<typeof useClipboard>;
|
|
clearEphemeral: () => void;
|
|
options: UploadOptionsStore['options'];
|
|
ephemeral: UploadOptionsStore['ephemeral'];
|
|
config: ReturnType<typeof useConfig>;
|
|
folder?: string;
|
|
},
|
|
) {
|
|
setLoading(true);
|
|
setProgress({ percent: 0, remaining: 0, speed: 0 });
|
|
|
|
const chunkSize = bytes(config.chunks.size);
|
|
|
|
for (let i = 0; i !== files.length; ++i) {
|
|
const file = files[i];
|
|
const nChunks = Math.ceil(file.size / chunkSize);
|
|
const chunks: {
|
|
blob: Blob;
|
|
start: number;
|
|
end: number;
|
|
}[] = [];
|
|
|
|
for (let j = 0; j !== nChunks; ++j) {
|
|
const start = j * chunkSize;
|
|
const end = Math.min(start + chunkSize, file.size);
|
|
chunks.push({
|
|
blob: file.slice(start, end),
|
|
start,
|
|
end,
|
|
});
|
|
}
|
|
|
|
notifications.show({
|
|
id: 'upload-partial',
|
|
title: 'Uploading partial file',
|
|
message: `Uploading partial ${i + 1}/${chunks.length}`,
|
|
loading: true,
|
|
autoClose: false,
|
|
});
|
|
|
|
let ready = true;
|
|
let totalLoaded = 0;
|
|
let identifier: string | undefined;
|
|
const start = Date.now();
|
|
|
|
for (let j = 0; j !== nChunks; ++j) {
|
|
while (!ready) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
|
|
const body = new FormData();
|
|
body.append('file', chunks[j].blob);
|
|
|
|
setLoading(true);
|
|
const req = new XMLHttpRequest();
|
|
let lastLoaded = 0;
|
|
|
|
req.upload.addEventListener('progress', (e) => {
|
|
if (e.lengthComputable) {
|
|
const sent = e.loaded - lastLoaded;
|
|
lastLoaded = e.loaded;
|
|
totalLoaded += sent;
|
|
|
|
const speed = totalLoaded / ((Date.now() - start) / 1000);
|
|
const remainingTime = (file.size - totalLoaded) / speed;
|
|
|
|
setProgress({
|
|
percent: Math.round((totalLoaded / file.size) * 100),
|
|
speed,
|
|
remaining: remainingTime,
|
|
});
|
|
}
|
|
});
|
|
|
|
req.addEventListener(
|
|
'load',
|
|
() => {
|
|
const res: Response['/api/upload/partial'] = JSON.parse(req.responseText);
|
|
|
|
if ((res as ErrorBody).error) {
|
|
notifications.update({
|
|
id: 'upload-partial',
|
|
title: 'Error uploading files',
|
|
message: (res as ErrorBody).error,
|
|
color: 'red',
|
|
icon: <IconFileXFilled size='1rem' />,
|
|
autoClose: false,
|
|
loading: false,
|
|
});
|
|
ready = false;
|
|
setFiles([]);
|
|
setProgress({ percent: 0, remaining: 0, speed: 0 });
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
notifications.update({
|
|
id: 'upload-partial',
|
|
title: 'Uploading partial file',
|
|
message: `Uploading partial ${j + 1}/${nChunks} successful`,
|
|
loading: false,
|
|
autoClose: false,
|
|
});
|
|
|
|
if (j === 0) {
|
|
identifier = res.partialIdentifier;
|
|
}
|
|
|
|
if (j === chunks.length - 1) {
|
|
notifications.update({
|
|
id: 'upload-partial',
|
|
title: 'Finalizing partial upload',
|
|
message: (
|
|
<Text>
|
|
The upload has been offloaded and will complete in the background.
|
|
<br />
|
|
<Anchor
|
|
component='span'
|
|
onClick={() => {
|
|
hideNotification('upload-partial');
|
|
clipboard.copy(res.files[0].url);
|
|
notifications.show({
|
|
title: 'Copied URL to clipboard',
|
|
message: (
|
|
<Anchor component={Link} to={res.files[0].url} target='_blank'>
|
|
{res.files[0].url}
|
|
</Anchor>
|
|
),
|
|
});
|
|
}}
|
|
>
|
|
Click here to copy the URL to clipboard while it's being processed.
|
|
</Anchor>
|
|
<br />
|
|
<Anchor component={Link} to='/dashboard/files?popen=true'>
|
|
View processing files
|
|
</Anchor>
|
|
</Text>
|
|
),
|
|
color: 'green',
|
|
icon: <IconFileUpload size='1rem' />,
|
|
autoClose: true,
|
|
loading: false,
|
|
});
|
|
|
|
setFiles([]);
|
|
setProgress({ percent: 100, remaining: 0, speed: 0 });
|
|
setLoading(false);
|
|
|
|
setTimeout(() => setProgress({ percent: 0, remaining: 0, speed: 0 }), 1000);
|
|
}
|
|
|
|
ready = true;
|
|
},
|
|
false,
|
|
);
|
|
|
|
req.open('POST', '/api/upload/partial');
|
|
options.deletesAt !== 'default' && req.setRequestHeader('x-zipline-deletes-at', options.deletesAt);
|
|
options.format !== 'default' && req.setRequestHeader('x-zipline-format', options.format);
|
|
options.imageCompressionPercent &&
|
|
req.setRequestHeader(
|
|
'x-zipline-image-compression-percent',
|
|
options.imageCompressionPercent.toString(),
|
|
);
|
|
options.imageCompressionFormat !== 'default' &&
|
|
req.setRequestHeader('x-zipline-image-compression-type', options.imageCompressionFormat);
|
|
options.maxViews && req.setRequestHeader('x-zipline-max-views', options.maxViews.toString());
|
|
options.addOriginalName && req.setRequestHeader('x-zipline-original-name', 'true');
|
|
options.overrides_returnDomain &&
|
|
req.setRequestHeader('x-zipline-domain', options.overrides_returnDomain);
|
|
|
|
ephemeral.password && req.setRequestHeader('x-zipline-password', ephemeral.password);
|
|
ephemeral.filename &&
|
|
req.setRequestHeader('x-zipline-filename', encodeURIComponent(ephemeral.filename));
|
|
|
|
if (folder) {
|
|
req.setRequestHeader('x-zipline-folder', folder);
|
|
} else if (ephemeral.folderId) {
|
|
req.setRequestHeader('x-zipline-folder', ephemeral.folderId);
|
|
}
|
|
|
|
identifier && req.setRequestHeader('x-zipline-p-identifier', identifier);
|
|
req.setRequestHeader('x-zipline-p-filename', encodeURIComponent(file.name));
|
|
req.setRequestHeader('x-zipline-p-lastchunk', j === chunks.length - 1 ? 'true' : 'false');
|
|
req.setRequestHeader('x-zipline-p-content-type', file.type);
|
|
req.setRequestHeader('x-zipline-p-content-length', file.size.toString());
|
|
req.setRequestHeader('content-range', `bytes ${chunks[j].start}-${chunks[j].end}/${file.size}`);
|
|
|
|
req.send(body);
|
|
|
|
ready = false;
|
|
}
|
|
}
|
|
}
|