feat(api): add support for zws images

This commit is contained in:
diced 2021-08-30 20:56:34 -07:00
parent 16ecdf41af
commit 912f716362
No known key found for this signature in database
GPG key ID: 85AB64C74535D76E
6 changed files with 81 additions and 41 deletions

View file

@ -0,0 +1,25 @@
/*
Warnings:
- A unique constraint covering the columns `[imageId]` on the table `InvisibleImage` will be added. If there are existing duplicate values, this will fail.
- Added the required column `imageId` to the `InvisibleImage` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "InvisibleImage" DROP CONSTRAINT "InvisibleImage_id_fkey";
-- DropIndex
DROP INDEX "InvisibleImage_id_unique";
-- AlterTable
CREATE SEQUENCE "invisibleimage_id_seq";
ALTER TABLE "InvisibleImage" ADD COLUMN "imageId" INTEGER NOT NULL,
ALTER COLUMN "id" SET DEFAULT nextval('invisibleimage_id_seq'),
ADD PRIMARY KEY ("id");
ALTER SEQUENCE "invisibleimage_id_seq" OWNED BY "InvisibleImage"."id";
-- CreateIndex
CREATE UNIQUE INDEX "InvisibleImage_imageId_unique" ON "InvisibleImage"("imageId");
-- AddForeignKey
ALTER TABLE "InvisibleImage" ADD FOREIGN KEY ("imageId") REFERENCES "Image"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -49,10 +49,11 @@ model Image {
}
model InvisibleImage {
id Int
image Image @relation(fields: [id], references: [id])
id Int @id @default(autoincrement())
invis String @unique
imageId Int
image Image @relation(fields: [imageId], references: [id])
}
model Url {

View file

@ -64,40 +64,46 @@ function shouldUseYarn() {
const parts = req.url.split('/');
if (!parts[2] || parts[2] === '') return;
const data = await getFile(config.uploader.directory, parts[2]);
if (!data) {
app.render404(req, res);
let image = await prisma.image.findFirst({
where: {
OR: { file: parts[2] },
OR: { 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 {
let image = await prisma.image.findFirst({
where: {
OR: {
file: parts[2],
},
OR: {
invisible: {
invis: decodeURI(parts[2])
}
}
}
});
if (image) {
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
}
}
where: { id: image.id },
data: { views: { increment: 1 } }
});
res.setHeader('Content-Type', image.mimetype);
res.end(data);
} else {
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);
}
res.end(data);
}
} else {
handle(req, res);

View file

@ -84,8 +84,8 @@ export default function Manage() {
const [severity, setSeverity] = useState('success');
const [message, setMessage] = useState('Saved');
const genShareX = withEmbed => {
let config = {
const genShareX = (withEmbed: boolean = false, withZws: boolean = false) => {
const config = {
Version: '13.2.1',
Name: 'Zipline',
DestinationType: 'ImageUploader, TextUploader',
@ -93,15 +93,17 @@ export default function Manage() {
RequestURL: `${window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '')}/api/upload`,
Headers: {
Authorization: user?.token,
...(withEmbed && {Embed: 'true'})
...(withEmbed && {Embed: 'true'}),
...(withZws && {ZWS: 'true'})
},
URL: '$json:url$',
Body: 'MultipartFormData',
FileFormName: 'file'
};
var pseudoElement = document.createElement('a');
pseudoElement.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t')));
pseudoElement.setAttribute('download', `zipline${withEmbed ? '_embed' : ''}.sxcu`);
const pseudoElement = document.createElement('a');
pseudoElement.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t')));
pseudoElement.setAttribute('download', `zipline${withEmbed ? '_embed' : ''}${withZws ? '_zws' : ''}.sxcu`);
pseudoElement.style.display = 'none';
document.body.appendChild(pseudoElement);
pseudoElement.click();
@ -245,6 +247,7 @@ export default function Manage() {
<Typography variant='h4' py={2}>ShareX Config</Typography>
<Button variant='contained' onClick={() => genShareX(false)} startIcon={<Download />}>ShareX Config</Button>
<Button variant='contained' sx={{ marginLeft: 1 }} onClick={() => genShareX(true)} startIcon={<Download />}>ShareX Config with Embed</Button>
<Button variant='contained' sx={{ marginLeft: 1 }} onClick={() => genShareX(false, true)} startIcon={<Download />}>ShareX Config with ZWS</Button>
</>
);
}

View file

@ -1,8 +1,9 @@
import { createHmac, timingSafeEqual } from 'crypto';
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
import { hash, verify } from 'argon2';
import { readdir, stat } from 'fs/promises';
import { join } from 'path';
import prisma from './prisma';
import { InvisibleImage } from '@prisma/client';
export async function hashPassword(s: string): Promise<string> {
return await hash(s);
@ -89,13 +90,14 @@ export function bytesToRead(bytes: number) {
}
export function createInvisURL(length: number) {
// some parts from https://github.com/tycrek/ass/blob/master/generators/lengthGen.js
const invisibleCharset = ['\u200B', '\u2060', '\u200C', '\u200D'];
for (var i = 0, output = ''; i <= length; ++i) output += invisibleCharset[Math.floor(Math.random() * 4)];
return output;
return [...randomBytes(length)].map((byte) => invisibleCharset[Number(byte) % invisibleCharset.length]).join('').slice(1).concat(invisibleCharset[0]);
}
export function createInvis(length: number, imageId: number) {
const retry = async () => {
const retry = async (): Promise<InvisibleImage> => {
const invis = createInvisURL(length);
const existing = await prisma.invisibleImage.findUnique({
@ -109,7 +111,7 @@ export function createInvis(length: number, imageId: number) {
const inv = await prisma.invisibleImage.create({
data: {
invis,
id: imageId
imageId
}
});

View file

@ -2,7 +2,7 @@ import multer from 'multer';
import prisma from 'lib/prisma';
import zconfig from 'lib/config';
import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline';
import { randomChars } from 'lib/util';
import { createInvis, randomChars } from 'lib/util';
import { writeFile } from 'fs/promises';
import { join } from 'path';
import Logger from 'lib/logger';
@ -26,8 +26,9 @@ async function handler(req: NextApiReq, res: NextApiRes) {
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);
let invis;
const image = await prisma.image.create({
data: {
file: `${rand}.${ext}`,
@ -36,12 +37,14 @@ async function handler(req: NextApiReq, res: NextApiRes) {
}
});
if (req.headers.zws) invis = await createInvis(zconfig.uploader.length, image.id);
await writeFile(join(process.cwd(), zconfig.uploader.directory, image.file), req.file.buffer);
Logger.get('image').info(`User ${user.username} (${user.id}) uploaded an image ${image.file} (${image.id})`);
return res.json({
url: `${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${req.headers.embed ? zconfig.uploader.embed_route : zconfig.uploader.route}/${image.file}`
url: `${zconfig.core.secure ? 'https' : 'http'}://${req.headers.host}${req.headers.embed ? zconfig.uploader.embed_route : zconfig.uploader.route}/${invis ? invis.invis : image.file}`
});
}