diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..c43b696 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,46 @@ +version: '3' +services: + postgres: + image: postgres + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DATABASE=postgres + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U postgres'] + interval: 10s + timeout: 5s + retries: 5 + + zipline: + build: + context: . + dockerfile: Dockerfile + ports: + - '3000:3000' + restart: unless-stopped + environment: + - SECURE=false + - SECRET=changethis + - HOST=0.0.0.0 + - PORT=3000 + - DATABASE_URL=postgresql://postgres:postgres@postgres/postgres/ + - UPLOADER_ROUTE=/u + - UPLOADER_EMBED_ROUTE=/a + - UPLOADER_LENGTH=6 + - UPLOADER_DIRECTORY=./uploads + - UPLOADER_ADMIN_LIMIT=104900000 + - UPLOADER_USER_LIMIT=104900000 + - UPLOADER_DISABLED_EXTS= + - URLS_ROUTE=/go + - URLS_LENGTH=6 + volumes: + - '$PWD/uploads:/zipline/uploads' + - '$PWD/public:/zipline/public' + depends_on: + - 'postgres' + +volumes: + pg_data: \ No newline at end of file diff --git a/next.config.js b/next.config.js index 5c08827..42911f6 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,11 @@ module.exports = { - reactStrictMode: true, + async redirects() { + return [ + { + source: '/', + destination: '/dashboard', + permanent: true, + }, + ]; + }, }; \ No newline at end of file diff --git a/package.json b/package.json index 62cb5ea..3c547ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zip3", - "version": "3.3.2", + "version": "3.4.0", "license": "MIT", "scripts": { "dev": "NODE_ENV=development node server", @@ -10,15 +10,21 @@ "migrate:dev": "prisma migrate dev --create-only", "start": "node server", "lint": "next lint", - "seed": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only prisma/seed.ts" + "seed": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only prisma/seed.ts", + "docker:run": "docker-compose up -d", + "docker:down": "docker-compose down", + "docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build" }, "dependencies": { - "@emotion/react": "^11.4.0", - "@emotion/styled": "^11.3.0", "@iarna/toml": "2.2.5", - "@mui/icons-material": "^5.0.0", - "@mui/material": "^5.0.2", - "@mui/styles": "^5.0.1", + "@mantine/core": "^3.6.9", + "@mantine/dropzone": "^3.6.9", + "@mantine/hooks": "^3.6.9", + "@mantine/modals": "^3.6.9", + "@mantine/next": "^3.6.9", + "@mantine/notifications": "^3.6.9", + "@mantine/prism": "^3.6.11", + "@modulz/radix-icons": "^4.0.0", "@prisma/client": "^3.9.2", "@prisma/migrate": "^3.9.2", "@prisma/sdk": "^3.9.2", @@ -26,17 +32,14 @@ "argon2": "^0.28.2", "colorette": "^1.2.2", "cookie": "^0.4.1", - "copy-to-clipboard": "^3.3.1", "fecha": "^4.2.1", - "formik": "^2.2.9", "multer": "^1.4.2", "next": "^12.1.0", "prisma": "^3.9.2", - "react": "17.0.2", - "react-color": "^2.19.3", - "react-dom": "17.0.2", - "react-dropzone": "^11.3.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", "react-redux": "^7.2.4", + "react-table": "^7.7.0", "redux": "^4.1.0", "redux-thunk": "^2.3.0", "uuid": "^8.3.2", @@ -46,6 +49,7 @@ "@types/cookie": "^0.4.0", "@types/multer": "^1.4.6", "@types/node": "^15.12.2", + "babel-plugin-import": "^1.13.3", "eslint": "^7.32.0", "eslint-config-next": "11.0.0", "npm-run-all": "^4.1.5", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2230c46..6a8f59f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,8 +13,7 @@ model User { password String token String administrator Boolean @default(false) - systemTheme String @default("dark_blue") - customTheme Theme? + systemTheme String @default("system") embedTitle String? embedColor String @default("#2f3136") embedSiteName String? @default("{image.file} • {user.name}") @@ -23,21 +22,6 @@ model User { urls Url[] } -model Theme { - id Int @id @default(autoincrement()) - type String - primary String - secondary String - error String - warning String - info String - border String - mainBackground String - paperBackground String - user User @relation(fields: [userId], references: [id]) - userId Int -} - enum ImageFormat { UUID DATE diff --git a/scripts/exts.js b/scripts/exts.js new file mode 100644 index 0000000..1f0554a --- /dev/null +++ b/scripts/exts.js @@ -0,0 +1,38 @@ +// https://github.com/toptal/haste-server/blob/master/static/application.js#L167-L174 +// Popular extension map +module.exports = { + rb: 'ruby', + py: 'python', + pl: 'perl', + php: 'php', + scala: 'scala', + go: 'go', + xml: 'xml', + html: 'xml', + htm: 'xml', + css: 'css', + js: 'javascript', + json: 'json', + vbs: 'vbscript', + lua: 'lua', + pas: 'delphi', + java: 'java', + cpp: 'cpp', + cc: 'cpp', + m: 'objectivec', + vala: 'vala', + sql: 'sql', + sm: 'smalltalk', + lisp: 'lisp', + ini: 'ini', + diff: 'diff', + bash: 'bash', + sh: 'bash', + tex: 'tex', + erl: 'erlang', + hs: 'haskell', + md: 'markdown', + txt: '', + coffee: 'coffee', + swift: 'swift', +}; \ No newline at end of file diff --git a/server/index.js b/server/index.js index 6b704da..0d3d541 100644 --- a/server/index.js +++ b/server/index.js @@ -1,18 +1,16 @@ const next = require('next').default; -const defaultConfig = require('next/dist/server/config-shared').defaultConfig; const { createServer } = require('http'); -const { stat, mkdir } = require('fs/promises'); +const { mkdir } = require('fs/promises'); const { extname } = require('path'); const validateConfig = require('./validateConfig'); const Logger = require('../src/lib/logger'); const readConfig = require('../src/lib/readConfig'); const mimes = require('../scripts/mimes'); -const { log, getStats, shouldUseYarn, getFile, migrations } = require('./util'); +const { log, getStats, getFile, migrations } = require('./util'); const { PrismaClient } = require('@prisma/client'); const { version } = require('../package.json'); -const nextConfig = require('../next.config'); +const exts = require('../scripts/exts'); const serverLog = Logger.get('server'); -const webLog = Logger.get('web'); serverLog.info(`starting zipline@${version} server`); @@ -42,7 +40,6 @@ async function run() { quiet: !dev, hostname: config.core.host, port: config.core.port, - conf: Object.assign(defaultConfig, nextConfig), }); await app.prepare(); @@ -51,15 +48,15 @@ async function run() { const prisma = new PrismaClient(); const srv = createServer(async (req, res) => { - const parts = req.url.split('/'); - if (!parts[2] || parts[2] === '') return; - if (req.url.startsWith('/r')) { + const parts = req.url.split('/'); + if (!parts[2] || parts[2] === '') return; + let image = await prisma.image.findFirst({ where: { OR: [ { file: parts[2] }, - { invisible: { invis: decodeURI(parts[2]) } }, + { invisible:{ invis: decodeURI(parts[2]) } }, ], }, select: { @@ -70,19 +67,17 @@ async function run() { }, }); - image && await prisma.image.update({ - where: { id: image.id }, - data: { views: { increment: 1 } }, - }); + if (!image) { + const data = await getFile(config.uploader.directory, parts[2]); + if (!data) return app.render404(req, res); - const data = await getFile(config.uploader.directory, parts[2]); - if (!data) return app.render404(req, res); - - if (!image) { // raw image const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream'; res.setHeader('Content-Type', mimetype); res.end(data); - } else { // raw image & update db + } else { + const data = await getFile(config.uploader.directory, image.file); + if (!data) return app.render404(req, res); + await prisma.image.update({ where: { id: image.id }, data: { views: { increment: 1 } }, @@ -91,14 +86,14 @@ async function run() { res.end(data); } } else if (req.url.startsWith(config.uploader.route)) { - const data = await getFile(config.uploader.directory, parts[2]); - if (!data) return app.render404(req, res); - + const parts = req.url.split('/'); + if (!parts[2] || parts[2] === '') return; + let image = await prisma.image.findFirst({ where: { OR: [ { file: parts[2] }, - { invisible: { invis: decodeURI(parts[2]) } }, + { invisible:{ invis: decodeURI(parts[2]) } }, ], }, select: { @@ -110,18 +105,25 @@ async function run() { }, }); - image && await prisma.image.update({ - where: { id: image.id }, - data: { views: { increment: 1 } }, - }); + if (!image) { + const data = await getFile(config.uploader.directory, parts[2]); + if (!data) return app.render404(req, res); - if (!image) { // raw image const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream'; res.setHeader('Content-Type', mimetype); res.end(data); - } else if (image.embed) { // embed image + } else if (image.embed) { handle(req, res); - } else { // raw image fallback + } else { + const ext = image.file.split('.').pop(); + if (Object.keys(exts).includes(ext)) return handle(req, res); + const data = await getFile(config.uploader.directory, image.file); + if (!data) return app.render404(req, res); + + await prisma.image.update({ + where: { id: image.id }, + data: { views: { increment: 1 } }, + }); res.setHeader('Content-Type', image.mimetype); res.end(data); } @@ -137,6 +139,7 @@ async function run() { process.exit(1); }); + srv.on('listening', () => { serverLog.info(`listening on ${config.core.host}:${config.core.port}`); }); diff --git a/server/validateConfig.js b/server/validateConfig.js index 0043e7d..1ae8471 100644 --- a/server/validateConfig.js +++ b/server/validateConfig.js @@ -7,7 +7,7 @@ const validator = object({ host: string().default('0.0.0.0'), port: number().default(3000), database_url: string().required(), - logger: boolean().default(true), + logger: boolean().default(false), stats_interval: number().default(1800), }).required(), uploader: object({ diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx deleted file mode 100644 index 4ecacfa..0000000 --- a/src/components/Alert.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { Snackbar, Alert as MuiAlert } from '@mui/material'; - -export default function Alert({ open, setOpen, severity, message }) { - return ( - setOpen(false)}> - - {message} - - - ); -} \ No newline at end of file diff --git a/src/components/Backdrop.tsx b/src/components/Backdrop.tsx index 472622d..05d886e 100644 --- a/src/components/Backdrop.tsx +++ b/src/components/Backdrop.tsx @@ -1,16 +1,8 @@ import React from 'react'; -import { - Backdrop as MuiBackdrop, - CircularProgress, -} from '@mui/material'; +import { LoadingOverlay } from '@mantine/core'; export default function Backdrop({ open }) { return ( - theme.zIndex.drawer + 1 }} - open={open} - > - - + ); } \ No newline at end of file diff --git a/src/components/Card.tsx b/src/components/Card.tsx index d035d8e..3c13897 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -1,19 +1,16 @@ import React from 'react'; import { - Card as MuiCard, - CardContent, - Typography, -} from '@mui/material'; + Card as MCard, + Title, +} from '@mantine/core'; export default function Card(props) { const { name, children, ...other } = props; return ( - - - {name} - {children} - - + + {name} + {children} + ); } \ No newline at end of file diff --git a/src/components/CenteredBox.tsx b/src/components/CenteredBox.tsx deleted file mode 100644 index 36378da..0000000 --- a/src/components/CenteredBox.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; - -export default function CenteredBox({ children, ...other }) { - return ( - - {children} - - ); -} \ No newline at end of file diff --git a/src/components/Image.tsx b/src/components/Image.tsx index 0af75f9..0a75c0a 100644 --- a/src/components/Image.tsx +++ b/src/components/Image.tsx @@ -1,83 +1,94 @@ import React, { useState } from 'react'; - -import { - Card, - CardMedia, - CardActionArea, - Button, - Dialog, - DialogTitle, - DialogActions, - DialogContent, -} from '@mui/material'; -import AudioIcon from '@mui/icons-material/Audiotrack'; -import copy from 'copy-to-clipboard'; import useFetch from 'hooks/useFetch'; +import { Button, Card, Group, Image as MImage, Modal, Title } from '@mantine/core'; +import { useNotifications } from '@mantine/notifications'; +import { CopyIcon, Cross1Icon, StarIcon, TrashIcon } from '@modulz/radix-icons'; +import { useClipboard } from '@mantine/hooks'; export default function Image({ image, updateImages }) { const [open, setOpen] = useState(false); const [t] = useState(image.mimetype.split('/')[0]); + const notif = useNotifications(); + const clipboard = useClipboard(); const handleDelete = async () => { const res = await useFetch('/api/user/files', 'DELETE', { id: image.id }); - if (!res.error) updateImages(true); + if (!res.error) { + updateImages(true); + notif.showNotification({ + title: 'Image Deleted', + message: '', + color: 'green', + icon: , + }); + } else { + notif.showNotification({ + title: 'Failed to delete image', + message: res.error, + color: 'red', + icon: , + }); + } setOpen(false); }; const handleCopy = () => { - copy(`${window.location.protocol}//${window.location.host}${image.url}`); + clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`); setOpen(false); + notif.showNotification({ + title: 'Copied to clipboard', + message: '', + icon: , + }); }; const handleFavorite = async () => { const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite }); if (!data.error) updateImages(true); + notif.showNotification({ + title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'), + message: '', + icon: , + }); }; const Type = (props) => { return { 'video':