diff --git a/prisma/migrations/20210830210159_zws/migration.sql b/prisma/migrations/20210830210159_zws/migration.sql
new file mode 100644
index 0000000..05795e5
--- /dev/null
+++ b/prisma/migrations/20210830210159_zws/migration.sql
@@ -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;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index b940de3..44bbafb 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -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 {
diff --git a/server/index.js b/server/index.js
index 4c28096..f521949 100644
--- a/server/index.js
+++ b/server/index.js
@@ -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);
diff --git a/src/components/pages/Manage.tsx b/src/components/pages/Manage.tsx
index c0b7717..d5914f2 100644
--- a/src/components/pages/Manage.tsx
+++ b/src/components/pages/Manage.tsx
@@ -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() {
ShareX Config
+
>
);
}
diff --git a/src/lib/util.ts b/src/lib/util.ts
index c075471..6a0e7c9 100644
--- a/src/lib/util.ts
+++ b/src/lib/util.ts
@@ -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 {
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 => {
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
}
});
diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts
index 7d48eb7..161e50f 100644
--- a/src/pages/api/upload.ts
+++ b/src/pages/api/upload.ts
@@ -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}`,
@@ -35,13 +36,15 @@ async function handler(req: NextApiReq, res: NextApiRes) {
userId: user.id
}
});
+
+ 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}`
});
}