commit ed8c61ea955e12541ce6ffda6a64cf958ea13dfd Author: Hajamieli Date: Sat Aug 12 06:15:32 2023 +0300 upload diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d67a29b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +readme.md +build +.env +./uploads +./tmp \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8f3086e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +uploads/* +tmp/* +.env +site +site/* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6455a9a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM denoland/deno:latest as base + +WORKDIR /app + +COPY . ./ + +RUN deno cache server.ts + +CMD ["run", "--allow-net", "--allow-write", "--allow-read", "--allow-env", "server.ts"] + diff --git a/fileTypes.ts b/fileTypes.ts new file mode 100644 index 0000000..de099e3 --- /dev/null +++ b/fileTypes.ts @@ -0,0 +1,19 @@ + +const allowedFiletypes = new Map() +allowedFiletypes.set("image/jpeg", ".jpg") +allowedFiletypes.set("image/png", ".png") +allowedFiletypes.set("image/avif", ".avif") +allowedFiletypes.set("image/gif", ".gif") +allowedFiletypes.set("image/webp", ".webp") +allowedFiletypes.set("image/apng", ".apng") +allowedFiletypes.set("image/bmp", ".bmp") +allowedFiletypes.set("image/svg+xml", ".svg") +allowedFiletypes.set("image/tiff", ".tiff") +allowedFiletypes.set("video/webm", ".webm") +allowedFiletypes.set("video/mp4",".mp4") +allowedFiletypes.set("video/mpeg",".mpeg") +allowedFiletypes.set("video/x-msvideo", ".avi") +allowedFiletypes.set("application/epub+zip", ".epub") +allowedFiletypes.set("application/pdf", ".pdf") + +export default allowedFiletypes \ No newline at end of file diff --git a/getFileHash.ts b/getFileHash.ts new file mode 100644 index 0000000..9312167 --- /dev/null +++ b/getFileHash.ts @@ -0,0 +1,16 @@ +import { crypto, toHashString } from 'https://deno.land/std@0.176.0/crypto/mod.ts'; + +const getFileBuffer = (filePath: string) => { + const file = Deno.openSync(filePath); + const buf = new Uint8Array(file.statSync().size); + file.readSync(buf); + file.close(); + return buf; +}; + +const getBuffer = (data: BufferSource) => toHashString(crypto.subtle.digestSync('SHA-256', data)); + +const getFileHash = (filePath: string) => getBuffer(getFileBuffer(filePath)); + + +export default getFileHash diff --git a/mongodb.ts b/mongodb.ts new file mode 100644 index 0000000..82ba191 --- /dev/null +++ b/mongodb.ts @@ -0,0 +1,10 @@ +import { Bson, MongoClient } from "https://deno.land/x/mongo@v0.31.1/mod.ts"; +import "https://deno.land/std@0.178.0/dotenv/load.ts"; + +const client = new MongoClient(); + +await client.connect(Deno.env.get("MONGOURL")); + +const db = client.database('hajabot') + +export default db; \ No newline at end of file diff --git a/routes.ts b/routes.ts new file mode 100644 index 0000000..0f2d1c8 --- /dev/null +++ b/routes.ts @@ -0,0 +1,219 @@ +import { RouterContext } from "https://deno.land/x/oak/mod.ts" +import "https://deno.land/std@0.178.0/dotenv/load.ts"; +import { writeFileSync, copy } from "https://deno.land/std/fs/mod.ts"; +import { renderFileToString } from "https://deno.land/x/dejs/mod.ts"; + +import getFileHash from './getFileHash.ts'; +import allowedFiletypes from './fileTypes.ts'; +import db from './mongodb.ts'; +const imageCollection = db.collection('images'); +const userTable = db.collection('users'); + + + +export const icons = async (ctx: RouterContext, next: any) => { + const identifier = ctx.params.filename + + console.log(["t_192.png", "t_48.png", "t_96.png"].indexOf(identifier)) + + if (["t_192.png", "t_48.png", "t_96.png"].indexOf(identifier) != -1){ + const img = await Deno.readFile(`${Deno.cwd()}/site/i/${ctx.params.filename}`); + ctx.response.type = "image/png"; + ctx.response.body = img; + } else { + next() + } +} + +export const announce = async (ctx: RouterContext) => { + const body = await ctx.request.body({ type: 'json'}).value; + + const count = await imageCollection.findOne({hash:body.hash}); + + if(body.size >= 50000000 && body.usercode == "anonymous"){ + ctx.response.status = 403 + } + else if(count){ + ctx.response.body = {file: count.filename}; + ctx.response.status = 409 + }else{ + if(body.size >= 50000000){ + const users = await userTable.findOne({token:body.usercode}); + if(users){ + ctx.response.status = 200 + }else{ + ctx.response.status = 403 + } + }else{ + ctx.response.status = 200 + } + } +} + +export const chunks = async (ctx: RouterContext) => { + const hash = ctx.request.headers.get("file-hash"); + const number = ctx.request.headers.get("file-hash"); + + const body = await ctx.request.body({ type: 'form-data'}); + const formData = await body.value.read(); + + const size = ctx.request.headers.get("content-range").split("/")[1] + const data = await Deno.readFile(formData.files[0].filename) + + Deno.writeFileSync(`${Deno.env.get("TMPPATH")}${hash}.${ctx.request.headers.get("content-number")}.temp`, data) + + ctx.response.status = 200 +} + +export const mergeChunks = async (ctx: RouterContext) => { + const body = await ctx.request.body({ type: 'json'}).value; + + const ts = new Date(); + let newname = `${ts.valueOf()}${allowedFiletypes.get(body.type)}` + + let files = [] + for await (const dirEntry of Deno.readDir(Deno.env.get("TMPPATH"))) { + if(dirEntry.name.includes(body.hash) && dirEntry.isFile){ + files.push(dirEntry.name) + } + + } + + const tempFile = await Deno.makeTempFile(); + + for (let i = 0; i < files.length; i++) { + let file = files.find(elem => elem.includes(`.${i}.temp`)) + const data = await Deno.readFileSync(`${Deno.env.get("TMPPATH")}${file}`) + + await Deno.writeFileSync(tempFile, data, {"append": true}) + Deno.remove(`${Deno.env.get("TMPPATH")}${file}`); + } + + await copy(tempFile, `${Deno.env.get("IMAGEPATH")}${newname}`) + + const id = await imageCollection.insertOne({ + hash:body.hash, + mime:body.type, + filename:newname, + usercode:body.usercode, + time:ts + }); + + + const newhash = await getFileHash(tempFile) + if(body.hash == newhash){ + ctx.response.body = {"file":newname} + ctx.response.status = 201 + }else{ + ctx.response.status = 418 + } +} + +export const getImage = async (ctx: RouterContext) => { + const identifier = ctx.params.filename; + + const info = await imageCollection.findOne({filename:identifier}); + + if(!info){ + ctx.response.status = 404; + ctx.response.body = {message: 'image does not exist'}; + return + }else{ + const img = await Deno.readFile(`${Deno.env.get("IMAGEPATH")}${identifier}`); + + ctx.response.type = info.mime; + ctx.response.body = img; + } + +} + +export const deleteImage = async (ctx: RouterContext) => { + const identifier = ctx.params.filename; + + const count = await imageCollection.findOne({filename:identifier}); + + if(!count){ + ctx.response.status = 404; + ctx.response.body = {message: 'image does not exist'}; + return + }else{ + console.log(count) + + if(ctx.request.headers.get("usercode")==count.usercode){ + const deleted = await imageCollection.deleteOne({_id:count._id}) + if(!deleted){ + ctx.response.status = 500; + }else{ + await Deno.remove(`${Deno.env.get("IMAGEPATH")}${count.filename}`); + ctx.response.status = 204 + } + + }else{ + ctx.response.body = {message: 'Unauthorized'}; + ctx.response.status = 401 + + } + } +} + +export const uploadImage = async (ctx: RouterContext) => { + const body = await ctx.request.body({ type: 'form-data'}).value; + const formData = await body.read(); + + const usercode = ctx.request.headers.get("usercode") ? ctx.request.headers.get("usercode") : "anonymous"; + + let promises = []; + let files = []; + + formData.files.forEach(file => promises.push(new Promise(async (resolve, reject)=>{ + if(!allowedFiletypes.get(file.contentType)){ + reject("rejected filetype") + }else{ + const timestamp = new Date() + const hash = getFileHash(file.filename) + + const count = await imageCollection.findOne({hash:hash}); + + if(!count){ + const newFilename = `${timestamp.valueOf()}${allowedFiletypes.get(file.contentType)}` + copy(file.filename, `${Deno.env.get("IMAGEPATH")}${newFilename}`, {overwrite: true}) + const id = await imageCollection.insertOne({ + hash:hash, + mime:file.contentType, + filename:newFilename, + usercode:usercode, + time:timestamp + }); + + files.push(newFilename) + resolve(newFilename) + }else{ + files.push(count.filename) + resolve(count.filename) + } + } + }))) + + const result = await Promise.allSettled(promises) + + const rejected = await result.filter(obj => Object.keys(obj).some(key => obj[key].includes("rejected"))) + + if(rejected.length >= 1){ + ctx.response.status = 405; + ctx.response.body = {message:rejected[0].reason} + }else{ + if(files.length > 1){ + ctx.response.body = files + }else{ + ctx.response.body = {"img":files[0]} + } + + ctx.response.status = 201 + } +} + + + + + + diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..ed82960 --- /dev/null +++ b/server.ts @@ -0,0 +1,34 @@ +import { Application, Router } from "https://deno.land/x/oak/mod.ts"; +import { getImage, uploadImage, deleteImage, chunks, mergeChunks, announce, icons} from './routes.ts'; +import "https://deno.land/std@0.178.0/dotenv/load.ts"; + + +const router = new Router(); + +router.get('/', async(ctx: RouterContext, next: any) => { + try { + await ctx.send({ + root: `${Deno.cwd()}/site`, + index: "index.html" + }) + } catch { + await next() + } + +}) +.get('/i/:filename', icons) +.post('/upload', uploadImage) +.post('/announce', announce) +.post('/uploadchunk', chunks) +.post('/finish', mergeChunks) +.delete('/image/:filename', deleteImage) +.get('/image/:filename', getImage) + + +const app = new Application(); + +app.use(router.routes()) +app.use(router.allowedMethods()); + +app.listen({ port: Deno.env.get("PORT") }); +console.log(`Server is running on port ${Deno.env.get("PORT")}`)