From 912e439645244a2dc75bc4511bcc57db486e69cf Mon Sep 17 00:00:00 2001
From: dicedtomato <35403473+diced@users.noreply.github.com>
Date: Fri, 3 Mar 2023 20:40:28 -0800
Subject: [PATCH] feat: file size (#308)
* feat: baseline support for file sizes
* feat: script to add file sizes
---
package.json | 3 +-
.../20230226051016_file_size/migration.sql | 2 +
prisma/schema.prisma | 1 +
src/components/File.tsx | 398 ++++++++++++++++++
src/components/icons/HardDriveIcon.tsx | 5 +
src/components/icons/index.tsx | 2 +
src/pages/api/upload.ts | 1 +
src/pages/api/user/files.ts | 2 +
src/pages/api/user/paged.ts | 2 +
src/pages/api/user/recent.ts | 1 +
src/scripts/query-size.ts | 38 ++
tsconfig.json | 6 +-
tsup.config.ts | 5 +
13 files changed, 460 insertions(+), 6 deletions(-)
create mode 100644 prisma/migrations/20230226051016_file_size/migration.sql
create mode 100644 src/components/File.tsx
create mode 100644 src/components/icons/HardDriveIcon.tsx
create mode 100644 src/scripts/query-size.ts
diff --git a/package.json b/package.json
index 2a5436f..aa6775e 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,8 @@
"scripts:import-dir": "node --enable-source-maps dist/scripts/import-dir",
"scripts:list-users": "node --enable-source-maps dist/scripts/list-users",
"scripts:set-user": "node --enable-source-maps dist/scripts/set-user",
- "scripts:clear-zero-byte": "node --enable-source-maps dist/scripts/clear-zero-byte"
+ "scripts:clear-zero-byte": "node --enable-source-maps dist/scripts/clear-zero-byte",
+ "scripts:query-size": "node --enable-source-maps dist/scripts/query-size"
},
"dependencies": {
"@emotion/react": "^11.10.6",
diff --git a/prisma/migrations/20230226051016_file_size/migration.sql b/prisma/migrations/20230226051016_file_size/migration.sql
new file mode 100644
index 0000000..b11ec84
--- /dev/null
+++ b/prisma/migrations/20230226051016_file_size/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "File" ADD COLUMN "size" INTEGER NOT NULL DEFAULT 0;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 1017b3e..ebf9340 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -53,6 +53,7 @@ model File {
originalName String?
mimetype String @default("image/png")
createdAt DateTime @default(now())
+ size Int @default(0)
expiresAt DateTime?
maxViews Int?
views Int @default(0)
diff --git a/src/components/File.tsx b/src/components/File.tsx
new file mode 100644
index 0000000..1e9e06c
--- /dev/null
+++ b/src/components/File.tsx
@@ -0,0 +1,398 @@
+import {
+ ActionIcon,
+ Card,
+ Group,
+ LoadingOverlay,
+ Modal,
+ Select,
+ SimpleGrid,
+ Stack,
+ Text,
+ Title,
+ Tooltip,
+} from '@mantine/core';
+import { useClipboard } from '@mantine/hooks';
+import { showNotification } from '@mantine/notifications';
+import useFetch from 'hooks/useFetch';
+import { useFileDelete, useFileFavorite } from 'lib/queries/files';
+import { useFolders } from 'lib/queries/folders';
+import { bytesToHuman } from 'lib/utils/bytes';
+import { relativeTime } from 'lib/utils/client';
+import { useState } from 'react';
+import {
+ CalendarIcon,
+ ClockIcon,
+ CopyIcon,
+ CrossIcon,
+ DeleteIcon,
+ DownloadIcon,
+ ExternalLinkIcon,
+ EyeIcon,
+ HardDriveIcon,
+ FileIcon,
+ FolderMinusIcon,
+ FolderPlusIcon,
+ HashIcon,
+ ImageIcon,
+ InfoIcon,
+ StarIcon,
+} from './icons';
+import MutedText from './MutedText';
+import Type from './Type';
+
+export function FileMeta({ Icon, title, subtitle, ...other }) {
+ return other.tooltip ? (
+
+
+
+
+ {title}
+ {subtitle}
+
+
+
+ ) : (
+
+
+
+ {title}
+ {subtitle}
+
+
+ );
+}
+
+export default function File({
+ image,
+ disableMediaPreview,
+ exifEnabled,
+ refreshImages,
+ reducedActions = false,
+}) {
+ const [open, setOpen] = useState(false);
+ const [overrideRender, setOverrideRender] = useState(false);
+ const deleteFile = useFileDelete();
+ const favoriteFile = useFileFavorite();
+ const clipboard = useClipboard();
+
+ const folders = useFolders();
+
+ const loading = deleteFile.isLoading || favoriteFile.isLoading;
+
+ const handleDelete = async () => {
+ deleteFile.mutate(image.id, {
+ onSuccess: () => {
+ showNotification({
+ title: 'File Deleted',
+ message: '',
+ color: 'green',
+ icon: ,
+ });
+ },
+
+ onError: (res: any) => {
+ showNotification({
+ title: 'Failed to delete file',
+ message: res.error,
+ color: 'red',
+ icon: ,
+ });
+ },
+
+ onSettled: () => {
+ setOpen(false);
+ },
+ });
+ };
+
+ const handleCopy = () => {
+ clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
+ setOpen(false);
+ showNotification({
+ title: 'Copied to clipboard',
+ message: '',
+ icon: ,
+ });
+ };
+
+ const handleFavorite = async () => {
+ favoriteFile.mutate(
+ { id: image.id, favorite: !image.favorite },
+ {
+ onSuccess: () => {
+ showNotification({
+ title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
+ message: '',
+ icon: ,
+ });
+ },
+
+ onError: (res: any) => {
+ showNotification({
+ title: 'Failed to favorite file',
+ message: res.error,
+ color: 'red',
+ icon: ,
+ });
+ },
+ }
+ );
+ };
+
+ const inFolder = image.folderId;
+
+ const refresh = () => {
+ refreshImages();
+ folders.refetch();
+ };
+
+ const removeFromFolder = async () => {
+ const res = await useFetch('/api/user/folders/' + image.folderId, 'DELETE', {
+ file: Number(image.id),
+ });
+
+ refresh();
+
+ if (!res.error) {
+ showNotification({
+ title: 'Removed from folder',
+ message: res.name,
+ color: 'green',
+ icon: ,
+ });
+ } else {
+ showNotification({
+ title: 'Failed to remove from folder',
+ message: res.error,
+ color: 'red',
+ icon: ,
+ });
+ }
+ };
+
+ const addToFolder = async (t) => {
+ const res = await useFetch('/api/user/folders/' + t, 'POST', {
+ file: Number(image.id),
+ });
+
+ refresh();
+
+ if (!res.error) {
+ showNotification({
+ title: 'Added to folder',
+ message: res.name,
+ color: 'green',
+ icon: ,
+ });
+ } else {
+ showNotification({
+ title: 'Failed to add to folder',
+ message: res.error,
+ color: 'red',
+ icon: ,
+ });
+ }
+ };
+
+ const createFolder = (t) => {
+ useFetch('/api/user/folders', 'POST', {
+ name: t,
+ add: [Number(image.id)],
+ }).then((res) => {
+ refresh();
+
+ if (!res.error) {
+ showNotification({
+ title: 'Created & added to folder',
+ message: res.name,
+ color: 'green',
+ icon: ,
+ });
+ } else {
+ showNotification({
+ title: 'Failed to create folder',
+ message: res.error,
+ color: 'red',
+ icon: ,
+ });
+ }
+ });
+ return { value: t, label: t };
+ };
+
+ return (
+ <>
+ setOpen(false)} title={{image.name}} size='xl'>
+
+
+
+
+
+
+
+
+ {image.maxViews && (
+
+ )}
+
+ {image.expiresAt && !reducedActions && (
+
+ )}
+
+
+
+
+
+
+ {exifEnabled && !reducedActions && (
+
+ window.open(`/dashboard/metadata/${image.id}`, '_blank')}
+ >
+
+
+
+ )}
+ {reducedActions ? null : inFolder && !folders.isLoading ? (
+ f.id === image.folderId)?.name ?? ''
+ }"`}
+ >
+
+
+
+
+ ) : (
+
+
+ )}
+
+
+ {reducedActions ? null : (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ window.open(image.url, '_blank')}>
+
+
+
+
+
+
+
+
+
+
+
+ window.open(`/r/${encodeURI(image.name)}?download=true`, '_blank')}
+ >
+
+
+
+
+
+
+
+
+
+ setOpen(true)}
+ disableMediaPreview={disableMediaPreview}
+ />
+
+
+ >
+ );
+}
diff --git a/src/components/icons/HardDriveIcon.tsx b/src/components/icons/HardDriveIcon.tsx
new file mode 100644
index 0000000..5e924d3
--- /dev/null
+++ b/src/components/icons/HardDriveIcon.tsx
@@ -0,0 +1,5 @@
+import { HardDrive } from 'react-feather';
+
+export default function HardDriveIcon({ ...props }) {
+ return ;
+}
diff --git a/src/components/icons/index.tsx b/src/components/icons/index.tsx
index f9d4ae9..dec72ed 100644
--- a/src/components/icons/index.tsx
+++ b/src/components/icons/index.tsx
@@ -41,6 +41,7 @@ import FolderPlusIcon from './FolderPlusIcon';
import GlobeIcon from './GlobeIcon';
import LockIcon from './LockIcon';
import UnlockIcon from './UnlockIcon';
+import HardDriveIcon from './HardDriveIcon';
export {
ActivityIcon,
@@ -86,4 +87,5 @@ export {
GlobeIcon,
LockIcon,
UnlockIcon,
+ HardDriveIcon,
};
diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts
index 1e8d6bd..22e13c3 100644
--- a/src/pages/api/upload.ts
+++ b/src/pages/api/upload.ts
@@ -299,6 +299,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
expiresAt: expiry,
maxViews: fileMaxViews,
originalName: req.headers['original-name'] ? file.originalname ?? null : null,
+ size: file.size,
},
});
diff --git a/src/pages/api/user/files.ts b/src/pages/api/user/files.ts
index 6370ac2..dac2c08 100644
--- a/src/pages/api/user/files.ts
+++ b/src/pages/api/user/files.ts
@@ -82,6 +82,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
expiresAt: Date;
maxViews: number;
views: number;
+ size: number;
}[] = await prisma.file.findMany({
where: {
userId: user.id,
@@ -99,6 +100,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
favorite: true,
views: true,
maxViews: true,
+ size: true,
},
});
diff --git a/src/pages/api/user/paged.ts b/src/pages/api/user/paged.ts
index 2db584b..e418ab9 100644
--- a/src/pages/api/user/paged.ts
+++ b/src/pages/api/user/paged.ts
@@ -58,6 +58,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
maxViews: number;
views: number;
folderId: number;
+ size: number;
password: string | boolean;
}[] = await prisma.file.findMany({
where,
@@ -74,6 +75,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
views: true,
maxViews: true,
folderId: true,
+ size: true,
password: true,
},
skip: page ? (Number(page) - 1) * pageCount : undefined,
diff --git a/src/pages/api/user/recent.ts b/src/pages/api/user/recent.ts
index 9f3fc85..344fd01 100644
--- a/src/pages/api/user/recent.ts
+++ b/src/pages/api/user/recent.ts
@@ -25,6 +25,7 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
views: true,
maxViews: true,
folderId: true,
+ size: true,
},
});
diff --git a/src/scripts/query-size.ts b/src/scripts/query-size.ts
new file mode 100644
index 0000000..a13d1f7
--- /dev/null
+++ b/src/scripts/query-size.ts
@@ -0,0 +1,38 @@
+import { PrismaClient } from '@prisma/client';
+import config from 'lib/config';
+import datasource from 'lib/datasource';
+import { migrations } from 'server/util';
+
+async function main() {
+ process.env.DATABASE_URL = config.core.database_url;
+ await migrations();
+
+ const prisma = new PrismaClient();
+
+ const files = await prisma.file.findMany();
+
+ console.log(`The script will attempt to query the size of ${files.length} files.`);
+
+ for (let i = 0; i !== files.length; ++i) {
+ const file = files[i];
+ const size = await datasource.size(file.name);
+ if (size === 0) {
+ console.log(`File ${file.name} has a size of 0 bytes. Ignoring...`);
+ } else {
+ console.log(`File ${file.name} has a size of ${size} bytes. Updating...`);
+ await prisma.file.update({
+ where: {
+ id: file.id,
+ },
+ data: {
+ size,
+ },
+ });
+ }
+ }
+
+ console.log('Done.');
+ process.exit(0);
+}
+
+main();
diff --git a/tsconfig.json b/tsconfig.json
index 726cd7c..32adce9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -25,10 +25,6 @@
},
"incremental": true
},
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ],
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist", ".yarn", ".next"]
}
diff --git a/tsup.config.ts b/tsup.config.ts
index 33a2755..9d8b661 100644
--- a/tsup.config.ts
+++ b/tsup.config.ts
@@ -39,4 +39,9 @@ export default defineConfig([
outDir: 'dist/scripts',
...opts,
},
+ {
+ entryPoints: ['src/scripts/query-size.ts'],
+ outDir: 'dist/scripts',
+ ...opts,
+ },
]);