mirror of
https://github.com/swissmakers/swiss-datashare.git
synced 2026-04-17 04:33:15 +02:00
feat: reverse shares (#86)
* add first concept * add reverse share funcionality to frontend * allow creator to limit share expiration * moved reverse share in seperate module * add table to manage reverse shares * delete complete share if reverse share was deleted * optimize function names * add db migration * enable reverse share email notifications * fix config variable descriptions * fix migration for new installations
This commit is contained in:
@@ -100,7 +100,7 @@ const AdminConfigTable = () => {
|
||||
<Space h="lg" />
|
||||
</>
|
||||
))}
|
||||
{category == "email" && (
|
||||
{category == "smtp" && (
|
||||
<Group position="right">
|
||||
<TestEmailButton />
|
||||
</Group>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ActionIcon, Avatar, Menu } from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { TbDoorExit, TbLink, TbSettings, TbUser } from "react-icons/tb";
|
||||
import { TbDoorExit, TbSettings, TbUser } from "react-icons/tb";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
|
||||
@@ -11,17 +11,10 @@ const ActionAvatar = () => {
|
||||
<Menu position="bottom-start" withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon>
|
||||
<Avatar size={28} radius="xl" />
|
||||
<Avatar size={28} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href="/account/shares"
|
||||
icon={<TbLink size={14} />}
|
||||
>
|
||||
My shares
|
||||
</Menu.Item>
|
||||
<Menu.Item component={Link} href="/account" icon={<TbUser size={14} />}>
|
||||
My account
|
||||
</Menu.Item>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Burger,
|
||||
Container,
|
||||
@@ -13,10 +14,12 @@ import {
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import Link from "next/link";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { TbPlus } from "react-icons/tb";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import Logo from "../Logo";
|
||||
import ActionAvatar from "./ActionAvatar";
|
||||
import NavbarShareMenu from "./NavbarShareMenu";
|
||||
|
||||
const HEADER_HEIGHT = 60;
|
||||
|
||||
@@ -117,6 +120,9 @@ const NavBar = () => {
|
||||
link: "/upload",
|
||||
label: "Upload",
|
||||
},
|
||||
{
|
||||
component: <NavbarShareMenu />,
|
||||
},
|
||||
{
|
||||
component: <ActionAvatar />,
|
||||
},
|
||||
|
||||
29
frontend/src/components/navBar/NavbarShareMenu.tsx
Normal file
29
frontend/src/components/navBar/NavbarShareMenu.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ActionIcon, Menu } from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { TbArrowLoopLeft, TbLink } from "react-icons/tb";
|
||||
|
||||
const NavbarShareMneu = () => {
|
||||
return (
|
||||
<Menu position="bottom-start" withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon>
|
||||
<TbLink />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item component={Link} href="/account/shares" icon={<TbLink />}>
|
||||
My shares
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
href="/account/reverseShares"
|
||||
icon={<TbArrowLoopLeft />}
|
||||
>
|
||||
Reverse shares
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavbarShareMneu;
|
||||
@@ -2,7 +2,7 @@ import { ActionIcon, Loader, Skeleton, Table } from "@mantine/core";
|
||||
import { TbCircleCheck, TbDownload } from "react-icons/tb";
|
||||
import shareService from "../../services/share.service";
|
||||
|
||||
import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
|
||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
|
||||
const FileList = ({
|
||||
files,
|
||||
@@ -28,7 +28,7 @@ const FileList = ({
|
||||
: files!.map((file) => (
|
||||
<tr key={file.name}>
|
||||
<td>{file.name}</td>
|
||||
<td>{byteStringToHumanSizeString(file.size)}</td>
|
||||
<td>{byteToHumanSizeString(file.size)}</td>
|
||||
<td>
|
||||
{file.uploadingState ? (
|
||||
file.uploadingState != "finished" ? (
|
||||
|
||||
62
frontend/src/components/share/FileSizeInput.tsx
Normal file
62
frontend/src/components/share/FileSizeInput.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Col, Grid, NumberInput, Select } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
byteToUnitAndSize,
|
||||
unitAndSizeToByte,
|
||||
} from "../../utils/fileSize.util";
|
||||
|
||||
const FileSizeInput = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (number: number) => void;
|
||||
}) => {
|
||||
const [unit, setUnit] = useState("MB");
|
||||
const [size, setSize] = useState(100);
|
||||
|
||||
useEffect(() => {
|
||||
const { unit, size } = byteToUnitAndSize(value);
|
||||
setUnit(unit);
|
||||
setSize(size);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Grid align="flex-end">
|
||||
<Col xs={6}>
|
||||
<NumberInput
|
||||
min={1}
|
||||
max={99999}
|
||||
precision={0}
|
||||
variant="filled"
|
||||
label={label}
|
||||
value={size}
|
||||
onChange={(value) => {
|
||||
setSize(value!);
|
||||
onChange(unitAndSizeToByte(unit, value!));
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<Select
|
||||
data={[
|
||||
{ label: "B", value: "B" },
|
||||
{ label: "KB", value: "KB" },
|
||||
{ label: "MB", value: "MB" },
|
||||
{ label: "GB", value: "GB" },
|
||||
{ label: "TB", value: "TB" },
|
||||
]}
|
||||
value={unit}
|
||||
onChange={(value) => {
|
||||
setUnit(value!);
|
||||
onChange(unitAndSizeToByte(value!, size));
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileSizeInput;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ActionIcon, Button, Stack, TextInput, Title } from "@mantine/core";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import { TbCopy } from "react-icons/tb";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
const showCompletedReverseShareModal = (
|
||||
modals: ModalsContextProps,
|
||||
link: string,
|
||||
getReverseShares: () => void
|
||||
) => {
|
||||
return modals.openModal({
|
||||
closeOnClickOutside: false,
|
||||
withCloseButton: false,
|
||||
closeOnEscape: false,
|
||||
title: (
|
||||
<Stack align="stretch" spacing={0}>
|
||||
<Title order={4}>Reverse share link</Title>
|
||||
</Stack>
|
||||
),
|
||||
children: <Body link={link} getReverseShares={getReverseShares} />,
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({
|
||||
link,
|
||||
getReverseShares,
|
||||
}: {
|
||||
link: string;
|
||||
getReverseShares: () => void;
|
||||
}) => {
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const modals = useModals();
|
||||
|
||||
return (
|
||||
<Stack align="stretch">
|
||||
<TextInput
|
||||
readOnly
|
||||
variant="filled"
|
||||
value={link}
|
||||
rightSection={
|
||||
window.isSecureContext && (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
clipboard.copy(link);
|
||||
toast.success("Your link was copied to the keyboard.");
|
||||
}}
|
||||
>
|
||||
<TbCopy />
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.closeAll();
|
||||
getReverseShares();
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default showCompletedReverseShareModal;
|
||||
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Grid,
|
||||
Group,
|
||||
NumberInput,
|
||||
Select,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import shareService from "../../../services/share.service";
|
||||
import { getExpirationPreview } from "../../../utils/date.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import FileSizeInput from "../FileSizeInput";
|
||||
import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
|
||||
|
||||
const showCreateReverseShareModal = (
|
||||
modals: ModalsContextProps,
|
||||
showSendEmailNotificationOption: boolean,
|
||||
getReverseShares: () => void
|
||||
) => {
|
||||
return modals.openModal({
|
||||
title: <Title order={4}>Create reverse share</Title>,
|
||||
children: (
|
||||
<Body
|
||||
showSendEmailNotificationOption={showSendEmailNotificationOption}
|
||||
getReverseShares={getReverseShares}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({
|
||||
getReverseShares,
|
||||
showSendEmailNotificationOption,
|
||||
}: {
|
||||
getReverseShares: () => void;
|
||||
showSendEmailNotificationOption: boolean;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
maxShareSize: 104857600,
|
||||
sendEmailNotification: false,
|
||||
expiration_num: 1,
|
||||
expiration_unit: "-days",
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Group>
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
shareService
|
||||
.createReverseShare(
|
||||
values.expiration_num + values.expiration_unit,
|
||||
values.maxShareSize,
|
||||
values.sendEmailNotification
|
||||
)
|
||||
.then(({ link }) => {
|
||||
modals.closeAll();
|
||||
showCompletedReverseShareModal(modals, link, getReverseShares);
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
})}
|
||||
>
|
||||
<Stack align="stretch">
|
||||
<div>
|
||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||
<Col xs={6}>
|
||||
<NumberInput
|
||||
min={1}
|
||||
max={99999}
|
||||
precision={0}
|
||||
variant="filled"
|
||||
label="Share expiration"
|
||||
{...form.getInputProps("expiration_num")}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<Select
|
||||
{...form.getInputProps("expiration_unit")}
|
||||
data={[
|
||||
// Set the label to singular if the number is 1, else plural
|
||||
{
|
||||
value: "-minutes",
|
||||
label:
|
||||
"Minute" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-hours",
|
||||
label:
|
||||
"Hour" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-days",
|
||||
label:
|
||||
"Day" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-weeks",
|
||||
label:
|
||||
"Week" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-months",
|
||||
label:
|
||||
"Month" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
<Text
|
||||
mt="sm"
|
||||
italic
|
||||
size="xs"
|
||||
sx={(theme) => ({
|
||||
color: theme.colors.gray[6],
|
||||
})}
|
||||
>
|
||||
{getExpirationPreview("reverse share", form)}
|
||||
</Text>
|
||||
</div>
|
||||
<FileSizeInput
|
||||
label="Max share size"
|
||||
value={form.values.maxShareSize}
|
||||
onChange={(number) => form.setFieldValue("maxShareSize", number)}
|
||||
/>
|
||||
{showSendEmailNotificationOption && (
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label="Send email notification"
|
||||
description="Send an email notification when a share is created with this reverse share link"
|
||||
{...form.getInputProps("sendEmailNotification", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button mt="md" type="submit">
|
||||
Create
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default showCreateReverseShareModal;
|
||||
@@ -4,7 +4,7 @@ import { Dispatch, ForwardedRef, SetStateAction, useRef } from "react";
|
||||
import { TbCloudUpload, TbUpload } from "react-icons/tb";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
|
||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@@ -33,10 +33,12 @@ const useStyles = createStyles((theme) => ({
|
||||
|
||||
const Dropzone = ({
|
||||
isUploading,
|
||||
maxShareSize,
|
||||
files,
|
||||
setFiles,
|
||||
}: {
|
||||
isUploading: boolean;
|
||||
maxShareSize: number;
|
||||
files: FileUpload[];
|
||||
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
||||
}) => {
|
||||
@@ -58,10 +60,10 @@ const Dropzone = ({
|
||||
0
|
||||
);
|
||||
|
||||
if (fileSizeSum > config.get("MAX_SHARE_SIZE")) {
|
||||
if (fileSizeSum > maxShareSize) {
|
||||
toast.error(
|
||||
`Your files exceed the maximum share size of ${byteStringToHumanSizeString(
|
||||
config.get("MAX_SHARE_SIZE")
|
||||
`Your files exceed the maximum share size of ${byteToHumanSizeString(
|
||||
maxShareSize
|
||||
)}.`
|
||||
);
|
||||
} else {
|
||||
@@ -84,9 +86,8 @@ const Dropzone = ({
|
||||
</Text>
|
||||
<Text align="center" size="sm" mt="xs" color="dimmed">
|
||||
Drag'n'drop files here to start your share. We can accept
|
||||
only files that are less than{" "}
|
||||
{byteStringToHumanSizeString(config.get("MAX_SHARE_SIZE"))} in
|
||||
total.
|
||||
only files that are less than {byteToHumanSizeString(maxShareSize)}{" "}
|
||||
in total.
|
||||
</Text>
|
||||
</div>
|
||||
</MantineDropzone>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import moment from "moment";
|
||||
|
||||
const ExpirationPreview = ({ form }: { form: any }) => {
|
||||
const value = form.values.never_expires
|
||||
? "never"
|
||||
: form.values.expiration_num + form.values.expiration_unit;
|
||||
if (value === "never") return "This share will never expire.";
|
||||
|
||||
const expirationDate = moment()
|
||||
.add(
|
||||
value.split("-")[0],
|
||||
value.split("-")[1] as moment.unitOfTime.DurationConstructor
|
||||
)
|
||||
.toDate();
|
||||
|
||||
return `This share will expire on ${moment(expirationDate).format("LLL")}`;
|
||||
};
|
||||
|
||||
export default ExpirationPreview;
|
||||
@@ -2,7 +2,7 @@ import { ActionIcon, Table } from "@mantine/core";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbTrash } from "react-icons/tb";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
import { byteStringToHumanSizeString } from "../../utils/math/byteStringToHumanSizeString.util";
|
||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
import UploadProgressIndicator from "./UploadProgressIndicator";
|
||||
|
||||
const FileList = ({
|
||||
@@ -19,7 +19,7 @@ const FileList = ({
|
||||
const rows = files.map((file, i) => (
|
||||
<tr key={i}>
|
||||
<td>{file.name}</td>
|
||||
<td>{byteStringToHumanSizeString(file.size.toString())}</td>
|
||||
<td>{byteToHumanSizeString(file.size)}</td>
|
||||
<td>
|
||||
{file.uploadingProgress == 0 ? (
|
||||
<ActionIcon
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Checkbox,
|
||||
Col,
|
||||
Grid,
|
||||
Group,
|
||||
MultiSelect,
|
||||
NumberInput,
|
||||
PasswordInput,
|
||||
@@ -24,12 +23,13 @@ import { TbAlertCircle } from "react-icons/tb";
|
||||
import * as yup from "yup";
|
||||
import shareService from "../../../services/share.service";
|
||||
import { CreateShare } from "../../../types/share.type";
|
||||
import ExpirationPreview from "../ExpirationPreview";
|
||||
import { getExpirationPreview } from "../../../utils/date.util";
|
||||
|
||||
const showCreateUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
options: {
|
||||
isUserSignedIn: boolean;
|
||||
isReverseShare: boolean;
|
||||
appUrl: string;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
@@ -54,6 +54,7 @@ const CreateUploadModalBody = ({
|
||||
uploadCallback: (createShare: CreateShare) => void;
|
||||
options: {
|
||||
isUserSignedIn: boolean;
|
||||
isReverseShare: boolean;
|
||||
appUrl: string;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
@@ -89,7 +90,7 @@ const CreateUploadModalBody = ({
|
||||
validate: yupResolver(validationSchema),
|
||||
});
|
||||
return (
|
||||
<Group>
|
||||
<>
|
||||
{showNotSignedInAlert && !options.isUserSignedIn && (
|
||||
<Alert
|
||||
withCloseButton
|
||||
@@ -161,72 +162,78 @@ const CreateUploadModalBody = ({
|
||||
{options.appUrl}/share/
|
||||
{form.values.link == "" ? "myAwesomeShare" : form.values.link}
|
||||
</Text>
|
||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||
<Col xs={6}>
|
||||
<NumberInput
|
||||
min={1}
|
||||
max={99999}
|
||||
precision={0}
|
||||
variant="filled"
|
||||
label="Expiration"
|
||||
placeholder="n"
|
||||
disabled={form.values.never_expires}
|
||||
{...form.getInputProps("expiration_num")}
|
||||
{!options.isReverseShare && (
|
||||
<>
|
||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||
<Col xs={6}>
|
||||
<NumberInput
|
||||
min={1}
|
||||
max={99999}
|
||||
precision={0}
|
||||
variant="filled"
|
||||
label="Expiration"
|
||||
placeholder="n"
|
||||
disabled={form.values.never_expires}
|
||||
{...form.getInputProps("expiration_num")}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<Select
|
||||
disabled={form.values.never_expires}
|
||||
{...form.getInputProps("expiration_unit")}
|
||||
data={[
|
||||
// Set the label to singular if the number is 1, else plural
|
||||
{
|
||||
value: "-minutes",
|
||||
label:
|
||||
"Minute" +
|
||||
(form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-hours",
|
||||
label:
|
||||
"Hour" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-days",
|
||||
label:
|
||||
"Day" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-weeks",
|
||||
label:
|
||||
"Week" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-months",
|
||||
label:
|
||||
"Month" +
|
||||
(form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-years",
|
||||
label:
|
||||
"Year" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
<Checkbox
|
||||
label="Never Expires"
|
||||
{...form.getInputProps("never_expires")}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={6}>
|
||||
<Select
|
||||
disabled={form.values.never_expires}
|
||||
{...form.getInputProps("expiration_unit")}
|
||||
data={[
|
||||
// Set the label to singular if the number is 1, else plural
|
||||
{
|
||||
value: "-minutes",
|
||||
label:
|
||||
"Minute" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-hours",
|
||||
label:
|
||||
"Hour" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-days",
|
||||
label: "Day" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-weeks",
|
||||
label:
|
||||
"Week" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-months",
|
||||
label:
|
||||
"Month" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
{
|
||||
value: "-years",
|
||||
label:
|
||||
"Year" + (form.values.expiration_num == 1 ? "" : "s"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
<Checkbox
|
||||
label="Never Expires"
|
||||
{...form.getInputProps("never_expires")}
|
||||
/>
|
||||
{/* Preview expiration date text */}
|
||||
<Text
|
||||
italic
|
||||
size="xs"
|
||||
sx={(theme) => ({
|
||||
color: theme.colors.gray[6],
|
||||
})}
|
||||
>
|
||||
{ExpirationPreview({ form })}
|
||||
</Text>
|
||||
<Text
|
||||
italic
|
||||
size="xs"
|
||||
sx={(theme) => ({
|
||||
color: theme.colors.gray[6],
|
||||
})}
|
||||
>
|
||||
{getExpirationPreview("share", form)}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Accordion>
|
||||
<Accordion.Item value="description" sx={{ borderBottom: "none" }}>
|
||||
<Accordion.Control>Description</Accordion.Control>
|
||||
@@ -296,7 +303,7 @@ const CreateUploadModalBody = ({
|
||||
<Button type="submit">Share</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
200
frontend/src/pages/account/reverseShares.tsx
Normal file
200
frontend/src/pages/account/reverseShares.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import moment from "moment";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbInfoCircle, TbLink, TbPlus, TbTrash } from "react-icons/tb";
|
||||
import showShareLinkModal from "../../components/account/showShareLinkModal";
|
||||
import Meta from "../../components/Meta";
|
||||
import showCreateReverseShareModal from "../../components/share/modals/showCreateReverseShareModal";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { MyReverseShare } from "../../types/share.type";
|
||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const MyShares = () => {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const router = useRouter();
|
||||
const config = useConfig();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const [reverseShares, setReverseShares] = useState<MyReverseShare[]>();
|
||||
|
||||
const getReverseShares = () => {
|
||||
shareService
|
||||
.getMyReverseShares()
|
||||
.then((shares) => setReverseShares(shares));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getReverseShares();
|
||||
}, []);
|
||||
|
||||
if (!user) {
|
||||
router.replace("/");
|
||||
} else {
|
||||
if (!reverseShares) return <LoadingOverlay visible />;
|
||||
return (
|
||||
<>
|
||||
<Meta title="My shares" />
|
||||
<Group position="apart" align="baseline" mb={20}>
|
||||
<Group align="center" spacing={3} mb={30}>
|
||||
<Title order={3}>My reverse shares</Title>
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
multiline
|
||||
width={220}
|
||||
label="A reverse share allows you to generate a unique URL for a single-use share for an external user."
|
||||
events={{ hover: true, focus: false, touch: true }}
|
||||
>
|
||||
<ActionIcon>
|
||||
<TbInfoCircle />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Button
|
||||
onClick={() =>
|
||||
showCreateReverseShareModal(
|
||||
modals,
|
||||
config.get("SMTP_ENABLED"),
|
||||
getReverseShares
|
||||
)
|
||||
}
|
||||
leftIcon={<TbPlus size={20} />}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Group>
|
||||
{reverseShares.length == 0 ? (
|
||||
<Center style={{ height: "70vh" }}>
|
||||
<Stack align="center" spacing={10}>
|
||||
<Title order={3}>It's empty here 👀</Title>
|
||||
<Text>You don't have any reverse shares.</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<Box sx={{ display: "block", overflowX: "auto" }}>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Visitors</th>
|
||||
<th>Max share size</th>
|
||||
<th>Expires at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{reverseShares.map((reverseShare) => (
|
||||
<tr key={reverseShare.id}>
|
||||
<td>
|
||||
{reverseShare.share ? (
|
||||
reverseShare.share?.id
|
||||
) : (
|
||||
<Text color="dimmed">No share created yet</Text>
|
||||
)}
|
||||
</td>
|
||||
<td>{reverseShare.share?.views ?? "0"}</td>
|
||||
<td>
|
||||
{byteToHumanSizeString(
|
||||
parseInt(reverseShare.maxShareSize)
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{moment(reverseShare.shareExpiration).unix() === 0
|
||||
? "Never"
|
||||
: moment(reverseShare.shareExpiration).format("LLL")}
|
||||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
{reverseShare.share && (
|
||||
<ActionIcon
|
||||
color="victoria"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("APP_URL")}/share/${
|
||||
reverseShare.share!.id
|
||||
}`
|
||||
);
|
||||
toast.success(
|
||||
"The share link was copied to the keyboard."
|
||||
);
|
||||
} else {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
reverseShare.share!.id,
|
||||
config.get("APP_URL")
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TbLink />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete reverse share`,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete this reverse
|
||||
share? If you do, the share will be deleted as
|
||||
well.
|
||||
</Text>
|
||||
),
|
||||
confirmProps: {
|
||||
color: "red",
|
||||
},
|
||||
labels: { confirm: "Confirm", cancel: "Cancel" },
|
||||
onConfirm: () => {
|
||||
shareService.removeReverseShare(
|
||||
reverseShare.id
|
||||
);
|
||||
setReverseShares(
|
||||
reverseShares.filter(
|
||||
(item) => item.id !== reverseShare.id
|
||||
)
|
||||
);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TbTrash />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default MyShares;
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Group,
|
||||
@@ -61,83 +62,85 @@ const MyShares = () => {
|
||||
</Stack>
|
||||
</Center>
|
||||
) : (
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Visitors</th>
|
||||
<th>Expires at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{shares.map((share) => (
|
||||
<tr key={share.id}>
|
||||
<td>{share.id}</td>
|
||||
<td>{share.views}</td>
|
||||
<td>
|
||||
{moment(share.expiration).unix() === 0
|
||||
? "Never"
|
||||
: moment(share.expiration).format("LLL")}
|
||||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<ActionIcon
|
||||
color="victoria"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("APP_URL")}/share/${share.id}`
|
||||
);
|
||||
toast.success(
|
||||
"Your link was copied to the keyboard."
|
||||
);
|
||||
} else {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
share.id,
|
||||
config.get("APP_URL")
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TbLink />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete share ${share.id}`,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete this share?
|
||||
</Text>
|
||||
),
|
||||
confirmProps: {
|
||||
color: "red",
|
||||
},
|
||||
labels: { confirm: "Confirm", cancel: "Cancel" },
|
||||
onConfirm: () => {
|
||||
shareService.remove(share.id);
|
||||
setShares(
|
||||
shares.filter((item) => item.id !== share.id)
|
||||
);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TbTrash />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</td>
|
||||
<Box sx={{ display: "block", overflowX: "auto" }}>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Visitors</th>
|
||||
<th>Expires at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{shares.map((share) => (
|
||||
<tr key={share.id}>
|
||||
<td>{share.id}</td>
|
||||
<td>{share.views}</td>
|
||||
<td>
|
||||
{moment(share.expiration).unix() === 0
|
||||
? "Never"
|
||||
: moment(share.expiration).format("LLL")}
|
||||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<ActionIcon
|
||||
color="victoria"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
if (window.isSecureContext) {
|
||||
clipboard.copy(
|
||||
`${config.get("APP_URL")}/share/${share.id}`
|
||||
);
|
||||
toast.success(
|
||||
"Your link was copied to the keyboard."
|
||||
);
|
||||
} else {
|
||||
showShareLinkModal(
|
||||
modals,
|
||||
share.id,
|
||||
config.get("APP_URL")
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<TbLink />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete share ${share.id}`,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Do you really want to delete this share?
|
||||
</Text>
|
||||
),
|
||||
confirmProps: {
|
||||
color: "red",
|
||||
},
|
||||
labels: { confirm: "Confirm", cancel: "Cancel" },
|
||||
onConfirm: () => {
|
||||
shareService.remove(share.id);
|
||||
setShares(
|
||||
shares.filter((item) => item.id !== share.id)
|
||||
);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TbTrash />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
43
frontend/src/pages/upload/[reverseShareToken].tsx
Normal file
43
frontend/src/pages/upload/[reverseShareToken].tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { LoadingOverlay } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { useEffect, useState } from "react";
|
||||
import Upload from ".";
|
||||
import showErrorModal from "../../components/share/showErrorModal";
|
||||
import shareService from "../../services/share.service";
|
||||
|
||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: { reverseShareToken: context.params!.reverseShareToken },
|
||||
};
|
||||
}
|
||||
|
||||
const Share = ({ reverseShareToken }: { reverseShareToken: string }) => {
|
||||
const modals = useModals();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [maxShareSize, setMaxShareSize] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
shareService
|
||||
.setReverseShare(reverseShareToken)
|
||||
.then((reverseShareTokenData) => {
|
||||
setMaxShareSize(parseInt(reverseShareTokenData.maxShareSize));
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
showErrorModal(
|
||||
modals,
|
||||
"Invalid Link",
|
||||
"This link is invalid. Please check your link."
|
||||
);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <LoadingOverlay visible />;
|
||||
|
||||
return <Upload isReverseShare maxShareSize={maxShareSize} />;
|
||||
};
|
||||
|
||||
export default Share;
|
||||
@@ -2,27 +2,34 @@ import { Button, Group } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { cleanNotifications } from "@mantine/notifications";
|
||||
import { AxiosError } from "axios";
|
||||
import { getCookie } from "cookies-next";
|
||||
import { useRouter } from "next/router";
|
||||
import pLimit from "p-limit";
|
||||
import { useEffect, useState } from "react";
|
||||
import Meta from "../components/Meta";
|
||||
import Dropzone from "../components/upload/Dropzone";
|
||||
import FileList from "../components/upload/FileList";
|
||||
import showCompletedUploadModal from "../components/upload/modals/showCompletedUploadModal";
|
||||
import showCreateUploadModal from "../components/upload/modals/showCreateUploadModal";
|
||||
import useConfig from "../hooks/config.hook";
|
||||
import useUser from "../hooks/user.hook";
|
||||
import shareService from "../services/share.service";
|
||||
import { FileUpload } from "../types/File.type";
|
||||
import { CreateShare, Share } from "../types/share.type";
|
||||
import toast from "../utils/toast.util";
|
||||
import Meta from "../../components/Meta";
|
||||
import Dropzone from "../../components/upload/Dropzone";
|
||||
import FileList from "../../components/upload/FileList";
|
||||
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
|
||||
import showCreateUploadModal from "../../components/upload/modals/showCreateUploadModal";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
import { CreateShare, Share } from "../../types/share.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const promiseLimit = pLimit(3);
|
||||
const chunkSize = 10 * 1024 * 1024; // 10MB
|
||||
let errorToastShown = false;
|
||||
let createdShare: Share;
|
||||
|
||||
const Upload = () => {
|
||||
const Upload = ({
|
||||
maxShareSize,
|
||||
isReverseShare = false,
|
||||
}: {
|
||||
maxShareSize?: number;
|
||||
isReverseShare: boolean;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const modals = useModals();
|
||||
|
||||
@@ -31,6 +38,8 @@ const Upload = () => {
|
||||
const [files, setFiles] = useState<FileUpload[]>([]);
|
||||
const [isUploading, setisUploading] = useState(false);
|
||||
|
||||
maxShareSize ??= parseInt(config.get("MAX_SHARE_SIZE"));
|
||||
|
||||
const uploadFiles = async (share: CreateShare) => {
|
||||
setisUploading(true);
|
||||
createdShare = await shareService.create(share);
|
||||
@@ -138,9 +147,9 @@ const Upload = () => {
|
||||
) {
|
||||
shareService
|
||||
.completeShare(createdShare.id)
|
||||
.then(() => {
|
||||
.then((share) => {
|
||||
setisUploading(false);
|
||||
showCompletedUploadModal(modals, createdShare, config.get("APP_URL"));
|
||||
showCompletedUploadModal(modals, share, config.get("APP_URL"));
|
||||
setFiles([]);
|
||||
})
|
||||
.catch(() =>
|
||||
@@ -149,8 +158,13 @@ const Upload = () => {
|
||||
}
|
||||
}, [files]);
|
||||
|
||||
if (!user && !config.get("ALLOW_UNAUTHENTICATED_SHARES")) {
|
||||
if (
|
||||
!user &&
|
||||
!config.get("ALLOW_UNAUTHENTICATED_SHARES") &&
|
||||
!getCookie("reverse_share_token")
|
||||
) {
|
||||
router.replace("/");
|
||||
return null;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
@@ -164,11 +178,14 @@ const Upload = () => {
|
||||
modals,
|
||||
{
|
||||
isUserSignedIn: user ? true : false,
|
||||
isReverseShare,
|
||||
appUrl: config.get("APP_URL"),
|
||||
allowUnauthenticatedShares: config.get(
|
||||
"ALLOW_UNAUTHENTICATED_SHARES"
|
||||
),
|
||||
enableEmailRecepients: config.get("ENABLE_EMAIL_RECIPIENTS"),
|
||||
enableEmailRecepients: config.get(
|
||||
"ENABLE_SHARE_EMAIL_RECIPIENTS"
|
||||
),
|
||||
},
|
||||
uploadFiles
|
||||
);
|
||||
@@ -177,7 +194,12 @@ const Upload = () => {
|
||||
Share
|
||||
</Button>
|
||||
</Group>
|
||||
<Dropzone files={files} setFiles={setFiles} isUploading={isUploading} />
|
||||
<Dropzone
|
||||
maxShareSize={maxShareSize}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
isUploading={isUploading}
|
||||
/>
|
||||
{files.length > 0 && <FileList files={files} setFiles={setFiles} />}
|
||||
</>
|
||||
);
|
||||
@@ -1,6 +1,8 @@
|
||||
import { setCookie } from "cookies-next";
|
||||
import { FileUploadResponse } from "../types/File.type";
|
||||
import {
|
||||
CreateShare,
|
||||
MyReverseShare,
|
||||
MyShare,
|
||||
Share,
|
||||
ShareMetaData,
|
||||
@@ -98,6 +100,34 @@ const uploadFile = async (
|
||||
).data;
|
||||
};
|
||||
|
||||
const createReverseShare = async (
|
||||
shareExpiration: string,
|
||||
maxShareSize: number,
|
||||
sendEmailNotification: boolean
|
||||
) => {
|
||||
return (
|
||||
await api.post("reverseShares", {
|
||||
shareExpiration,
|
||||
maxShareSize: maxShareSize.toString(),
|
||||
sendEmailNotification,
|
||||
})
|
||||
).data;
|
||||
};
|
||||
|
||||
const getMyReverseShares = async (): Promise<MyReverseShare[]> => {
|
||||
return (await api.get("reverseShares")).data;
|
||||
};
|
||||
|
||||
const setReverseShare = async (reverseShareToken: string) => {
|
||||
const { data } = await api.get(`/reverseShares/${reverseShareToken}`);
|
||||
setCookie("reverse_share_token", reverseShareToken);
|
||||
return data;
|
||||
};
|
||||
|
||||
const removeReverseShare = async (id: string) => {
|
||||
await api.delete(`/reverseShares/${id}`);
|
||||
};
|
||||
|
||||
export default {
|
||||
create,
|
||||
completeShare,
|
||||
@@ -109,4 +139,8 @@ export default {
|
||||
isShareIdAvailable,
|
||||
downloadFile,
|
||||
uploadFile,
|
||||
setReverseShare,
|
||||
createReverseShare,
|
||||
getMyReverseShares,
|
||||
removeReverseShare,
|
||||
};
|
||||
|
||||
@@ -26,6 +26,13 @@ export type MyShare = Share & {
|
||||
cratedAt: Date;
|
||||
};
|
||||
|
||||
export type MyReverseShare = {
|
||||
id: string;
|
||||
maxShareSize: string;
|
||||
shareExpiration: Date;
|
||||
share?: MyShare;
|
||||
};
|
||||
|
||||
export type ShareSecurity = {
|
||||
maxViews?: number;
|
||||
password?: string;
|
||||
|
||||
26
frontend/src/utils/date.util.ts
Normal file
26
frontend/src/utils/date.util.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import moment from "moment";
|
||||
|
||||
export const getExpirationPreview = (
|
||||
name: string,
|
||||
form: {
|
||||
values: {
|
||||
never_expires?: boolean;
|
||||
expiration_num: number;
|
||||
expiration_unit: string;
|
||||
};
|
||||
}
|
||||
) => {
|
||||
const value = form.values.never_expires
|
||||
? "never"
|
||||
: form.values.expiration_num + form.values.expiration_unit;
|
||||
if (value === "never") return `This ${name} will never expire.`;
|
||||
|
||||
const expirationDate = moment()
|
||||
.add(
|
||||
value.split("-")[0],
|
||||
value.split("-")[1] as moment.unitOfTime.DurationConstructor
|
||||
)
|
||||
.toDate();
|
||||
|
||||
return `This ${name} will expire on ${moment(expirationDate).format("LLL")}`;
|
||||
};
|
||||
23
frontend/src/utils/fileSize.util.ts
Normal file
23
frontend/src/utils/fileSize.util.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export function byteToHumanSizeString(bytes: number) {
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
if (bytes == 0) return "0 Byte";
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
|
||||
return (bytes / Math.pow(1024, i)).toFixed(1).toString() + " " + sizes[i];
|
||||
}
|
||||
|
||||
export function byteToUnitAndSize(bytes: number) {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
if (bytes == 0) return { unit: "B", size: 0 };
|
||||
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString());
|
||||
|
||||
return {
|
||||
size: parseFloat((bytes / Math.pow(1024, i)).toFixed(1)),
|
||||
unit: units[i],
|
||||
};
|
||||
}
|
||||
|
||||
export function unitAndSizeToByte(unit: string, size: number) {
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = units.indexOf(unit);
|
||||
return Math.pow(1024, i) * size;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export function byteStringToHumanSizeString(bytes: string) {
|
||||
const bytesNumber = parseInt(bytes);
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
if (bytesNumber == 0) return "0 Byte";
|
||||
const i = parseInt(
|
||||
Math.floor(Math.log(bytesNumber) / Math.log(1024)).toString()
|
||||
);
|
||||
return (
|
||||
(bytesNumber / Math.pow(1024, i)).toFixed(1).toString() + " " + sizes[i]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user