feat(api): add support for zws images
This commit is contained in:
parent
16ecdf41af
commit
912f716362
6 changed files with 81 additions and 41 deletions
25
prisma/migrations/20210830210159_zws/migration.sql
Normal file
25
prisma/migrations/20210830210159_zws/migration.sql
Normal 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;
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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}`
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue