Compare commits

..

17 Commits

Author SHA1 Message Date
diced
4728258750 feat: v3.2.2 new file management & viewing 2021-09-12 21:17:27 -07:00
diced
ece3e16459 fix(api): fixed being able to override user (#98) 2021-09-08 20:56:47 -07:00
Nguyen Thanh Quang
9208dbe2f3 fix(api): fixed being able to override user (#98)
* fix(api): fixed being able to override user

* Update index.ts
2021-09-08 20:56:10 -07:00
diced
636de18642 feat(pages): add recent images to dashboard 2021-09-06 15:58:01 -07:00
diced
ee48456291 fix(api): change cookie max-age from 10m seconds to 1 week 2021-09-06 15:32:39 -07:00
diced
a06d5ffaed fix(api): sort images count and types count 2021-09-06 15:22:36 -07:00
diced
606821a2c0 fix(components): add skeleton placeholders to dashboard 2021-09-06 14:54:25 -07:00
diced
5c980c21e5 feat(api): allow bulk uploading (#97) 2021-09-05 11:23:52 -07:00
diced
771cc380df fix(api): maybe fix default user not being created 2021-09-05 09:14:19 -07:00
diced
38217870fe fix(api): maybe fix default user not being created 2021-09-05 09:13:13 -07:00
dicedtomato
5b82c96a43 Update README.md 2021-09-04 15:01:08 -07:00
diced
6f5f9869ad refactor(assets): update assets to v3 2021-09-04 14:42:38 -07:00
diced
b29bfeb8b1 fix(api): new way to handle non-embedded images to overwrite nextjs & supports discord now 2021-09-03 17:20:43 -07:00
diced
cb40559e49 fix(server): some changes 2021-09-03 17:03:47 -07:00
diced
90c72f7ffe fix(api): small images height strech fix 2021-09-03 16:50:14 -07:00
diced
002bd2e6f7 refactor(api): redirect non-embedded images to /r 2021-09-03 16:40:50 -07:00
diced
7b44f17a64 refactor(api): /raw -> /r 2021-09-03 16:26:04 -07:00
28 changed files with 310 additions and 246 deletions

View File

@@ -1,3 +1,5 @@
prisma/migrations
prisma
node_modules
.next
.next
uploads
.git

View File

@@ -1,25 +1,28 @@
<p align="center"><img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/></p>
![Version](https://img.shields.io/github/package-json/v/diced/zipline)
![LICENCE](https://img.shields.io/github/license/diced/zipline)
[![Discord](https://img.shields.io/discord/729771078196527176)](https://discord.gg/EAhCRfGxCF)
![Stars](https://img.shields.io/github/stars/diced/zipline)
![GitHub repo size](https://img.shields.io/github/repo-size/diced/zipline)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/diced/zipline/trunk)
<br>
# Zipline
Fast & lightweight file uploading.
# Features
<div align="center">
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
Zipline is a file sharing, URL sharing, lightweight and easy to use!
![Build](https://img.shields.io/github/workflow/status/diced/zipline/CD:%20Push%20Docker%20Images?logo=github&style=flat-square)
![Stars](https://img.shields.io/github/stars/diced/zipline?logo=github&style=flat-square)
![Version](https://img.shields.io/github/package-json/v/diced/zipline?logo=git&logoColor=white&style=flat-square)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/diced/zipline/trunk?logo=git&logoColor=white&style=flat-square)
[![Discord](https://img.shields.io/discord/729771078196527176?color=%23777ed3&label=discord&logo=discord&logoColor=white&style=flat-square)](https://discord.gg/EAhCRfGxCF)
</div>
## Features
- Configurable
- Fast
- Built with Next.js & React
- Token protected uploading
- Easy setup instructions on [docs](https://zipline.diced.me) (One command install `docker-compose up`)
# Installing
## Installing
[See how to install here](https://zipline.diced.me/get-started)
[See how to install here](https://zipline.diced.me/docs/getting-started)
## Configuration
[See how to configure here](https://zipline.diced.me/configuration/overview)
## Theming
[See how to theme here](https://zipline.diced.me/themes)

View File

@@ -47,7 +47,8 @@ module.exports = {
'middleware',
'redux',
'themes',
'lib'
'lib',
'assets'
],
],
},

View File

@@ -1,6 +1,6 @@
{
"name": "zip3",
"version": "3.2.1",
"version": "3.2.2",
"scripts": {
"prepare": "husky install",
"dev": "NODE_ENV=development node server",
@@ -17,10 +17,9 @@
"@emotion/styled": "^11.3.0",
"@iarna/toml": "2.2.5",
"@material-ui/core": "^5.0.0-alpha.37",
"@material-ui/data-grid": "^4.0.0-alpha.32",
"@material-ui/icons": "^5.0.0-alpha.37",
"@material-ui/styles": "^5.0.0-alpha.35",
"@prisma/client": "^2.30.3",
"@prisma/client": "^3.0.2",
"@reduxjs/toolkit": "^1.6.0",
"argon2": "^0.28.2",
"colorette": "^1.2.2",
@@ -30,7 +29,7 @@
"formik": "^2.2.9",
"multer": "^1.4.2",
"next": "11.1.1",
"prisma": "^2.30.3",
"prisma": "^3.0.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-dropzone": "^11.3.2",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -12,9 +12,9 @@ const prismaRun = require('../scripts/prisma-run');
const readConfig = require('../src/lib/readConfig');
const mimes = require('../scripts/mimes');
const deployDb = require('../scripts/deploy-db');
const { version } = require('../package.json');
Logger.get('server').info('starting zipline server');
Logger.get('server').info(`starting zipline@${version} server`);
const dev = process.env.NODE_ENV === 'development';
@@ -42,10 +42,11 @@ function shouldUseYarn() {
Logger.get('database').info('some migrations are not applied, applying them now...');
await deployDb(config);
Logger.get('database').info('finished applying migrations');
} else {
Logger.get('database').info('migrations up to date');
}
process.env.DATABASE_URL = config.core.database_url;
await stat('./.next');
await mkdir(config.uploader.directory, { recursive: true });
const app = next({
@@ -55,12 +56,13 @@ function shouldUseYarn() {
}, config.core.port, config.core.host);
await app.prepare();
await stat('./.next');
const handle = app.getRequestHandler();
const prisma = new PrismaClient();
const srv = createServer(async (req, res) => {
if (req.url.startsWith('/raw')) {
if (req.url.startsWith('/r')) {
const parts = req.url.split('/');
if (!parts[2] || parts[2] === '') return;
@@ -121,6 +123,7 @@ function shouldUseYarn() {
});
srv.on('listening', () => {
Logger.get('server').info(`listening on ${config.core.host}:${config.core.port}`);
if (process.platform === 'linux' && dev) execSync(`xdg-open ${config.core.secure ? 'https' : 'http'}://${config.core.host === '0.0.0.0' ? 'localhost' : config.core.host}:${config.core.port}`);
});
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');

View File

@@ -9,7 +9,7 @@ export default function Card(props) {
const { name, children, ...other } = props;
return (
<MuiCard sx={{ minWidth: 100 }} {...other}>
<MuiCard sx={{ minWidth: '100%' }} {...other}>
<CardContent>
<Typography variant='h3'>{name}</Typography>
{children}

View File

@@ -4,64 +4,80 @@ import {
Card,
CardMedia,
CardActionArea,
Popover,
Button,
ButtonGroup
Dialog,
DialogTitle,
DialogActions,
DialogContent
} from '@material-ui/core';
import AudioIcon from '@material-ui/icons/Audiotrack';
import copy from 'copy-to-clipboard';
import useFetch from '../lib/hooks/useFetch';
import useFetch from 'hooks/useFetch';
export default function Image({ image, updateImages }) {
const [anchorEl, setAnchorEl] = useState(null);
const [open, setOpen] = useState(false);
const [t,] = useState(image.mimetype.split('/')[0]);
const handleDelete = async () => {
const res = await useFetch('/api/user/images', 'DELETE', { id: image.id });
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
if (!res.error) updateImages(true);
setAnchorEl(null);
setOpen(false);
};
const handleCopy = () => {
copy(`${window.location.protocol}//${window.location.host}${image.url}`);
setAnchorEl(null);
setOpen(false);
};
const handleFavorite = async () => {
const data = await useFetch('/api/user/images', 'PATCH', { id: image.id, favorite: !image.favorite });
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
if (!data.error) updateImages(true);
};
const Type = (props) => {
return {
'video': <video controls {...props} />,
// eslint-disable-next-line jsx-a11y/alt-text
'image': <img {...props} />,
'audio': <audio controls {...props} />
}[t];
};
return (
<>
<Card sx={{ maxWidth: '100%' }}>
<CardActionArea>
<CardActionArea sx={t === 'audio' ? { justifyContent: 'center', display: 'flex', alignItems: 'center' } : {}}>
<CardMedia
sx={{ height: 320 }}
sx={{ height: 320, fontSize: 70, width: '100%' }}
image={image.url}
title={image.file}
onClick={e => setAnchorEl(e.currentTarget)}
component={t === 'audio' ? AudioIcon : t} // this is done because audio without controls is hidden
onClick={() => setOpen(true)}
/>
</CardActionArea>
</Card>
<Popover
open={Boolean(anchorEl)}
anchorEl={anchorEl}
onClose={() => setAnchorEl(null)}
anchorOrigin={{
vertical: 'center',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'center',
horizontal: 'center',
}}
<Dialog
open={open}
onClose={() => setOpen(false)}
>
<ButtonGroup variant='contained'>
<Button onClick={handleDelete} color='primary'>Delete</Button>
<Button onClick={handleCopy} color='primary'>Copy URL</Button>
<Button onClick={handleFavorite} color='primary'>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
</ButtonGroup>
</Popover>
<DialogTitle id='alert-dialog-title'>
{image.file}
</DialogTitle>
<DialogContent>
<Type
style={{ width: '100%' }}
src={image.url}
alt={image.url}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleDelete} color='inherit'>Delete</Button>
<Button onClick={handleCopy} color='inherit'>Copy URL</Button>
<Button onClick={handleFavorite} color='inherit'>{image.favorite ? 'Unfavorite' : 'Favorite'}</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,6 +1,5 @@
import React, { useState } from 'react';
import Link from 'next/link';
import useFetch from '../lib/hooks/useFetch';
import {
AppBar,
@@ -29,13 +28,13 @@ import {
Menu as MenuIcon,
Home as HomeIcon,
AccountCircle as AccountIcon,
Image as ImageIcon,
Folder as FolderIcon,
Upload as UploadIcon,
ContentCopy as CopyIcon,
Autorenew as ResetIcon,
Logout as LogoutIcon,
PeopleAlt as UsersIcon,
Brush as BrushIcon
Brush as BrushIcon,
} from '@material-ui/icons';
import copy from 'copy-to-clipboard';
import Backdrop from './Backdrop';
@@ -43,6 +42,7 @@ import { friendlyThemeName, themes } from './Theming';
import { useRouter } from 'next/router';
import { useStoreDispatch } from 'lib/redux/store';
import { updateUser } from 'lib/redux/reducers/user';
import useFetch from 'hooks/useFetch';
const items = [
{
@@ -51,9 +51,9 @@ const items = [
link: '/dashboard'
},
{
icon: <ImageIcon />,
text: 'Images',
link: '/dashboard/images'
icon: <FolderIcon />,
text: 'Files',
link: '/dashboard/files'
},
{
icon: <UploadIcon />,

View File

@@ -1,6 +1,8 @@
import React from 'react';
import { ThemeProvider } from '@emotion/react';
import { CssBaseline } from '@material-ui/core';
// themes
import dark_blue from 'lib/themes/dark_blue';
import dark from 'lib/themes/dark';
import ayu_dark from 'lib/themes/ayu_dark';
@@ -9,6 +11,7 @@ import ayu_light from 'lib/themes/ayu_light';
import nord from 'lib/themes/nord';
import polar from 'lib/themes/polar';
import dracula from 'lib/themes/dracula';
import { useStoreSelector } from 'lib/redux/store';
import createTheme from 'lib/themes';

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';
import {
Paper,
Table,
TableBody,
TableCell,
@@ -11,12 +10,16 @@ import {
Button,
ButtonGroup,
Typography,
Grid
Grid,
Skeleton,
CardActionArea,
CardMedia,
Card as MuiCard
} from '@material-ui/core';
import AudioIcon from '@material-ui/icons/Audiotrack';
import Link from 'components/Link';
import Card from 'components/Card';
import Backdrop from 'components/Backdrop';
import useFetch from 'lib/hooks/useFetch';
import { useStoreSelector } from 'lib/redux/store';
@@ -59,18 +62,19 @@ function StatTable({ rows, columns }) {
<TableHead>
<TableRow>
{columns.map(col => (
<TableCell key={col.name}>{col.name}</TableCell>
<TableCell key={col.name} sx={{ borderColor: t => t.palette.divider }}>{col.name}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map((row, i) => (
{rows.map(row => (
<TableRow
hover
key={row.username}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
{columns.map(col => (
<TableCell key={col.id}>
<TableCell key={col.id} sx={{ borderColor: t => t.palette.divider }}>
{col.format ? col.format(row[col.id]) : row[col.id]}
</TableCell>
))}
@@ -86,33 +90,31 @@ export default function Dashboard() {
const user = useStoreSelector(state => state.user);
const [images, setImages] = useState([]);
const [recent, setRecent] = useState([]);
const [page, setPage] = useState(0);
const [stats, setStats] = useState(null);
const [apiLoading, setApiLoading] = useState(true);
const [rowsPerPage, setRowsPerPage] = useState(10);
const updateImages = async () => {
setApiLoading(true);
const imgs = await useFetch('/api/user/images');
const imgs = await useFetch('/api/user/files');
const recent = await useFetch('/api/user/recent?filter=media');
const stts = await useFetch('/api/stats');
setImages(imgs);
setStats(stts);
setApiLoading(false);
setRecent(recent);
};
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
const handleChangeRowsPerPage = event => {
setRowsPerPage(+event.target.value);
setPage(0);
};
const handleDelete = async image => {
const res = await useFetch('/api/user/images', 'DELETE', { id: image.id });
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
if (!res.error) updateImages();
};
@@ -122,70 +124,88 @@ export default function Dashboard() {
return (
<>
<Backdrop open={apiLoading} />
<Typography variant='h4'>Welcome back {user?.username}</Typography>
<Typography color='GrayText' pb={2}>You have <b>{images.length}</b> images</Typography>
<Typography color='GrayText' pb={2}>You have <b>{images.length ? images.length : '...'}</b> images</Typography>
<Typography variant='h4'>Recent Images</Typography>
<Grid container spacing={4} py={2}>
{recent.length ? recent.map(image => (
<Grid item xs={12} sm={3} key={image.id}>
<MuiCard sx={{ minWidth: '100%' }}>
<CardActionArea>
<CardMedia
sx={{ height: 220 }}
image={image.url}
title={image.file}
controls
component={image.mimetype.split('/')[0] === 'audio' ? AudioIcon : image.mimetype.split('/')[0]} // this is done because audio without controls is hidden
/>
</CardActionArea>
</MuiCard>
</Grid>
)) : [1,2,3,4].map(x => (
<Grid item xs={12} sm={3} key={x}>
<Skeleton variant='rectangular' width='100%' height={220} sx={{ borderRadius: 1 }}/>
</Grid>
))}
</Grid>
<Typography variant='h4'>Stats</Typography>
{stats && (
<Grid container spacing={4} py={2}>
<Grid item xs={12} sm={4}>
<Card name='Size' sx={{ height: '100%' }}>
<StatText>{stats.size}</StatText>
<Typography variant='h3'>Average Size</Typography>
<StatText>{bytesToRead(stats.size_num / stats.count)}</StatText>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Card name='Images' sx={{ height: '100%' }}>
<StatText>{stats.count}</StatText>
<Typography variant='h3'>Views</Typography>
<StatText>{stats.views_count} ({isNaN(stats.views_count / stats.count) ? '0' : stats.views_count / stats.count})</StatText>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Card name='Users' sx={{ height: '100%' }}>
<StatText>{stats.count_users}</StatText>
</Card>
</Grid>
<Grid container spacing={4} py={2}>
<Grid item xs={12} sm={4}>
<Card name='Size' sx={{ height: '100%' }}>
<StatText>{stats ? stats.size : <Skeleton variant='text' />}</StatText>
<Typography variant='h3'>Average Size</Typography>
<StatText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton variant='text' />}</StatText>
</Card>
</Grid>
)}
<Grid item xs={12} sm={4}>
<Card name='Images' sx={{ height: '100%' }}>
<StatText>{stats ? stats.count : <Skeleton variant='text' />}</StatText>
<Typography variant='h3'>Views</Typography>
<StatText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? '0' : stats.views_count / stats.count})` : <Skeleton variant='text' />}</StatText>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Card name='Users' sx={{ height: '100%' }}>
<StatText>{stats ? stats.count_users : <Skeleton variant='text' />}</StatText>
</Card>
</Grid>
</Grid>
<Card name='Images' sx={{ my: 2 }} elevation={0} variant='outlined'>
<Link href='/dashboard/images' pb={2}>View Gallery</Link>
<TableContainer sx={{ maxHeight: 440 }}>
<Table size='small'>
<TableHead>
<TableRow>
{columns.map((column) => (
{columns.map(column => (
<TableCell
key={column.id}
align={column.align}
sx={{ minWidth: column.minWidth }}
sx={{ minWidth: column.minWidth, borderColor: t => t.palette.divider }}
>
{column.label}
</TableCell>
))}
<TableCell sx={{ minWidth: 200 }} align='right'>
Actions
<TableCell sx={{ minWidth: 200, borderColor: t => t.palette.divider }} align='right'>
Actions
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{images
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row) => {
.map(row => {
return (
<TableRow hover role='checkbox' tabIndex={-1} key={row.id}>
{columns.map((column) => {
{columns.map(column => {
const value = row[column.id];
return (
<TableCell key={column.id} align={column.align}>
<TableCell key={column.id} align={column.align} sx={{ borderColor: t => t.palette.divider }}>
{column.format ? column.format(value) : value}
</TableCell>
);
})}
<TableCell align='right'>
<TableCell align='right' sx={{ borderColor: t => t.palette.divider }}>
<ButtonGroup variant='outlined'>
<Button onClick={() => handleDelete(row)} color='error' size='small'>Delete</Button>
</ButtonGroup>
@@ -203,32 +223,24 @@ export default function Dashboard() {
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
onRowsPerPageChange={handleChangeRowsPerPage} />
</Card>
<Card name='Images per User' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
<StatTable
columns={[
{ id: 'username', name: 'Name' },
{ id: 'count', name: 'Images' }
]}
rows={stats ? stats.count_by_user : []} />
</Card>
<Card name='Types' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
<StatTable
columns={[
{ id: 'mimetype', name: 'Type' },
{ id: 'count', name: 'Count' }
]}
rows={stats ? stats.types_count : []} />
</Card>
{stats && (
<>
<Card name='Images per User' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
<StatTable
columns={[
{ id: 'username', name: 'Name' },
{ id: 'count', name: 'Images' }
]}
rows={stats.count_by_user}
/>
</Card>
<Card name='Types' sx={{ height: '100%', my: 2 }} elevation={0} variant='outlined'>
<StatTable
columns={[
{ id: 'mimetype', name: 'Type' },
{ id: 'count', name: 'Count' }
]}
rows={stats.types_count}
/>
</Card>
</>
)}
</>
);
}

View File

@@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react';
import { Grid, Pagination, Box, Typography, Accordion, AccordionSummary, AccordionDetails } from '@material-ui/core';
import { ExpandMore } from '@material-ui/icons';
import Backdrop from 'components/Backdrop';
import ZiplineImage from 'components/Image';
import useFetch from 'hooks/useFetch';
import { ExpandMore } from '@material-ui/icons';
export default function Upload() {
export default function Files() {
const [pages, setPages] = useState([]);
const [page, setPage] = useState(1);
const [favoritePages, setFavoritePages] = useState([]);
@@ -15,9 +15,9 @@ export default function Upload() {
const updatePages = async favorite => {
setLoading(true);
const pages = await useFetch('/api/user/images?paged=true&filter=image');
const pages = await useFetch('/api/user/files?paged=true&filter=media');
if (favorite) {
const fPages = await useFetch('/api/user/images?paged=true&favorite=true');
const fPages = await useFetch('/api/user/files?paged=true&favorite=media');
setFavoritePages(fPages);
}
setPages(pages);
@@ -39,13 +39,13 @@ export default function Upload() {
pt={2}
pb={3}
>
<Typography variant='h4'>No Images</Typography>
<Typography variant='h4'>No Files</Typography>
</Box>
) : <Typography variant='h4'>Images</Typography>}
) : <Typography variant='h4'>Files</Typography>}
{favoritePages.length ? (
<Accordion sx={{ my: 2, border: 1, borderColor: t => t.palette.divider }} elevation={0}>
<AccordionSummary expandIcon={<ExpandMore />}>
<Typography variant='h4'>Favorite Images</Typography>
<Typography variant='h4'>Favorite Files</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={2}>

View File

@@ -167,7 +167,6 @@ export default function Manage() {
validationSchema: themeValidationSchema,
onSubmit: async values => {
setLoading(true);
const newUser = await useFetch('/api/user', 'PATCH', { customTheme: values });
if (newUser.error) {

View File

@@ -8,19 +8,21 @@ import Alert from 'components/Alert';
import { useStoreSelector } from 'lib/redux/store';
import CenteredBox from 'components/CenteredBox';
import copy from 'copy-to-clipboard';
import Link from 'components/Link';
export default function Upload({ route }) {
const user = useStoreSelector(state => state.user);
const [file, setFile] = useState(null);
const [files, setFiles] = useState([]);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [severity, setSeverity] = useState('success');
const [message, setMessage] = useState('Saved');
const handleUpload = async () => {
const body = new FormData();
body.append('file', file);
for (let i = 0; i !== files.length; ++i) body.append('file', files[i]);
setLoading(true);
const res = await fetch('/api/upload', {
@@ -34,8 +36,11 @@ export default function Upload({ route }) {
if (res.ok && json.error === undefined) {
setOpen(true);
setSeverity('success');
setMessage(`Copied to clipboard! ${json.url}`);
//@ts-ignore
setMessage(<>Copied first image to clipboard! <br/>{json.files.map(x => (<Link key={x} href={x}>{x}<br/></Link>))}</>);
copy(json.url);
setFiles([]);
} else {
setOpen(true);
setSeverity('error');
@@ -50,7 +55,7 @@ export default function Upload({ route }) {
<Alert open={open} setOpen={setOpen} message={message} severity={severity} />
<Typography variant='h4' pb={2}>Upload file</Typography>
<Dropzone onDrop={acceptedFiles => setFile(acceptedFiles[0])}>
<Dropzone onDrop={acceptedFiles => setFiles([...files, ...acceptedFiles])}>
{({getRootProps, getInputProps}) => (
<CardActionArea>
<Paper
@@ -67,7 +72,9 @@ export default function Upload({ route }) {
<input {...getInputProps()} />
<CenteredBox><UploadIcon sx={{ fontSize: 100 }} /></CenteredBox>
<CenteredBox><Typography variant='h5'>Drag an image or click to upload an image.</Typography></CenteredBox>
<CenteredBox><Typography variant='h6'>{file && file.name}</Typography></CenteredBox>
{files.map(file => (
<CenteredBox key={file.name}><Typography variant='h6'>{file.name}</Typography></CenteredBox>
))}
</Paper>
</CardActionArea>
)}

View File

@@ -30,7 +30,7 @@ export type NextApiReq = NextApiRequest & {
} | null | void>;
getCookie: (name: string) => string | null;
cleanCookie: (name: string) => void;
file?: NextApiFile;
files?: NextApiFile[];
}
export type NextApiRes = NextApiResponse & {

View File

@@ -48,7 +48,7 @@ export default function createTheme(o: ThemeOptions) {
backgroundColor: o.border
}
}
}
}
},
},
});
}

View File

@@ -19,9 +19,6 @@ export interface ConfigUploader {
// The route uploads will be served on
route: string;
// The route embedded routes will be served on
embed_route: string;
// Length of random chars to generate for file names
length: number;

View File

@@ -4,6 +4,7 @@ import { GetServerSideProps } from 'next';
import { Box } from '@material-ui/core';
import config from 'lib/config';
import prisma from 'lib/prisma';
import getFile from '../../server/static';
export default function EmbeddedImage({ image, title, username, color, normal, embed }) {
const dataURL = (route: string) => `${route}/${image.file}`;
@@ -12,19 +13,13 @@ export default function EmbeddedImage({ image, title, username, color, normal, e
const imageEl = document.getElementById('image_content') as HTMLImageElement;
const original = new Image;
original.src = dataURL('/raw');
original.src = dataURL('/r');
if (original.width > innerWidth) {
imageEl.width = Math.floor(original.width * Math.min((innerHeight / original.height), (innerWidth / original.width)));
imageEl.height = innerHeight;
} else {
imageEl.width = original.width;
imageEl.height = original.height;
}
if (original.width > innerWidth) imageEl.width = Math.floor(original.width * Math.min((innerHeight / original.height), (innerWidth / original.width)));
else imageEl.width = original.width;
};
if (typeof window !== 'undefined') window.onresize = () => updateImage();
useEffect(() => updateImage(), []);
return (
@@ -44,7 +39,7 @@ export default function EmbeddedImage({ image, title, username, color, normal, e
<meta property='og:url' content={dataURL(normal)} />
</>
)}
<meta property='og:image' content={dataURL('/raw')} />
<meta property='og:image' content={dataURL('/r')} />
<meta property='twitter:card' content='summary_large_image' />
<title>{image.file}</title>
</Head>
@@ -54,7 +49,7 @@ export default function EmbeddedImage({ image, title, username, color, normal, e
alignItems='center'
minHeight='100vh'
>
<img src={dataURL('/raw')} alt={dataURL('/raw')} id='image_content' />
<img src={dataURL('/r')} alt={dataURL('/r')} id='image_content' />
</Box>
</>
);
@@ -86,6 +81,14 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
if (!image) return { notFound: true };
if (!image.embed) {
const data = await getFile(config.uploader.directory, id);
if (!data) return { notFound: true };
context.res.end(data);
return { props: {} };
};
const user = await prisma.user.findFirst({
select: {
embedTitle: true,
@@ -97,11 +100,12 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
}
});
if (!image.mimetype.startsWith('image')) return {
redirect: {
permanent: true,
destination: `raw/${image.file}`,
}
if (!image.mimetype.startsWith('image')) {
const data = await getFile(config.uploader.directory, id);
if (!data) return { notFound: true };
context.res.end(data);
return { props: {} };
};
return {

View File

@@ -1,9 +1,7 @@
import prisma from 'lib/prisma';
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
import { checkPassword } from 'lib/util';
import { checkPassword, createToken, hashPassword } from 'lib/util';
import Logger from 'lib/logger';
import prismaRun from '../../../../scripts/prisma-run';
import config from 'lib/config';
async function handler(req: NextApiReq, res: NextApiRes) {
if (req.method !== 'POST') return res.status(405).end();
@@ -11,7 +9,16 @@ async function handler(req: NextApiReq, res: NextApiRes) {
const users = await prisma.user.findMany();
if (users.length === 0) {
await prismaRun(config.core.database_url, ['db', 'seed', '--preview-feature']);
Logger.get('database').info('no users found... creating default user...');
await prisma.user.create({
data: {
username: 'administrator',
password: await hashPassword('password'),
token: createToken(),
administrator: true
}
});
Logger.get('database').info('created default user:\nUsername: "administrator"\nPassword: "password"');
}
const user = await prisma.user.findFirst({
@@ -25,7 +32,8 @@ async function handler(req: NextApiReq, res: NextApiRes) {
const valid = await checkPassword(password, user.password);
if (!valid) return res.forbid('Wrong password');
res.setCookie('user', user.id, { sameSite: true, maxAge: 10000000, path: '/' });
// 604800 seconds is 1 week
res.setCookie('user', user.id, { sameSite: true, maxAge: 604800, path: '/' });
Logger.get('user').info(`User ${user.username} (${user.id}) logged in`);

View File

@@ -43,21 +43,19 @@ async function handler(req: NextApiReq, res: NextApiRes) {
by: ['mimetype'],
_count: {
mimetype: true
}
},
});
const types_count = [];
for (let i = 0, L = typesCount.length; i !== L; ++i) {
types_count.push({ mimetype: typesCount[i].mimetype, count: typesCount[i]._count.mimetype });
}
for (let i = 0, L = typesCount.length; i !== L; ++i) types_count.push({ mimetype: typesCount[i].mimetype, count: typesCount[i]._count.mimetype });
return res.json({
size: bytesToRead(size),
size_num: size,
count,
count_by_user,
count_by_user: count_by_user.sort((a,b) => b.count-a.count),
count_users,
views_count: (viewsCount[0]?._sum?.views ?? 0),
types_count
types_count: types_count.sort((a,b) => b.count-a.count)
});
}

View File

@@ -20,33 +20,41 @@ async function handler(req: NextApiReq, res: NextApiRes) {
token: req.headers.authorization
}
});
if (!user) return res.forbid('authorization incorect');
if (!req.file) return res.error('no file');
if (req.file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit']) return res.error('file size too big');
if (!req.files) return res.error('no files');
if (req.files && req.files.length === 0) return res.error('no files');
const ext = req.file.originalname.split('.').pop();
if (zconfig.uploader.disabled_extentions.includes(ext)) return res.error('disabled extension recieved: ' + ext);
const rand = randomChars(zconfig.uploader.length);
const files = [];
let invis;
const image = await prisma.image.create({
data: {
file: `${rand}.${ext}`,
mimetype: req.file.mimetype,
userId: user.id,
embed: !!req.headers.embed
}
});
if (req.headers.zws) invis = await createInvis(zconfig.uploader.length, image.id);
for (let i = 0; i !== req.files.length; ++i) {
const file = req.files[i];
if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit']) return res.error('file size too big');
await writeFile(join(process.cwd(), zconfig.uploader.directory, image.file), req.file.buffer);
const ext = file.originalname.split('.').pop();
if (zconfig.uploader.disabled_extentions.includes(ext)) return res.error('disabled extension recieved: ' + ext);
const rand = randomChars(zconfig.uploader.length);
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
let invis;
const image = await prisma.image.create({
data: {
file: `${rand}.${ext}`,
mimetype: file.mimetype,
userId: user.id,
embed: !!req.headers.embed
}
});
if (req.headers.zws) invis = await createInvis(zconfig.uploader.length, image.id);
return res.json({
url: `${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route}/${invis ? invis.invis : image.file}`
});
await writeFile(join(process.cwd(), zconfig.uploader.directory, image.file), file.buffer);
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
files.push(`${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
}
// url will be deprecated soon
return res.json({ files, url: files[0] });
}
function run(middleware: any) {
@@ -60,7 +68,7 @@ function run(middleware: any) {
}
export default async function handlers(req, res) {
await run(uploader.single('file'))(req, res);
await run(uploader.array('file'))(req, res);
return withZipline(handler)(req, res);
};

View File

@@ -54,8 +54,8 @@ async function handler(req: NextApiReq, res: NextApiRes) {
// @ts-ignore
images.map(image => image.url = `/raw/${image.file}`);
if (req.query.filter && req.query.filter === 'image') images = images.filter(x => x.mimetype.startsWith('image'));
images.map(image => image.url = `/r/${image.file}`);
if (req.query.filter && req.query.filter === 'media') images = images.filter(x => /^(video|audio|image)/.test(x.mimetype));
return res.json(req.query.paged ? chunk(images, 16) : images);
}

View File

@@ -16,10 +16,20 @@ async function handler(req: NextApiReq, res: NextApiRes) {
});
}
if (req.body.username) await prisma.user.update({
where: { id: user.id },
data: { username: req.body.username }
});
if (req.body.username) {
const existing = await prisma.user.findFirst({
where: {
username: req.body.username
}
});
if (existing && user.username !== req.body.username) {
return res.forbid('Username is already taken');
}
await prisma.user.update({
where: { id: user.id },
data: { username: req.body.username }
});
}
if (req.body.embedTitle) await prisma.user.update({
where: { id: user.id },
@@ -82,4 +92,4 @@ async function handler(req: NextApiReq, res: NextApiRes) {
}
}
export default withZipline(handler);
export default withZipline(handler);

View File

@@ -5,11 +5,11 @@ async function handler(req: NextApiReq, res: NextApiRes) {
const user = await req.user();
if (!user) return res.forbid('not logged in');
const take = Number(req.query.take ?? 3);
const take = Number(req.query.take ?? 4);
if (take > 50) return res.error('take can\'t be more than 50');
const images = await prisma.image.findMany({
let images = await prisma.image.findMany({
take,
orderBy: {
created_at: 'desc'
@@ -21,6 +21,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
}
});
// @ts-ignore
images.map(image => image.url = `/r/${image.file}`);
if (req.query.filter && req.query.filter === 'media') images = images.filter(x => /^(video|audio|image)/.test(x.mimetype));
return res.json(images);
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import useLogin from 'hooks/useLogin';
import Layout from 'components/Layout';
import Images from 'components/pages/Images';
import Files from 'components/pages/Files';
export default function ImagesPage() {
const { user, loading } = useLogin();
@@ -14,7 +14,7 @@ export default function ImagesPage() {
loading={loading}
noPaper={false}
>
<Images />
<Files />
</Layout>
);
}

View File

@@ -399,16 +399,6 @@
react-is "^17.0.0"
react-transition-group "^4.4.0"
"@material-ui/data-grid@^4.0.0-alpha.32":
version "4.0.0-alpha.32"
resolved "https://registry.yarnpkg.com/@material-ui/data-grid/-/data-grid-4.0.0-alpha.32.tgz#72952d1dea9a9440a02827dd66996a8e94d3f28f"
integrity sha512-yEmQ8OGGHCB9fUx6f6/ncIxAQpH/U/295EqqocCsbIjLJA1rUYF5eseo2/jnW1Wd69o3aTsXozdlKV8tQNit2Q==
dependencies:
"@material-ui/utils" "^5.0.0-alpha.14"
clsx "^1.0.4"
prop-types "^15.7.2"
reselect "^4.0.0"
"@material-ui/icons@^5.0.0-alpha.37":
version "5.0.0-alpha.37"
resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-5.0.0-alpha.37.tgz#8579986e0a3b4586dc7fbf23234d8bfe909b6c3c"
@@ -487,7 +477,7 @@
prop-types "^15.7.2"
react-is "^17.0.0"
"@material-ui/utils@5.0.0-alpha.35", "@material-ui/utils@^5.0.0-alpha.14":
"@material-ui/utils@5.0.0-alpha.35":
version "5.0.0-alpha.35"
resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-5.0.0-alpha.35.tgz#89078592c42bca5db712e82e12d56cd4be737c01"
integrity sha512-Msu+zIXd7Y2JrTU9JIf0xjjjAMdWEIdlj2Tmj9bSYFF6bgStrQ1WXXZxxFz5GmdzT7FcLi5U3PqBynSNX/QDGA==
@@ -612,22 +602,22 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
"@prisma/client@^2.30.3":
version "2.30.3"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-2.30.3.tgz#49c1015e2cec26a44b20c62eb2fd738cb0bb043b"
integrity sha512-Ey2miZ+Hne12We3rA8XrlPoAF0iuKEhw5IK2nropaelSt0Ju3b2qSz9Qt50a/1Mx3+7yRSu/iSXt8y9TUMl/Yw==
"@prisma/client@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.0.2.tgz#f04d9b252f3d0c6918df43ad228eac27d03f6db1"
integrity sha512-6SrDYY2Yr5AmYpVB3XAXFqfzxKMdDTemXR7FmfXthnxWhQHoBwRLNZ3B3GyI/MmWa5tr+kaaGDJjp1LU0vuYvQ==
dependencies:
"@prisma/engines-version" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20"
"@prisma/engines-version" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
"@prisma/engines-version@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20":
version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#d5ef55c92beeba56e52bba12b703af0bfd30530d"
integrity sha512-/iDRgaoSQC77WN2oDsOM8dn61fykm6tnZUAClY+6p+XJbOEgZ9gy4CKuKTBgrjSGDVjtQ/S2KGcYd3Ring8xaw==
"@prisma/engines-version@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db":
version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#c45323e420f47dd950b22c873bdcf38f75e65779"
integrity sha512-iArSApZZImVmT9oC/rGOjzvpG2AOqlIeqYcVnop9poA3FxD4zfVPbNPH9DTgOWhc06OkBHujJZeAcsNddVabIQ==
"@prisma/engines@2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20":
version "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20.tgz#2df768aa7c9f84acaa1f35c970417822233a9fb1"
integrity sha512-WPnA/IUrxDihrRhdP6+8KAVSwsc0zsh8ioPYsLJjOhzVhwpRbuFH2tJDRIAbc+qFh+BbTIZbeyBYt8fpNXaYQQ==
"@prisma/engines@2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db":
version "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db.tgz#b6cf70bc05dd2a62168a16f3ea58a1b011074621"
integrity sha512-Q9CwN6e5E5Abso7J3A1fHbcF4NXGRINyMnf7WQ07fXaebxTTARY5BNUzy2Mo5uH82eRVO5v7ImNuR044KTjLJg==
"@reduxjs/toolkit@^1.6.0":
version "1.6.0"
@@ -4444,12 +4434,12 @@ prepend-http@^1.0.1:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prisma@^2.30.3:
version "2.30.3"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-2.30.3.tgz#e4a770e1f52151e72c1c5be0aa2e75222a0135c4"
integrity sha512-48qYba2BIyUmXuosBZs0g3kYGrxKvo4VkSHYOuLlDdDirmKyvoY2hCYMUYHSx3f++8ovfgs+MX5KmNlP+iAZrQ==
prisma@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.0.2.tgz#e86cb6abf4a815c7ac97b9d0ed383f01c253ce34"
integrity sha512-TyOCbtWGDVdWvsM1RhUzJXoGClXGalHhyYWIc5eizSF8T1ScGiOa34asBUdTnXOUBFSErbsqMNw40DHAteBm1A==
dependencies:
"@prisma/engines" "2.30.1-2.b8c35d44de987a9691890b3ddf3e2e7effb9bf20"
"@prisma/engines" "2.31.0-32.2452cc6313d52b8b9a96999ac0e974d0aedf88db"
process-nextick-args@~2.0.0:
version "2.0.1"