mirror of
https://github.com/diced/zipline.git
synced 2025-12-25 04:15:41 -08:00
Compare commits
2 Commits
v3.4.4
...
feature/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45f29c7b68 | ||
|
|
114a7a05a9 |
@@ -2,6 +2,3 @@ node_modules/
|
||||
.next/
|
||||
uploads/
|
||||
.git/
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
|
||||
41
.github/workflows/docker-arm.yml
vendored
41
.github/workflows/docker-arm.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: 'CD: Push ARM64 Docker Images'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ trunk ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'server/**'
|
||||
- 'prisma/**'
|
||||
- '.github/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_to_ghcr:
|
||||
name: Push Image to GitHub Packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
file: ./Dockerfile-arm
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: ghcr.io/diced/zipline/arm64:trunk
|
||||
39
.github/workflows/docker.yml
vendored
39
.github/workflows/docker.yml
vendored
@@ -18,31 +18,28 @@ jobs:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v1
|
||||
- name: Push to GitHub Packages
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
registry: docker.pkg.github.com
|
||||
repository: diced/zipline/zipline
|
||||
dockerfile: Dockerfile
|
||||
tag_with_ref: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
push_to_dockerhub:
|
||||
name: Push Image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Push to Docker Hub
|
||||
uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/diced/zipline/zipline:trunk
|
||||
ghcr.io/diced/zipline/amd64:trunk
|
||||
diced/zipline:trunk
|
||||
repository: diced/zipline
|
||||
dockerfile: Dockerfile
|
||||
tag_with_ref: true
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -5,11 +5,6 @@
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
@@ -41,6 +36,4 @@ yarn-error.log*
|
||||
|
||||
# zipline
|
||||
config.toml
|
||||
uploads/
|
||||
dist/
|
||||
docker-compose.local.yml
|
||||
uploads/
|
||||
786
.yarn/releases/yarn-3.2.1.cjs
vendored
786
.yarn/releases/yarn-3.2.1.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
||||
13
Dockerfile
13
Dockerfile
@@ -1,21 +1,20 @@
|
||||
FROM node:16-alpine AS deps
|
||||
WORKDIR /build
|
||||
|
||||
COPY .yarn .yarn
|
||||
COPY package.json yarn.lock .yarnrc.yml ./
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN yarn install --immutable
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
FROM node:16-alpine AS builder
|
||||
WORKDIR /build
|
||||
|
||||
COPY --from=deps /build/node_modules ./node_modules
|
||||
COPY src ./src
|
||||
COPY server ./server
|
||||
COPY scripts ./scripts
|
||||
COPY prisma ./prisma
|
||||
COPY .yarn .yarn
|
||||
COPY package.json yarn.lock .yarnrc.yml esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
|
||||
COPY package.json yarn.lock next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
|
||||
|
||||
ENV ZIPLINE_DOCKER_BUILD 1
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
@@ -32,11 +31,11 @@ RUN addgroup --system --gid 1001 zipline
|
||||
RUN adduser --system --uid 1001 zipline
|
||||
|
||||
COPY --from=builder --chown=zipline:zipline /build/.next ./.next
|
||||
COPY --from=builder --chown=zipline:zipline /build/dist ./dist
|
||||
COPY --from=builder --chown=zipline:zipline /build/node_modules ./node_modules
|
||||
|
||||
COPY --from=builder /build/next.config.js ./next.config.js
|
||||
COPY --from=builder /build/src ./src
|
||||
COPY --from=builder /build/server ./server
|
||||
COPY --from=builder /build/scripts ./scripts
|
||||
COPY --from=builder /build/prisma ./prisma
|
||||
COPY --from=builder /build/tsconfig.json ./tsconfig.json
|
||||
@@ -44,4 +43,4 @@ COPY --from=builder /build/package.json ./package.json
|
||||
|
||||
USER zipline
|
||||
|
||||
CMD ["node", "dist/server"]
|
||||
CMD ["node", "server"]
|
||||
@@ -1,46 +0,0 @@
|
||||
FROM node:16 AS deps
|
||||
WORKDIR /build
|
||||
|
||||
COPY .yarn .yarn
|
||||
COPY package.json yarn.lock .yarnrc.yml ./
|
||||
|
||||
RUN yarn install --immutable
|
||||
|
||||
FROM node:16 AS builder
|
||||
WORKDIR /build
|
||||
|
||||
COPY --from=deps /build/node_modules ./node_modules
|
||||
COPY src ./src
|
||||
COPY scripts ./scripts
|
||||
COPY prisma ./prisma
|
||||
COPY .yarn .yarn
|
||||
COPY package.json yarn.lock .yarnrc.yml esbuild.config.js next.config.js next-env.d.ts zip-env.d.ts tsconfig.json ./
|
||||
|
||||
ENV ZIPLINE_DOCKER_BUILD 1
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN yarn build
|
||||
|
||||
FROM node:16 AS runner
|
||||
WORKDIR /zipline
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN addgroup --system --gid 1001 zipline
|
||||
RUN adduser --system --uid 1001 zipline
|
||||
|
||||
COPY --from=builder --chown=zipline:zipline /build/.next ./.next
|
||||
COPY --from=builder --chown=zipline:zipline /build/dist ./dist
|
||||
COPY --from=builder --chown=zipline:zipline /build/node_modules ./node_modules
|
||||
|
||||
COPY --from=builder /build/next.config.js ./next.config.js
|
||||
COPY --from=builder /build/src ./src
|
||||
COPY --from=builder /build/scripts ./scripts
|
||||
COPY --from=builder /build/prisma ./prisma
|
||||
COPY --from=builder /build/tsconfig.json ./tsconfig.json
|
||||
COPY --from=builder /build/package.json ./package.json
|
||||
|
||||
USER zipline
|
||||
|
||||
CMD ["node", "dist/server"]
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 dicedtomato
|
||||
Copyright (c) 2021 dicedtomato
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -17,19 +17,18 @@
|
||||
- Built with Next.js & React
|
||||
- Token protected uploading
|
||||
- Image uploading
|
||||
- Password Protected Uploads
|
||||
- URL shortening
|
||||
- Text uploading
|
||||
- URL Formats (uuid, dates, random alphanumeric, original name, zws)
|
||||
- Discord embeds (OG metadata)
|
||||
- Gallery viewer, and multiple file format support
|
||||
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`)
|
||||
- Easy setup instructions on [docs](https://zipline.diced.tech/) (One command install `docker-compose up -d`)
|
||||
|
||||
## Installing
|
||||
[See how to install here](https://zipl.vercel.app/docs/get-started)
|
||||
[See how to install here](https://zipline.diced.tech/docs/get-started)
|
||||
|
||||
## Configuration
|
||||
[See how to configure here](https://zipl.vercel.app/docs/config/overview)
|
||||
[See how to configure here](https://zipline.diced.tech/docs/config/overview)
|
||||
|
||||
## Theming
|
||||
[See how to theme here](https://zipl.vercel.app/docs/themes/reference)
|
||||
[See how to theme here](https://zipline.diced.tech/docs/themes/reference)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.4.4 | :white_check_mark: |
|
||||
| 3.2.x | :white_check_mark: |
|
||||
| < 3 | :x: |
|
||||
| < 2 | :x: |
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[core]
|
||||
secure = true
|
||||
secret = 'changethis'
|
||||
secret = 'some secret'
|
||||
host = '0.0.0.0'
|
||||
port = 3000
|
||||
database_url = 'postgres://postgres:postgres@postgres/postgres'
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
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:
|
||||
image: ghcr.io/diced/zipline/arm64:trunk
|
||||
ports:
|
||||
- '3000:3000'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SECURE=false
|
||||
- SECRET=changethis
|
||||
- HOST=0.0.0.0
|
||||
- PORT=3000
|
||||
- DATASOURCE_TYPE=local
|
||||
- DATASOURCE_DIRECTORY=./uploads
|
||||
- DATABASE_URL=postgresql://postgres:postgres@postgres/postgres/
|
||||
- UPLOADER_ROUTE=/u
|
||||
- UPLOADER_EMBED_ROUTE=/a
|
||||
- UPLOADER_LENGTH=6
|
||||
- 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:
|
||||
@@ -2,12 +2,11 @@ version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
volumes:
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
@@ -22,17 +21,16 @@ services:
|
||||
ports:
|
||||
- '3000:3000'
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
environment:
|
||||
- SECURE=false
|
||||
- SECRET=changethis
|
||||
- HOST=0.0.0.0
|
||||
- PORT=3000
|
||||
- DATASOURCE_TYPE=local
|
||||
- DATASOURCE_DIRECTORY=./uploads
|
||||
- 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=
|
||||
|
||||
@@ -2,12 +2,11 @@ version: '3'
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
volumes:
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
@@ -19,18 +18,17 @@ services:
|
||||
image: ghcr.io/diced/zipline/zipline:trunk
|
||||
ports:
|
||||
- '3000:3000'
|
||||
restart: always
|
||||
environment:
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SECURE=false
|
||||
- SECRET=changethis
|
||||
- HOST=0.0.0.0
|
||||
- PORT=3000
|
||||
- DATASOURCE_TYPE=local
|
||||
- DATASOURCE_DIRECTORY=./uploads
|
||||
- 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=
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
const esbuild = require('esbuild');
|
||||
const { existsSync } = require('fs');
|
||||
const { rm } = require('fs/promises');
|
||||
|
||||
(async () => {
|
||||
const watch = process.argv[2] === '--watch';
|
||||
|
||||
if (existsSync('./dist')) {
|
||||
await rm('./dist', { recursive: true });
|
||||
}
|
||||
|
||||
await esbuild.build({
|
||||
tsconfig: 'tsconfig.json',
|
||||
outdir: 'dist',
|
||||
bundle: false,
|
||||
platform: 'node',
|
||||
treeShaking: true,
|
||||
entryPoints: [
|
||||
'src/server/index.ts',
|
||||
'src/server/server.ts',
|
||||
'src/server/util.ts',
|
||||
'src/server/validateConfig.ts',
|
||||
'src/lib/logger.ts',
|
||||
'src/lib/readConfig.ts',
|
||||
'src/lib/datasource/datasource.ts',
|
||||
'src/lib/datasource/index.ts',
|
||||
'src/lib/datasource/Local.ts',
|
||||
'src/lib/datasource/S3.ts',
|
||||
'src/lib/ds.ts',
|
||||
'src/lib/config.ts',
|
||||
],
|
||||
format: 'cjs',
|
||||
resolveExtensions: ['.ts', '.js'],
|
||||
write: true,
|
||||
watch,
|
||||
incremental: watch,
|
||||
sourcemap: false,
|
||||
minify: process.env.NODE_ENV === 'production',
|
||||
});
|
||||
})();
|
||||
28
package.json
28
package.json
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"name": "zipline",
|
||||
"version": "3.4.4",
|
||||
"name": "zip3",
|
||||
"version": "3.4.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.js && REACT_EDITOR=code-insiders NODE_ENV=development node dist/server",
|
||||
"build": "npm-run-all build:server build:schema build:next",
|
||||
"build:server": "node esbuild.config.js",
|
||||
"dev": "NODE_ENV=development node server",
|
||||
"build": "npm-run-all build:schema build:next",
|
||||
"build:next": "next build",
|
||||
"build:schema": "prisma generate --schema=prisma/schema.prisma",
|
||||
"migrate:dev": "prisma migrate dev --create-only",
|
||||
"start": "node dist/server",
|
||||
"start": "node server",
|
||||
"lint": "next lint",
|
||||
"seed": "ts-node --compiler-options \"{\\\"module\\\":\\\"commonjs\\\"}\" --transpile-only prisma/seed.ts",
|
||||
"docker:run": "docker-compose up -d",
|
||||
@@ -26,20 +25,17 @@
|
||||
"@mantine/notifications": "^3.6.9",
|
||||
"@mantine/prism": "^3.6.11",
|
||||
"@modulz/radix-icons": "^4.0.0",
|
||||
"@prisma/client": "^3.14.0",
|
||||
"@prisma/migrate": "^3.14.0",
|
||||
"@prisma/sdk": "^3.14.0",
|
||||
"@prisma/client": "^3.9.2",
|
||||
"@prisma/migrate": "^3.9.2",
|
||||
"@prisma/sdk": "^3.9.2",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"argon2": "^0.28.2",
|
||||
"aws-sdk": "^2.1085.0",
|
||||
"colorette": "^1.2.2",
|
||||
"cookie": "^0.4.1",
|
||||
"fecha": "^4.2.1",
|
||||
"fflate": "^0.7.3",
|
||||
"find-my-way": "^5.2.0",
|
||||
"multer": "^1.4.4",
|
||||
"multer": "^1.4.2",
|
||||
"next": "^12.1.0",
|
||||
"prisma": "^3.14.0",
|
||||
"prisma": "^3.9.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.2.4",
|
||||
@@ -54,7 +50,6 @@
|
||||
"@types/multer": "^1.4.6",
|
||||
"@types/node": "^15.12.2",
|
||||
"babel-plugin-import": "^1.13.3",
|
||||
"esbuild": "^0.14.23",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-next": "11.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
@@ -64,6 +59,5 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/diced/zipline.git"
|
||||
},
|
||||
"packageManager": "yarn@3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `Theme` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
|
||||
@@ -9,4 +11,4 @@ ALTER TABLE "Theme" DROP CONSTRAINT "Theme_userId_fkey";
|
||||
ALTER TABLE "User" ALTER COLUMN "systemTheme" SET DEFAULT E'system';
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Theme";
|
||||
DROP TABLE "Theme";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "domains" TEXT[];
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Image" ADD COLUMN "password" TEXT;
|
||||
@@ -8,17 +8,16 @@ generator client {
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
username String
|
||||
password String
|
||||
token String
|
||||
administrator Boolean @default(false)
|
||||
systemTheme String @default("system")
|
||||
administrator Boolean @default(false)
|
||||
systemTheme String @default("system")
|
||||
embedTitle String?
|
||||
embedColor String @default("#2f3136")
|
||||
embedSiteName String? @default("{image.file} • {user.name}")
|
||||
ratelimited Boolean @default(false)
|
||||
domains String[]
|
||||
embedColor String @default("#2f3136")
|
||||
embedSiteName String? @default("{image.file} • {user.name}")
|
||||
ratelimited Boolean @default(false)
|
||||
images Image[]
|
||||
urls Url[]
|
||||
}
|
||||
@@ -38,7 +37,6 @@ model Image {
|
||||
views Int @default(0)
|
||||
favorite Boolean @default(false)
|
||||
embed Boolean @default(false)
|
||||
password String?
|
||||
invisible InvisibleImage?
|
||||
format ImageFormat @default(RANDOM)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
@@ -48,7 +46,7 @@ model Image {
|
||||
model InvisibleImage {
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
imageId Int @unique
|
||||
imageId Int
|
||||
image Image @relation(fields: [imageId], references: [id])
|
||||
}
|
||||
|
||||
@@ -66,12 +64,12 @@ model Url {
|
||||
model InvisibleUrl {
|
||||
id Int @id @default(autoincrement())
|
||||
invis String @unique
|
||||
urlId String @unique
|
||||
urlId String
|
||||
url Url @relation(fields: [urlId], references: [id])
|
||||
}
|
||||
|
||||
model Stats {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
created_at DateTime @default(now())
|
||||
data Json
|
||||
}
|
||||
}
|
||||
164
server/index.js
Normal file
164
server/index.js
Normal file
@@ -0,0 +1,164 @@
|
||||
const next = require('next').default;
|
||||
const { createServer } = require('http');
|
||||
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, getFile, migrations } = require('./util');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
const { version } = require('../package.json');
|
||||
const exts = require('../scripts/exts');
|
||||
const serverLog = Logger.get('server');
|
||||
|
||||
serverLog.info(`starting zipline@${version} server`);
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await run();
|
||||
} catch (e) {
|
||||
serverLog.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
async function run() {
|
||||
const a = readConfig();
|
||||
const config = validateConfig(a);
|
||||
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
await migrations();
|
||||
|
||||
await mkdir(config.uploader.directory, { recursive: true });
|
||||
|
||||
const app = next({
|
||||
dir: '.',
|
||||
dev,
|
||||
quiet: !dev,
|
||||
hostname: config.core.host,
|
||||
port: config.core.port,
|
||||
});
|
||||
|
||||
await app.prepare();
|
||||
|
||||
const handle = app.getRequestHandler();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const srv = createServer(async (req, res) => {
|
||||
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]) } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
const data = await getFile(config.uploader.directory, parts[2]);
|
||||
if (!data) return app.render404(req, res);
|
||||
|
||||
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimetype);
|
||||
res.end(data);
|
||||
} 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 } },
|
||||
});
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
res.end(data);
|
||||
}
|
||||
} else if (req.url.startsWith(config.uploader.route)) {
|
||||
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]) } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
mimetype: true,
|
||||
id: true,
|
||||
file: true,
|
||||
invisible: true,
|
||||
embed: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
const data = await getFile(config.uploader.directory, parts[2]);
|
||||
if (!data) return app.render404(req, res);
|
||||
|
||||
const mimetype = mimes[extname(parts[2])] ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimetype);
|
||||
res.end(data);
|
||||
} else if (image.embed) {
|
||||
handle(req, res);
|
||||
} 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);
|
||||
}
|
||||
} else {
|
||||
handle(req, res);
|
||||
}
|
||||
|
||||
if (config.core.logger) log(req.url, res.statusCode);
|
||||
});
|
||||
|
||||
srv.on('error', (e) => {
|
||||
serverLog.error(e);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
srv.on('listening', () => {
|
||||
serverLog.info(`listening on ${config.core.host}:${config.core.port}`);
|
||||
});
|
||||
|
||||
srv.listen(config.core.port, config.core.host ?? '0.0.0.0');
|
||||
|
||||
const stats = await getStats(prisma, config);
|
||||
await prisma.stats.create({
|
||||
data: {
|
||||
data: stats,
|
||||
},
|
||||
});
|
||||
setInterval(async () => {
|
||||
const stats = await getStats(prisma, config);
|
||||
await prisma.stats.create({
|
||||
data: {
|
||||
data: stats,
|
||||
},
|
||||
});
|
||||
if (config.core.logger) serverLog.info('stats updated');
|
||||
}, config.core.stats_interval * 1000);
|
||||
}
|
||||
@@ -1,34 +1,60 @@
|
||||
import { Migrate } from '@prisma/migrate/dist/Migrate';
|
||||
import { ensureDatabaseExists } from '@prisma/migrate/dist/utils/ensureDatabaseExists';
|
||||
import Logger from '../lib/logger';
|
||||
import { Datasource } from 'lib/datasource';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const { readFile, readdir, stat } = require('fs/promises');
|
||||
const { join } = require('path');
|
||||
const { Migrate } = require('@prisma/migrate/dist/Migrate.js');
|
||||
const Logger = require('../src/lib/logger.js');
|
||||
|
||||
export async function migrations() {
|
||||
async function migrations() {
|
||||
const migrate = new Migrate('./prisma/schema.prisma');
|
||||
await ensureDatabaseExists('apply', true, './prisma/schema.prisma');
|
||||
|
||||
const diagnose = await migrate.diagnoseMigrationHistory({
|
||||
optInToShadowDatabase: false,
|
||||
});
|
||||
|
||||
if (diagnose.history?.diagnostic === 'databaseIsBehind') {
|
||||
try {
|
||||
Logger.get('database').info('migrating database');
|
||||
await migrate.applyMigrations();
|
||||
} finally {
|
||||
migrate.stop();
|
||||
Logger.get('database').info('finished migrating database');
|
||||
}
|
||||
Logger.get('database').info('migrating database');
|
||||
await migrate.applyMigrations();
|
||||
Logger.get('database').info('finished migrating database');
|
||||
}
|
||||
|
||||
migrate.stop();
|
||||
}
|
||||
|
||||
export function log(url: string) {
|
||||
function log(url) {
|
||||
if (url.startsWith('/_next') || url.startsWith('/__nextjs')) return;
|
||||
return Logger.get('url').info(url);
|
||||
}
|
||||
|
||||
export function bytesToRead(bytes: number) {
|
||||
function shouldUseYarn() {
|
||||
try {
|
||||
execSync('yarnpkg --version', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getFile(dir, file) {
|
||||
try {
|
||||
const data = await readFile(join(process.cwd(), dir, file));
|
||||
return data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function sizeOfDir(directory) {
|
||||
const files = await readdir(directory);
|
||||
|
||||
let size = 0;
|
||||
for (let i = 0, L = files.length; i !== L; ++i) {
|
||||
const sta = await stat(join(directory, files[i]));
|
||||
size += sta.size;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
function bytesToRead(bytes) {
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||
let num = 0;
|
||||
|
||||
@@ -41,8 +67,8 @@ export function bytesToRead(bytes: number) {
|
||||
}
|
||||
|
||||
|
||||
export async function getStats(prisma: PrismaClient, datasource: Datasource) {
|
||||
const size = await datasource.size();
|
||||
async function getStats(prisma, config) {
|
||||
const size = await sizeOfDir(join(process.cwd(), config.uploader.directory));
|
||||
const byUser = await prisma.image.groupBy({
|
||||
by: ['userId'],
|
||||
_count: {
|
||||
@@ -86,9 +112,19 @@ export async function getStats(prisma: PrismaClient, datasource: Datasource) {
|
||||
size: bytesToRead(size),
|
||||
size_num: size,
|
||||
count,
|
||||
count_by_user: count_by_user.sort((a, b) => b.count - a.count),
|
||||
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.sort((a, b) => b.count - a.count),
|
||||
types_count: types_count.sort((a,b) => b.count-a.count),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
migrations,
|
||||
bytesToRead,
|
||||
getFile,
|
||||
getStats,
|
||||
log,
|
||||
sizeOfDir,
|
||||
shouldUseYarn,
|
||||
};
|
||||
40
server/validateConfig.js
Normal file
40
server/validateConfig.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const { object, bool, string, number, boolean, array } = require('yup');
|
||||
|
||||
const validator = object({
|
||||
core: object({
|
||||
secure: bool().default(false),
|
||||
secret: string().min(8).required(),
|
||||
host: string().default('0.0.0.0'),
|
||||
port: number().default(3000),
|
||||
database_url: string().required(),
|
||||
logger: boolean().default(false),
|
||||
stats_interval: number().default(1800),
|
||||
}).required(),
|
||||
uploader: object({
|
||||
route: string().default('/u'),
|
||||
embed_route: string().default('/a'),
|
||||
length: number().default(6),
|
||||
directory: string().default('./uploads'),
|
||||
admin_limit: number().default(104900000),
|
||||
user_limit: number().default(104900000),
|
||||
disabled_extensions: array().default([]),
|
||||
}).required(),
|
||||
urls: object({
|
||||
route: string().default('/go'),
|
||||
length: number().default(6),
|
||||
}).required(),
|
||||
ratelimit: object({
|
||||
user: number().default(0),
|
||||
admin: number().default(0),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
module.exports = function validate(config) {
|
||||
try {
|
||||
return validator.validateSync(config, { abortEarly: false });
|
||||
} catch (e) {
|
||||
if (process.env.ZIPLINE_DOCKER_BUILD) return {};
|
||||
throw `${e.errors.length} errors occured\n${e.errors.map(x => '\t' + x).join('\n')}`;
|
||||
}
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'react-table';
|
||||
import {
|
||||
ActionIcon,
|
||||
Checkbox,
|
||||
createStyles,
|
||||
Divider,
|
||||
Group,
|
||||
|
||||
@@ -122,7 +122,6 @@ export default function Layout({ children, user }) {
|
||||
|
||||
const openResetToken = () => modals.openConfirmModal({
|
||||
title: 'Reset Token',
|
||||
centered: true,
|
||||
children: (
|
||||
<Text size='sm'>
|
||||
Once you reset your token, you will have to update any uploaders to use this new token.
|
||||
@@ -154,7 +153,6 @@ export default function Layout({ children, user }) {
|
||||
|
||||
const openCopyToken = () => modals.openConfirmModal({
|
||||
title: 'Copy Token',
|
||||
centered: true,
|
||||
children: (
|
||||
<Text size='sm'>
|
||||
Make sure you don't share this token with anyone as they will be able to upload files on your behalf.
|
||||
@@ -328,4 +326,4 @@ export default function Layout({ children, user }) {
|
||||
<Paper withBorder padding='md' shadow='xs'>{children}</Paper>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text } from '@mantine/core';
|
||||
|
||||
export default function StatText({ children }) {
|
||||
return <Text color='gray' size='xl'>{children}</Text>;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Card from 'components/Card';
|
||||
import ZiplineImage from 'components/Image';
|
||||
import Image from 'components/Image';
|
||||
import ImagesTable from 'components/ImagesTable';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import { Text, Skeleton, Title, SimpleGrid } from '@mantine/core';
|
||||
import { Box, Text, Table, Skeleton, Title, SimpleGrid } from '@mantine/core';
|
||||
import { randomId, useClipboard } from '@mantine/hooks';
|
||||
import Link from 'components/Link';
|
||||
import { CopyIcon, Cross1Icon, TrashIcon } from '@modulz/radix-icons';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import StatText from 'components/StatText';
|
||||
|
||||
type Aligns = 'inherit' | 'right' | 'left' | 'center' | 'justify';
|
||||
|
||||
@@ -28,6 +27,37 @@ export function bytesToRead(bytes: number) {
|
||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||
}
|
||||
|
||||
function StatText({ children }) {
|
||||
return <Text color='gray' size='xl'>{children}</Text>;
|
||||
}
|
||||
|
||||
function StatTable({ rows, columns }) {
|
||||
return (
|
||||
<Box sx={{ pt: 1 }}>
|
||||
<Table highlightOnHover>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th key={randomId()}>{col.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={randomId()}>
|
||||
{columns.map(col => (
|
||||
<td key={randomId()}>
|
||||
{col.format ? col.format(row[col.id]) : row[col.id]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
|
||||
@@ -98,7 +128,8 @@ export default function Dashboard() {
|
||||
]}
|
||||
>
|
||||
{recent.length ? recent.map(image => (
|
||||
<ZiplineImage key={randomId()} image={image} updateImages={updateImages} />
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
<Image key={randomId()} image={image} updateImages={updateImages} />
|
||||
)) : [1,2,3,4].map(x => (
|
||||
<div key={x}>
|
||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }}/>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Link from 'components/Link';
|
||||
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
|
||||
import { updateUser } from 'lib/redux/reducers/user';
|
||||
import { randomId, useForm } from '@mantine/hooks';
|
||||
import { Tooltip, TextInput, Button, Text, Title, Group, ColorInput, MultiSelect, Space } from '@mantine/core';
|
||||
import { DownloadIcon, Cross1Icon } from '@modulz/radix-icons';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { useForm } from '@mantine/hooks';
|
||||
import { Tooltip, TextInput, Button, Text, Title, Group, ColorInput } from '@mantine/core';
|
||||
import { DownloadIcon } from '@modulz/radix-icons';
|
||||
|
||||
function VarsTooltip({ children }) {
|
||||
return (
|
||||
@@ -28,9 +27,6 @@ function VarsTooltip({ children }) {
|
||||
export default function Manage() {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
const dispatch = useStoreDispatch();
|
||||
const notif = useNotifications();
|
||||
|
||||
const [domains, setDomains] = useState(user.domains ?? []);
|
||||
|
||||
const genShareX = (withEmbed: boolean = false, withZws: boolean = false) => {
|
||||
const config = {
|
||||
@@ -65,7 +61,6 @@ export default function Manage() {
|
||||
embedTitle: user.embedTitle ?? '',
|
||||
embedColor: user.embedColor,
|
||||
embedSiteName: user.embedSiteName ?? '',
|
||||
domains: user.domains ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,52 +73,19 @@ export default function Manage() {
|
||||
|
||||
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
||||
|
||||
const id = notif.showNotification({
|
||||
title: 'Updating user...',
|
||||
message: '',
|
||||
loading: true,
|
||||
autoClose: false,
|
||||
});
|
||||
|
||||
const data = {
|
||||
username: cleanUsername,
|
||||
password: cleanPassword === '' ? null : cleanPassword,
|
||||
embedTitle: cleanEmbedTitle === '' ? null : cleanEmbedTitle,
|
||||
embedColor: cleanEmbedColor === '' ? null : cleanEmbedColor,
|
||||
embedSiteName: cleanEmbedSiteName === '' ? null : cleanEmbedSiteName,
|
||||
domains,
|
||||
};
|
||||
|
||||
const newUser = await useFetch('/api/user', 'PATCH', data);
|
||||
|
||||
if (newUser.error) {
|
||||
if (newUser.invalidDomains) {
|
||||
notif.updateNotification(id, {
|
||||
message: <>
|
||||
<Text mt='xs'>The following domains are invalid:</Text>
|
||||
{newUser.invalidDomains.map(err => (
|
||||
<>
|
||||
<Text color='gray' key={randomId()}>{err.domain}: {err.reason}</Text>
|
||||
<Space h='md' />
|
||||
</>
|
||||
))}
|
||||
</>,
|
||||
color: 'red',
|
||||
icon: <Cross1Icon />,
|
||||
});
|
||||
}
|
||||
notif.updateNotification(id, {
|
||||
title: 'Couldn\'t save user',
|
||||
message: newUser.error,
|
||||
color: 'red',
|
||||
icon: <Cross1Icon />,
|
||||
});
|
||||
} else {
|
||||
dispatch(updateUser(newUser));
|
||||
notif.updateNotification(id, {
|
||||
title: 'Saved User',
|
||||
message: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,23 +97,10 @@ export default function Manage() {
|
||||
</VarsTooltip>
|
||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
|
||||
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
|
||||
<TextInput id='password' label='Password'type='password' {...form.getInputProps('password')} />
|
||||
<TextInput id='embedTitle' label='Embed Title' {...form.getInputProps('embedTitle')} />
|
||||
<ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} />
|
||||
<TextInput id='embedSiteName' label='Embed Site Name' {...form.getInputProps('embedSiteName')} />
|
||||
<MultiSelect
|
||||
id='domains'
|
||||
label='Domains'
|
||||
data={domains}
|
||||
placeholder='Leave blank if you dont want random domain selection.'
|
||||
creatable
|
||||
searchable
|
||||
clearable
|
||||
getCreateLabel={query => `Add ${query}`}
|
||||
onCreate={query => setDomains((current) => [...current, query])}
|
||||
{...form.getInputProps('domains')}
|
||||
/>
|
||||
|
||||
<Group position='right' sx={{ paddingTop: 12 }}>
|
||||
<Button
|
||||
type='submit'
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import Card from 'components/Card';
|
||||
import StatText from 'components/StatText';
|
||||
import Image from 'components/Image';
|
||||
import ImagesTable from 'components/ImagesTable';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import { Box, Text, Table, Skeleton, Title, SimpleGrid } from '@mantine/core';
|
||||
import { randomId } from '@mantine/hooks';
|
||||
import { randomId, useClipboard } from '@mantine/hooks';
|
||||
import Link from 'components/Link';
|
||||
import { CopyIcon, Cross1Icon, TrashIcon } from '@modulz/radix-icons';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
|
||||
type Aligns = 'inherit' | 'right' | 'left' | 'center' | 'justify';
|
||||
|
||||
export function bytesToRead(bytes: number) {
|
||||
if (isNaN(bytes)) return '0.0 B';
|
||||
@@ -21,6 +27,10 @@ export function bytesToRead(bytes: number) {
|
||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||
}
|
||||
|
||||
function StatText({ children }) {
|
||||
return <Text color='gray' size='xl'>{children}</Text>;
|
||||
}
|
||||
|
||||
function StatTable({ rows, columns }) {
|
||||
return (
|
||||
<Box sx={{ pt: 1 }}>
|
||||
@@ -48,7 +58,9 @@ function StatTable({ rows, columns }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function Stats() {
|
||||
export default function Dashboard() {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
|
||||
const [stats, setStats] = useState(null);
|
||||
|
||||
const update = async () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ function getIconColor(status, theme) {
|
||||
: theme.black;
|
||||
}
|
||||
|
||||
export default function Upload() {
|
||||
export default function Upload({ route }) {
|
||||
const theme = useMantineTheme();
|
||||
const notif = useNotifications();
|
||||
const clipboard = useClipboard();
|
||||
@@ -58,7 +58,6 @@ export default function Upload() {
|
||||
title: 'Uploading Images...',
|
||||
message: '',
|
||||
loading: true,
|
||||
autoClose: false,
|
||||
});
|
||||
|
||||
const res = await fetch('/api/upload', {
|
||||
@@ -90,8 +89,10 @@ export default function Upload() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropzone onDrop={(f) => setFiles([...files, ...f])}>
|
||||
{status => (
|
||||
<Dropzone
|
||||
onDrop={(f) => setFiles([...files, ...f])}
|
||||
>
|
||||
{(status) => (
|
||||
<>
|
||||
<Group position='center' spacing='xl' style={{ minHeight: 220, pointerEvents: 'none' }}>
|
||||
<ImageUploadIcon
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useForm } from '@mantine/hooks';
|
||||
import { Avatar, Modal, Title, TextInput, Group, Button, Card, ActionIcon, SimpleGrid, Switch, Skeleton, Checkbox } from '@mantine/core';
|
||||
import { Avatar, Modal, Title, TextInput, Group, Button, Card, Grid, ActionIcon, SimpleGrid, Switch, Skeleton } from '@mantine/core';
|
||||
import { Cross1Icon, PlusIcon, TrashIcon } from '@modulz/radix-icons';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { useModals } from '@mantine/modals';
|
||||
|
||||
|
||||
function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||
@@ -51,7 +51,7 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||
|
||||
updateUsers();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={open}
|
||||
@@ -76,15 +76,22 @@ export default function Users() {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
const router = useRouter();
|
||||
const notif = useNotifications();
|
||||
const modals = useModals();
|
||||
|
||||
const [users, setUsers] = useState([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleDelete = async (user, delete_images) => {
|
||||
const updateUsers = async () => {
|
||||
const us = await useFetch('/api/users');
|
||||
if (!us.error) {
|
||||
setUsers(us);
|
||||
} else {
|
||||
router.push('/dashboard');
|
||||
};
|
||||
};
|
||||
|
||||
const handleDelete = async (user) => {
|
||||
const res = await useFetch('/api/users', 'DELETE', {
|
||||
id: user.id,
|
||||
delete_images,
|
||||
});
|
||||
if (res.error) {
|
||||
notif.showNotification({
|
||||
@@ -100,39 +107,9 @@ export default function Users() {
|
||||
color: 'green',
|
||||
icon: <TrashIcon />,
|
||||
});
|
||||
updateUsers();
|
||||
}
|
||||
};
|
||||
|
||||
// 2-step modal for deleting user if they want to delete their images too.
|
||||
const openDeleteModal = user => modals.openConfirmModal({
|
||||
title: `Delete ${user.username}?`,
|
||||
closeOnConfirm: false,
|
||||
centered: true,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: () => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete ${user.username}'s images?`,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: () => {
|
||||
handleDelete(user, true);
|
||||
modals.closeAll();
|
||||
},
|
||||
onCancel: () => {
|
||||
handleDelete(user, false);
|
||||
modals.closeAll();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateUsers = async () => {
|
||||
const us = await useFetch('/api/users');
|
||||
if (!us.error) {
|
||||
setUsers(us);
|
||||
} else {
|
||||
router.push('/dashboard');
|
||||
};
|
||||
updateUsers();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -144,7 +121,7 @@ export default function Users() {
|
||||
<CreateUserModal open={open} setOpen={setOpen} updateUsers={updateUsers} />
|
||||
<Group>
|
||||
<Title sx={{ marginBottom: 12 }}>Users</Title>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}><PlusIcon /></ActionIcon>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}><PlusIcon/></ActionIcon>
|
||||
</Group>
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
@@ -153,23 +130,23 @@ export default function Users() {
|
||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
||||
]}
|
||||
>
|
||||
{users.length ? users.filter(x => x.username !== user.username).map(user => (
|
||||
{users.length ? users.filter(x => x.username !== user.username).map((user, i) => (
|
||||
<Card key={user.id} sx={{ maxWidth: '100%' }}>
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<Avatar color={user.administrator ? 'primary' : 'dark'}>{user.username[0]}</Avatar>
|
||||
<Avatar color={user.administrator ? 'primary' : 'dark'}>{user.username[0]}</Avatar>
|
||||
<Title>{user.username}</Title>
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
|
||||
<ActionIcon aria-label='delete' onClick={() => handleDelete(user)}>
|
||||
<TrashIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
)) : [1, 2, 3, 4].map(x => (
|
||||
)): [1,2,3,4].map(x => (
|
||||
<div key={x}>
|
||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }}/>
|
||||
</div>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Config } from './types';
|
||||
import readConfig from './readConfig';
|
||||
import validateConfig from '../server/validateConfig';
|
||||
import validateConfig from '../../server/validateConfig';
|
||||
|
||||
if (!global.config) global.config = validateConfig(readConfig()) as unknown as Config;
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { createReadStream, ReadStream } from 'fs';
|
||||
import { readdir, rm, stat, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { Datasource } from './datasource';
|
||||
|
||||
export class Local extends Datasource {
|
||||
public name: string = 'local';
|
||||
|
||||
public constructor(public path: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async save(file: string, data: Buffer): Promise<void> {
|
||||
await writeFile(join(process.cwd(), this.path, file), data);
|
||||
}
|
||||
|
||||
public async delete(file: string): Promise<void> {
|
||||
await rm(join(process.cwd(), this.path, file));
|
||||
}
|
||||
|
||||
public get(file: string): ReadStream {
|
||||
try {
|
||||
return createReadStream(join(process.cwd(), this.path, file));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async size(): Promise<number> {
|
||||
const files = await readdir(this.path);
|
||||
|
||||
let size = 0;
|
||||
for (let i = 0, L = files.length; i !== L; ++i) {
|
||||
const sta = await stat(join(this.path, files[i]));
|
||||
size += sta.size;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Datasource } from './datasource';
|
||||
import AWS from 'aws-sdk';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export class S3 extends Datasource {
|
||||
public name: string = 'S3';
|
||||
public s3: AWS.S3;
|
||||
|
||||
public constructor(
|
||||
public accessKey: string,
|
||||
public secretKey: string,
|
||||
public bucket: string,
|
||||
) {
|
||||
super();
|
||||
this.s3 = new AWS.S3({
|
||||
accessKeyId: accessKey,
|
||||
secretAccessKey: secretKey,
|
||||
});
|
||||
}
|
||||
|
||||
public async save(file: string, data: Buffer): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.s3.upload({
|
||||
Bucket: this.bucket,
|
||||
Key: file,
|
||||
Body: data,
|
||||
}, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async delete(file: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.s3.deleteObject({
|
||||
Bucket: this.bucket,
|
||||
Key: file,
|
||||
}, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public get(file: string): Readable {
|
||||
// Unfortunately, aws-sdk is bad and the stream still loads everything into memory.
|
||||
return this.s3.getObject({
|
||||
Bucket: this.bucket,
|
||||
Key: file,
|
||||
}).createReadStream();
|
||||
}
|
||||
|
||||
public async size(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.s3.listObjects({
|
||||
Bucket: this.bucket,
|
||||
}, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const size = data.Contents.reduce((acc, cur) => acc + cur.Size, 0);
|
||||
resolve(size);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export abstract class Datasource {
|
||||
public name: string;
|
||||
|
||||
public abstract save(file: string, data: Buffer): Promise<void>;
|
||||
public abstract delete(file: string): Promise<void>;
|
||||
public abstract get(file: string): Readable;
|
||||
public abstract size(): Promise<number>;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export { Datasource } from './datasource';
|
||||
export { Local } from './Local';
|
||||
export { S3 } from './S3';
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import config from './config';
|
||||
import { S3, Local } from './datasource';
|
||||
import Logger from './logger';
|
||||
|
||||
if (!global.datasource) {
|
||||
switch (config.datasource.type) {
|
||||
case 's3':
|
||||
Logger.get('datasource').info(`Using S3(${config.datasource.s3.bucket}) datasource`);
|
||||
global.datasource = new S3(config.datasource.s3.access_key_id, config.datasource.s3.secret_access_key, config.datasource.s3.bucket);
|
||||
break;
|
||||
case 'local':
|
||||
Logger.get('datasource').info(`Using local(${config.datasource.local.directory}) datasource`);
|
||||
global.datasource = new Local(config.datasource.local.directory);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid datasource type');
|
||||
}
|
||||
}
|
||||
|
||||
export default global.datasource;
|
||||
38
src/lib/logger.js
Normal file
38
src/lib/logger.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const { format } = require('fecha');
|
||||
const { blueBright, red, cyan } = require('colorette');
|
||||
|
||||
module.exports = class Logger {
|
||||
static get(clas) {
|
||||
if (typeof clas !== 'function') if (typeof clas !== 'string') throw new Error('not string/function');
|
||||
|
||||
const name = clas.name ?? clas;
|
||||
|
||||
return new Logger(name);
|
||||
}
|
||||
|
||||
constructor (name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
info(message) {
|
||||
console.log(this.formatMessage('INFO', this.name, message));
|
||||
}
|
||||
|
||||
error(error) {
|
||||
console.log(this.formatMessage('ERROR', this.name, error.stack ?? error));
|
||||
}
|
||||
|
||||
formatMessage(level, name, message) {
|
||||
const time = format(new Date(), 'YYYY-MM-DD hh:mm:ss,SSS A');
|
||||
return `${time} ${this.formatLevel(level)} [${blueBright(name)}] ${message}`;
|
||||
}
|
||||
|
||||
formatLevel(level) {
|
||||
switch (level) {
|
||||
case 'INFO':
|
||||
return cyan('INFO ');
|
||||
case 'ERROR':
|
||||
return red('ERROR');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { format } from 'fecha';
|
||||
import { blueBright, red, cyan } from 'colorette';
|
||||
|
||||
export enum LoggerLevel {
|
||||
ERROR,
|
||||
INFO,
|
||||
}
|
||||
|
||||
export default class Logger {
|
||||
public name: string;
|
||||
|
||||
static get(clas: any) {
|
||||
if (typeof clas !== 'function') if (typeof clas !== 'string') throw new Error('not string/function');
|
||||
|
||||
const name = clas.name ?? clas;
|
||||
|
||||
return new Logger(name);
|
||||
}
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
info(...args) {
|
||||
console.log(this.formatMessage(LoggerLevel.INFO, this.name, args.join(' ')));
|
||||
}
|
||||
|
||||
error(...args: any[]) {
|
||||
console.log(this.formatMessage(LoggerLevel.ERROR, this.name, args.map(error => error.stack ?? error).join(' ')));
|
||||
}
|
||||
|
||||
formatMessage(level: LoggerLevel, name, message) {
|
||||
const time = format(new Date(), 'YYYY-MM-DD hh:mm:ss,SSS A');
|
||||
return `${time} ${this.formatLevel(level)} [${blueBright(name)}] ${message}`;
|
||||
}
|
||||
|
||||
formatLevel(level: LoggerLevel) {
|
||||
switch (level) {
|
||||
case LoggerLevel.INFO:
|
||||
return cyan('INFO ');
|
||||
case LoggerLevel.ERROR:
|
||||
return red('ERROR');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -11,7 +11,7 @@ export interface NextApiFile {
|
||||
originalname: string;
|
||||
encoding: string;
|
||||
mimetype: string;
|
||||
buffer: Buffer;
|
||||
buffer: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ export type NextApiReq = NextApiRequest & {
|
||||
administrator: boolean;
|
||||
id: number;
|
||||
password: string;
|
||||
domains: string[];
|
||||
} | null | void>;
|
||||
getCookie: (name: string) => string | null;
|
||||
cleanCookie: (name: string) => void;
|
||||
@@ -34,7 +33,7 @@ export type NextApiReq = NextApiRequest & {
|
||||
|
||||
export type NextApiRes = NextApiResponse & {
|
||||
error: (message: string) => void;
|
||||
forbid: (message: string, extra?: any) => void;
|
||||
forbid: (message: string) => void;
|
||||
bad: (message: string) => void;
|
||||
json: (json: any) => void;
|
||||
ratelimited: () => void;
|
||||
@@ -53,12 +52,11 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
};
|
||||
|
||||
res.forbid = (message: string, extra: any = {}) => {
|
||||
res.forbid = (message: string) => {
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
res.status(403);
|
||||
res.json({
|
||||
error: '403: ' + message,
|
||||
...extra,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -95,7 +93,6 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
||||
maxAge: undefined,
|
||||
}));
|
||||
};
|
||||
|
||||
req.user = async () => {
|
||||
try {
|
||||
const userId = req.getCookie('user');
|
||||
@@ -114,7 +111,6 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
||||
systemTheme: true,
|
||||
token: true,
|
||||
username: true,
|
||||
domains: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import parse from '@iarna/toml/parse-string';
|
||||
import Logger from './logger';
|
||||
import { Config } from './types';
|
||||
const { existsSync, readFileSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
const parse = require('@iarna/toml/parse-string.js');
|
||||
const Logger = require('./logger.js');
|
||||
|
||||
const e = (val, type, fn) => ({ val, type, fn });
|
||||
|
||||
@@ -15,14 +14,9 @@ const envValues = [
|
||||
e('LOGGER', 'boolean', (c, v) => c.core.logger = v ?? true),
|
||||
e('STATS_INTERVAL', 'number', (c, v) => c.core.stats_interval = v),
|
||||
|
||||
e('DATASOURCE_TYPE', 'string', (c, v) => c.datasource.type = v),
|
||||
e('DATASOURCE_LOCAL_DIRECTORY', 'string', (c, v) => c.datasource.local.directory = v),
|
||||
e('DATASOURCE_S3_ACCESS_KEY_ID', 'string', (c, v) => c.datasource.s3.access_key_id = v ),
|
||||
e('DATASOURCE_S3_SECRET_ACCESS_KEY', 'string', (c, v) => c.datasource.s3.secret_access_key = v),
|
||||
e('DATASOURCE_S3_BUCKET', 'string', (c, v) => c.datasource.s3.bucket = v),
|
||||
|
||||
e('UPLOADER_ROUTE', 'string', (c, v) => c.uploader.route = v),
|
||||
e('UPLOADER_LENGTH', 'number', (c, v) => c.uploader.length = v),
|
||||
e('UPLOADER_DIRECTORY', 'string', (c, v) => c.uploader.directory = v),
|
||||
e('UPLOADER_ADMIN_LIMIT', 'number', (c, v) => c.uploader.admin_limit = v),
|
||||
e('UPLOADER_USER_LIMIT', 'number', (c, v) => c.uploader.user_limit = v),
|
||||
e('UPLOADER_DISABLED_EXTS', 'array', (c, v) => v ? c.uploader.disabled_extentions = v : c.uploader.disabled_extentions = []),
|
||||
@@ -34,7 +28,7 @@ const envValues = [
|
||||
e('RATELIMIT_ADMIN', 'number', (c, v) => c.ratelimit.user = v ?? 0),
|
||||
];
|
||||
|
||||
export default function readConfig(): Config {
|
||||
module.exports = function readConfig() {
|
||||
if (!existsSync(join(process.cwd(), 'config.toml'))) {
|
||||
if (!process.env.ZIPLINE_DOCKER_BUILD) Logger.get('config').info('reading environment');
|
||||
return tryReadEnv();
|
||||
@@ -49,7 +43,7 @@ export default function readConfig(): Config {
|
||||
}
|
||||
};
|
||||
|
||||
function tryReadEnv(): Config {
|
||||
function tryReadEnv() {
|
||||
const config = {
|
||||
core: {
|
||||
secure: undefined,
|
||||
@@ -60,20 +54,10 @@ function tryReadEnv(): Config {
|
||||
logger: undefined,
|
||||
stats_interval: undefined,
|
||||
},
|
||||
datasource: {
|
||||
type: undefined,
|
||||
local: {
|
||||
directory: undefined,
|
||||
},
|
||||
s3: {
|
||||
access_key_id: undefined,
|
||||
secret_access_key: undefined,
|
||||
bucket: undefined,
|
||||
},
|
||||
},
|
||||
uploader: {
|
||||
route: undefined,
|
||||
length: undefined,
|
||||
directory: undefined,
|
||||
admin_limit: undefined,
|
||||
user_limit: undefined,
|
||||
disabled_extentions: undefined,
|
||||
@@ -90,7 +74,7 @@ function tryReadEnv(): Config {
|
||||
|
||||
for (let i = 0, L = envValues.length; i !== L; ++i) {
|
||||
const envValue = envValues[i];
|
||||
let value: any = process.env[envValue.val];
|
||||
let value = process.env[envValue.val];
|
||||
|
||||
if (!value) {
|
||||
envValues[i].fn(config, undefined);
|
||||
@@ -7,7 +7,6 @@ export interface User {
|
||||
embedColor: string;
|
||||
embedSiteName: string;
|
||||
systemTheme: string;
|
||||
domains: string[];
|
||||
}
|
||||
|
||||
const initialState: User = null;
|
||||
|
||||
@@ -21,26 +21,6 @@ export interface ConfigCore {
|
||||
stats_interval: number;
|
||||
}
|
||||
|
||||
export interface ConfigDatasource {
|
||||
// The type of datasource
|
||||
type: 'local' | 's3';
|
||||
|
||||
// The local datasource
|
||||
local: ConfigLocalDatasource;
|
||||
s3?: ConfigS3Datasource;
|
||||
}
|
||||
|
||||
export interface ConfigLocalDatasource {
|
||||
// The directory to store files in
|
||||
directory: string;
|
||||
}
|
||||
|
||||
export interface ConfigS3Datasource {
|
||||
access_key_id: string;
|
||||
secret_access_key: string;
|
||||
bucket: string;
|
||||
}
|
||||
|
||||
export interface ConfigUploader {
|
||||
// The route uploads will be served on
|
||||
route: string;
|
||||
@@ -48,6 +28,9 @@ export interface ConfigUploader {
|
||||
// Length of random chars to generate for file names
|
||||
length: number;
|
||||
|
||||
// Where uploads are stored
|
||||
directory: string;
|
||||
|
||||
// Admin file upload limit
|
||||
admin_limit: number;
|
||||
|
||||
@@ -80,5 +63,4 @@ export interface Config {
|
||||
uploader: ConfigUploader;
|
||||
urls: ConfigUrls;
|
||||
ratelimit: ConfigRatelimit;
|
||||
datasource: ConfigDatasource;
|
||||
}
|
||||
|
||||
@@ -1,85 +1,47 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { Box, Button, Modal, PasswordInput } from '@mantine/core';
|
||||
import { Box, useMantineTheme } from '@mantine/core';
|
||||
import config from 'lib/config';
|
||||
import prisma from 'lib/prisma';
|
||||
import { getFile } from '../../server/util';
|
||||
import { parse } from 'lib/clientUtils';
|
||||
import * as exts from '../../scripts/exts';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import ZiplineTheming from 'components/Theming';
|
||||
|
||||
export default function EmbeddedImage({ image, user, pass }) {
|
||||
export default function EmbeddedImage({ image, user }) {
|
||||
const dataURL = (route: string) => `${route}/${image.file}`;
|
||||
|
||||
const [opened, setOpened] = useState(pass);
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// reapply date from workaround
|
||||
image.created_at = new Date(image.created_at);
|
||||
|
||||
const check = async () => {
|
||||
const res = await fetch(`/api/auth/image?id=${image.id}&password=${password}`);
|
||||
|
||||
if (res.ok) {
|
||||
setError('');
|
||||
updateImage(`/api/auth/image?id=${image.id}&password=${password}`);
|
||||
setOpened(false);
|
||||
} else {
|
||||
setError('Invalid password');
|
||||
}
|
||||
};
|
||||
|
||||
const updateImage = async (url?: string) => {
|
||||
const updateImage = () => {
|
||||
const imageEl = document.getElementById('image_content') as HTMLImageElement;
|
||||
|
||||
const img = new Image();
|
||||
img.addEventListener('load', function() {
|
||||
if (this.naturalWidth > innerWidth) imageEl.width = Math.floor(this.naturalWidth * Math.min((innerHeight / this.naturalHeight), (innerWidth / this.naturalWidth)));
|
||||
else imageEl.width = this.naturalWidth;
|
||||
});
|
||||
|
||||
img.src = url || dataURL('/r');
|
||||
if (url) {
|
||||
imageEl.src = url;
|
||||
};
|
||||
};
|
||||
const original = new Image;
|
||||
original.src = dataURL('/r');
|
||||
|
||||
useEffect(() => {
|
||||
if (pass) {
|
||||
setOpened(true);
|
||||
} else {
|
||||
updateImage();
|
||||
}
|
||||
}, []);
|
||||
if (original.width > innerWidth) imageEl.width = Math.floor(original.width * Math.min((innerHeight / original.height), (innerWidth / original.width)));
|
||||
else imageEl.width = original.width;
|
||||
};
|
||||
|
||||
useEffect(() => updateImage(), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{image.embed && (
|
||||
<>
|
||||
{user.embedSiteName && <meta property='og:site_name' content={parse(user.embedSiteName, image, user)} />}
|
||||
{user.embedTitle && <meta property='og:title' content={parse(user.embedTitle, image, user)} />}
|
||||
<meta property='theme-color' content={user.embedColor} />
|
||||
{user.embedSiteName && (<meta property='og:site_name' content={parse(user.embedSiteName, image, user)} />)}
|
||||
{user.embedTitle && (<meta property='og:title' content={parse(user.embedTitle, image, user)} />)}
|
||||
<meta property='theme-color' content={user.embedColor}/>
|
||||
</>
|
||||
)}
|
||||
<meta property='og:image' content={dataURL('/r')} />
|
||||
<meta property='twitter:card' content='summary_large_image' />
|
||||
<title>{image.file}</title>
|
||||
</Head>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title='Password Protected'
|
||||
centered={true}
|
||||
hideCloseButton={true}
|
||||
closeOnEscape={false}
|
||||
closeOnClickOutside={false}
|
||||
>
|
||||
<PasswordInput label='Password' placeholder='Password' error={error} value={password} onChange={e => setPassword(e.target.value)} />
|
||||
<Button fullWidth onClick={() => check()} mt='md'>
|
||||
Submit
|
||||
</Button>
|
||||
</Modal>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -118,7 +80,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
props: {},
|
||||
redirect: {
|
||||
destination: url.destination,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
} else {
|
||||
@@ -137,7 +99,6 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
userId: true,
|
||||
embed: true,
|
||||
created_at: true,
|
||||
password: true,
|
||||
},
|
||||
});
|
||||
if (!image) return { notFound: true };
|
||||
@@ -159,6 +120,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
image.created_at = image.created_at.toString();
|
||||
|
||||
const prismRender = Object.keys(exts).includes(image.file.split('.').pop());
|
||||
// let prismRenderCode;/
|
||||
// if (prismRender) prismRenderCode = (await getFile(config.uploader.directory, id)).toString();
|
||||
if (prismRender) return {
|
||||
redirect: {
|
||||
destination: `/code/${image.file}`,
|
||||
@@ -167,21 +130,17 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
};
|
||||
|
||||
if (!image.mimetype.startsWith('image')) {
|
||||
const { default: datasource } = await import('lib/ds');
|
||||
|
||||
const data = datasource.get(image.file);
|
||||
const data = await getFile(config.uploader.directory, id);
|
||||
if (!data) return { notFound: true };
|
||||
|
||||
data.pipe(context.res);
|
||||
context.res.end(data);
|
||||
return { props: {} };
|
||||
}
|
||||
const pass = image.password ? true : false;
|
||||
delete image.password;
|
||||
|
||||
return {
|
||||
props: {
|
||||
image,
|
||||
user,
|
||||
pass,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import { checkPassword } from 'lib/util';
|
||||
import datasource from 'lib/ds';
|
||||
import mimes from '../../../../scripts/mimes';
|
||||
import { extname } from 'path';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const { id, password } = req.query;
|
||||
|
||||
const image = await prisma.image.findFirst({
|
||||
where: {
|
||||
id: Number(id),
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) return res.status(404).end(JSON.stringify({ error: 'Image not found' }));
|
||||
if (!password) return res.forbid('No password provided');
|
||||
|
||||
const valid = await checkPassword(password as string, image.password);
|
||||
if (!valid) return res.forbid('Wrong password');
|
||||
|
||||
const data = datasource.get(image.file);
|
||||
if (!data) return res.error('Image not found');
|
||||
const mimetype = mimes[extname(image.file)] ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimetype);
|
||||
|
||||
data.pipe(res);
|
||||
data.on('error', () => res.error('Image not found'));
|
||||
data.on('end', () => res.end());
|
||||
}
|
||||
|
||||
export default withZipline(handler);
|
||||
@@ -2,19 +2,22 @@ import multer from 'multer';
|
||||
import prisma from 'lib/prisma';
|
||||
import zconfig from 'lib/config';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
|
||||
import { createInvisImage, randomChars, hashPassword } from 'lib/util';
|
||||
import { createInvisImage, randomChars } from 'lib/util';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import Logger from 'lib/logger';
|
||||
import { ImageFormat, InvisibleImage } from '@prisma/client';
|
||||
import { format as formatDate } from 'fecha';
|
||||
import { v4 } from 'uuid';
|
||||
import datasource from 'lib/ds';
|
||||
|
||||
const uploader = multer();
|
||||
const uploader = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
});
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
if (req.method !== 'POST') return res.forbid('invalid method');
|
||||
if (!req.headers.authorization) return res.forbid('no authorization');
|
||||
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
token: req.headers.authorization,
|
||||
@@ -23,16 +26,13 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
||||
if (!user) return res.forbid('authorization incorect');
|
||||
if (user.ratelimited) return res.ratelimited();
|
||||
|
||||
await run(uploader.array('file'))(req, res);
|
||||
|
||||
if (!req.files) return res.error('no files');
|
||||
|
||||
if (req.files && req.files.length === 0) return res.error('no files');
|
||||
|
||||
const rawFormat = ((req.headers.format || '') as string).toUpperCase() || 'RANDOM';
|
||||
const format: ImageFormat = Object.keys(ImageFormat).includes(rawFormat) && ImageFormat[rawFormat];
|
||||
const files = [];
|
||||
|
||||
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[${i}] size too big`);
|
||||
@@ -56,10 +56,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
break;
|
||||
}
|
||||
|
||||
let password = null;
|
||||
if (req.headers.password) {
|
||||
password = await hashPassword(req.headers.password as string);
|
||||
}
|
||||
|
||||
let invis: InvisibleImage;
|
||||
const image = await prisma.image.create({
|
||||
@@ -69,20 +65,14 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
userId: user.id,
|
||||
embed: !!req.headers.embed,
|
||||
format,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (req.headers.zws) invis = await createInvisImage(zconfig.uploader.length, image.id);
|
||||
|
||||
await datasource.save(image.file, file.buffer);
|
||||
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
|
||||
if (user.domains.length) {
|
||||
const domain = user.domains[Math.floor(Math.random() * user.domains.length)];
|
||||
files.push(`${domain}${zconfig.uploader.route}/${invis ? invis.invis : image.file}`);
|
||||
} else {
|
||||
files.push(`${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}`);
|
||||
}
|
||||
|
||||
if (user.administrator && zconfig.ratelimit.admin !== 0) {
|
||||
@@ -123,6 +113,8 @@ function run(middleware: any) {
|
||||
}
|
||||
|
||||
export default async function handlers(req, res) {
|
||||
await run(uploader.array('file'))(req, res);
|
||||
|
||||
return withZipline(handler)(req, res);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import config from 'lib/config';
|
||||
import { chunk } from 'lib/util';
|
||||
import { rm } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import Logger from 'lib/logger';
|
||||
import datasource from 'lib/ds';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const user = await req.user();
|
||||
@@ -17,11 +19,10 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
},
|
||||
});
|
||||
|
||||
await datasource.delete(image.file);
|
||||
await rm(join(process.cwd(), config.uploader.directory, image.file));
|
||||
|
||||
Logger.get('image').info(`User ${user.username} (${user.id}) deleted an image ${image.file} (${image.id})`);
|
||||
|
||||
delete image.password;
|
||||
return res.json(image);
|
||||
} else if (req.method === 'PATCH') {
|
||||
if (!req.body.id) return res.error('no file id');
|
||||
@@ -35,7 +36,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
},
|
||||
});
|
||||
|
||||
delete image.password;
|
||||
return res.json(image);
|
||||
} else {
|
||||
let images = await prisma.image.findMany({
|
||||
@@ -43,9 +43,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
userId: user.id,
|
||||
favorite: !!req.query.favorite,
|
||||
},
|
||||
orderBy: {
|
||||
created_at: 'desc',
|
||||
},
|
||||
select: {
|
||||
created_at: true,
|
||||
file: true,
|
||||
|
||||
@@ -2,7 +2,6 @@ import prisma from 'lib/prisma';
|
||||
import { hashPassword } from 'lib/util';
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import Logger from 'lib/logger';
|
||||
import pkg from '../../../../package.json';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const user = await req.user();
|
||||
@@ -52,36 +51,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
data: { systemTheme: req.body.systemTheme },
|
||||
});
|
||||
|
||||
if (req.body.domains) {
|
||||
if (!req.body.domains) await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { domains: [] },
|
||||
});
|
||||
|
||||
const invalidDomains = [];
|
||||
|
||||
for (const domain of req.body.domains) {
|
||||
try {
|
||||
const url = new URL(domain);
|
||||
url.pathname = '/api/version';
|
||||
const res = await fetch(url.toString());
|
||||
if (!res.ok) invalidDomains.push({ domain, reason: 'Got a non OK response' });
|
||||
else {
|
||||
const body = await res.json();
|
||||
if (body?.local !== pkg.version) invalidDomains.push({ domain, reason: 'Version mismatch' });
|
||||
else await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { domains: { push: url.origin } },
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
invalidDomains.push({ domain, reason: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidDomains.length) return res.forbid('Invalid domains', { invalidDomains });
|
||||
}
|
||||
|
||||
const newUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: Number(user.id),
|
||||
@@ -97,7 +66,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
systemTheme: true,
|
||||
token: true,
|
||||
username: true,
|
||||
domains: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import prisma from 'lib/prisma';
|
||||
import Logger from 'lib/logger';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
const user = await req.user();
|
||||
@@ -17,15 +16,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
});
|
||||
if (!deleteUser) return res.forbid('user doesn\'t exist');
|
||||
|
||||
if (req.body.delete_images) {
|
||||
const { count } = await prisma.image.deleteMany({
|
||||
where: {
|
||||
userId: deleteUser.id,
|
||||
},
|
||||
});
|
||||
Logger.get('image').info(`User ${user.username} (${user.id}) deleted ${count} images of user ${deleteUser.username} (${deleteUser.id})`);
|
||||
}
|
||||
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: deleteUser.id,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Cross1Icon, DownloadIcon } from '@modulz/radix-icons';
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const notif = useNotifications();
|
||||
const [versions, setVersions] = React.useState<{ upstream: string, local: string }>(null);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
@@ -35,14 +36,25 @@ export default function Login() {
|
||||
icon: <Cross1Icon />,
|
||||
});
|
||||
} else {
|
||||
await router.push(router.query.url as string || '/dashboard');
|
||||
router.push(router.query.url as string || '/dashboard');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const a = await fetch('/api/user');
|
||||
if (a.ok) await router.push('/dashboard');
|
||||
if (a.ok) router.push('/dashboard');
|
||||
else {
|
||||
const v = await useFetch('/api/version');
|
||||
setVersions(v);
|
||||
if (v.local !== v.upstream) {
|
||||
notif.showNotification({
|
||||
title: 'Update available',
|
||||
message: `A new version of Zipline is available. You are running ${v.local} and the latest version is ${v.upstream}.`,
|
||||
icon: <DownloadIcon />,
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
@@ -59,6 +71,27 @@ export default function Login() {
|
||||
</form>
|
||||
</div>
|
||||
</Center>
|
||||
<Box
|
||||
sx={{
|
||||
zIndex: 99,
|
||||
position: 'fixed',
|
||||
bottom: '10px',
|
||||
right: '20px',
|
||||
|
||||
}}
|
||||
>
|
||||
{versions && (
|
||||
<Tooltip
|
||||
wrapLines
|
||||
width={220}
|
||||
transition='rotate-left'
|
||||
transitionDuration={200}
|
||||
label={versions.local !== versions.upstream ? 'Looks like you are running an outdated version of Zipline. Please update to the latest version.' : 'You are running the latest version of Zipline.'}
|
||||
>
|
||||
<Badge radius='md' size='lg' variant='dot' color={versions.local !== versions.upstream ? 'red' : 'primary'}>{versions.local}</Badge>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
export default function Logout() {
|
||||
const router = useRouter();
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -18,7 +20,7 @@ export default function Logout() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LoadingOverlay visible={true} />
|
||||
<LoadingOverlay visible={visible} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Code() {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch('/r/' + id);
|
||||
if (id && !res.ok) await router.push('/404');
|
||||
if (id && !res.ok) router.push('/404');
|
||||
const data = await res.text();
|
||||
if (id) setPrismRenderCode(data);
|
||||
})();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { GetStaticProps } from 'next';
|
||||
import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Upload from 'components/pages/Upload';
|
||||
import config from 'lib/config';
|
||||
|
||||
export default function UploadPage({ route }) {
|
||||
const { user, loading } = useLogin();
|
||||
@@ -12,9 +14,17 @@ export default function UploadPage({ route }) {
|
||||
<Layout
|
||||
user={user}
|
||||
>
|
||||
<Upload/>
|
||||
<Upload route={route}/>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
route: process.env.ZIPLINE_DOCKER_BUILD === '1' ? '/u' : config.uploader.route,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
UploadPage.title = 'Zipline - Upload';
|
||||
@@ -1,7 +0,0 @@
|
||||
import { version } from '../../package.json';
|
||||
import Logger from '../lib/logger';
|
||||
|
||||
Logger.get('server').info(`starting zipline@${version} server`);
|
||||
|
||||
import Server from './server';
|
||||
new Server();
|
||||
@@ -1,182 +0,0 @@
|
||||
import Router from 'find-my-way';
|
||||
import { NextServer, RequestHandler } from 'next/dist/server/next';
|
||||
import { Image, PrismaClient } from '@prisma/client';
|
||||
import { createServer, IncomingMessage, OutgoingMessage, Server as HttpServer, ServerResponse } from 'http';
|
||||
import next from 'next';
|
||||
import config from '../lib/config';
|
||||
import datasource from '../lib/ds';
|
||||
import { getStats, log, migrations } from './util';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import Logger from '../lib/logger';
|
||||
import mimes from '../../scripts/mimes';
|
||||
import { extname } from 'path';
|
||||
import exts from '../../scripts/exts';
|
||||
|
||||
const serverLog = Logger.get('server');
|
||||
|
||||
export default class Server {
|
||||
public router: Router.Instance<Router.HTTPVersion.V1>;
|
||||
public nextServer: NextServer;
|
||||
public handle: RequestHandler;
|
||||
public prisma: PrismaClient;
|
||||
|
||||
private http: HttpServer;
|
||||
|
||||
public constructor() {
|
||||
this.start();
|
||||
}
|
||||
|
||||
private async start() {
|
||||
// annoy user if they didnt change secret from default "changethis"
|
||||
if (config.core.secret === 'changethis') {
|
||||
serverLog.error('Secret is not set!');
|
||||
serverLog.error('Running Zipline as is, without a randomized secret is not recommended and leaves your instance at risk!');
|
||||
serverLog.error('Please change your secret in the config file or environment variables.');
|
||||
serverLog.error('The config file is located at `config.toml`, or if using docker-compose you can change the variables in the `docker-compose.yml` file.');
|
||||
serverLog.error('It is recomended to use a secret that is alphanumeric and randomized. A way you can generate this is through a password manager you may have.');
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development';
|
||||
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
await migrations();
|
||||
|
||||
this.prisma = new PrismaClient();
|
||||
|
||||
if (config.datasource.type === 'local') {
|
||||
await mkdir(config.datasource.local.directory, { recursive: true });
|
||||
}
|
||||
|
||||
this.nextServer = next({
|
||||
dir: '.',
|
||||
dev,
|
||||
quiet: !dev,
|
||||
hostname: config.core.host,
|
||||
port: config.core.port,
|
||||
});
|
||||
|
||||
this.handle = this.nextServer.getRequestHandler();
|
||||
this.router = Router({
|
||||
defaultRoute: (req, res) => {
|
||||
this.handle(req, res);
|
||||
},
|
||||
});
|
||||
|
||||
this.router.on('GET', `${config.uploader.route}/:id`, async (req, res, params) => {
|
||||
const image = await this.prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ file: params.id },
|
||||
{ invisible: { invis: decodeURI(params.id) } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) await this.rawFile(req, res, params.id);
|
||||
|
||||
if (image.password) await this.handle(req, res);
|
||||
else if (image.embed) await this.handle(req, res);
|
||||
else await this.fileDb(req, res, image);
|
||||
});
|
||||
|
||||
this.router.on('GET', '/r/:id', async (req, res, params) => {
|
||||
const image = await this.prisma.image.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ file: params.id },
|
||||
{ invisible: { invis: decodeURI(params.id) } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (!image) await this.rawFile(req, res, params.id);
|
||||
|
||||
if (image.password) await this.handle(req, res);
|
||||
else await this.rawFileDb(req, res, image);
|
||||
});
|
||||
|
||||
await this.nextServer.prepare();
|
||||
|
||||
this.http = createServer((req, res) => {
|
||||
this.router.lookup(req, res);
|
||||
if (config.core.logger) log(req.url);
|
||||
});
|
||||
|
||||
this.http.on('error', (e) => {
|
||||
serverLog.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
this.http.on('listening', () => {
|
||||
serverLog.info(`listening on ${config.core.host}:${config.core.port}`);
|
||||
});
|
||||
|
||||
this.http.listen(config.core.port, config.core.host ?? '0.0.0.0');
|
||||
|
||||
this.stats();
|
||||
}
|
||||
|
||||
private async rawFile(req: IncomingMessage, res: OutgoingMessage, id: string) {
|
||||
const data = datasource.get(id);
|
||||
if (!data) return this.nextServer.render404(req, res as ServerResponse);
|
||||
const mimetype = mimes[extname(id)] ?? 'application/octet-stream';
|
||||
res.setHeader('Content-Type', mimetype);
|
||||
|
||||
data.pipe(res);
|
||||
data.on('error', () => this.nextServer.render404(req, res as ServerResponse));
|
||||
data.on('end', () => res.end());
|
||||
}
|
||||
|
||||
private async rawFileDb(req: IncomingMessage, res: OutgoingMessage, image: Image) {
|
||||
const data = datasource.get(image.file);
|
||||
if (!data) return this.nextServer.render404(req, res as ServerResponse);
|
||||
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
data.pipe(res);
|
||||
data.on('error', () => this.nextServer.render404(req, res as ServerResponse));
|
||||
data.on('end', () => res.end());
|
||||
|
||||
await this.prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
private async fileDb(req: IncomingMessage, res: OutgoingMessage, image: Image) {
|
||||
const ext = image.file.split('.').pop();
|
||||
if (Object.keys(exts).includes(ext)) return this.handle(req, res as ServerResponse);
|
||||
|
||||
const data = datasource.get(image.file);
|
||||
if (!data) return this.nextServer.render404(req, res as ServerResponse);
|
||||
|
||||
res.setHeader('Content-Type', image.mimetype);
|
||||
data.pipe(res);
|
||||
data.on('error', () => this.nextServer.render404(req, res as ServerResponse));
|
||||
data.on('end', () => res.end());
|
||||
|
||||
await this.prisma.image.update({
|
||||
where: { id: image.id },
|
||||
data: { views: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
private async stats() {
|
||||
const stats = await getStats(this.prisma, datasource);
|
||||
await this.prisma.stats.create({
|
||||
data: {
|
||||
data: stats,
|
||||
},
|
||||
});
|
||||
|
||||
setInterval(async () => {
|
||||
const stats = await getStats(this.prisma, datasource);
|
||||
await this.prisma.stats.create({
|
||||
data: {
|
||||
data: stats,
|
||||
},
|
||||
});
|
||||
if (config.core.logger) serverLog.info('stats updated');
|
||||
}, config.core.stats_interval * 1000);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Config } from 'lib/types';
|
||||
import { object, bool, string, number, boolean, array } from 'yup';
|
||||
|
||||
const validator = object({
|
||||
core: object({
|
||||
secure: bool().default(false),
|
||||
secret: string().min(8).required(),
|
||||
host: string().default('0.0.0.0'),
|
||||
port: number().default(3000),
|
||||
database_url: string().required(),
|
||||
logger: boolean().default(false),
|
||||
stats_interval: number().default(1800),
|
||||
}).required(),
|
||||
datasource: object({
|
||||
type: string().default('local'),
|
||||
local: object({
|
||||
directory: string().default('./uploads'),
|
||||
}),
|
||||
s3: object({
|
||||
access_key_id: string(),
|
||||
secret_access_key: string(),
|
||||
bucket: string(),
|
||||
}).notRequired(),
|
||||
}).required(),
|
||||
uploader: object({
|
||||
route: string().default('/u'),
|
||||
embed_route: string().default('/a'),
|
||||
length: number().default(6),
|
||||
admin_limit: number().default(104900000),
|
||||
user_limit: number().default(104900000),
|
||||
disabled_extensions: array().default([]),
|
||||
}).required(),
|
||||
urls: object({
|
||||
route: string().default('/go'),
|
||||
length: number().default(6),
|
||||
}).required(),
|
||||
ratelimit: object({
|
||||
user: number().default(0),
|
||||
admin: number().default(0),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
export default function validate(config): Config {
|
||||
try {
|
||||
const validated = validator.validateSync(config, { abortEarly: false });
|
||||
if (validated.datasource.type === 's3') {
|
||||
const errors = [];
|
||||
if (!validated.datasource.s3.access_key_id) errors.push('datasource.s3.access_key_id is a required field');
|
||||
if (!validated.datasource.s3.secret_access_key) errors.push('datasource.s3.secret_access_key is a required field');
|
||||
if (!validated.datasource.s3.bucket) errors.push('datasource.s3.bucket is a required field');
|
||||
if (errors.length) throw { errors };
|
||||
}
|
||||
|
||||
return validated as unknown as Config;
|
||||
} catch (e) {
|
||||
if (process.env.ZIPLINE_DOCKER_BUILD) return null;
|
||||
throw `${e.errors.length} errors occured\n${e.errors.map(x => '\t' + x).join('\n')}`;
|
||||
}
|
||||
};
|
||||
4
zip-env.d.ts
vendored
4
zip-env.d.ts
vendored
@@ -1,13 +1,11 @@
|
||||
import type { PrismaClient } from '@prisma/client';
|
||||
import type { Datasource } from 'lib/datasource';
|
||||
import type { Config } from '.lib/types';
|
||||
import type { Config } from './src/lib/types';
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
prisma: PrismaClient;
|
||||
config: Config;
|
||||
datasource: Datasource
|
||||
}
|
||||
|
||||
interface ProcessEnv {
|
||||
|
||||
Reference in New Issue
Block a user