From 365aace294fc7f1c60a677f8ba105f0a083b45a1 Mon Sep 17 00:00:00 2001 From: "Jack W." Date: Tue, 24 Oct 2023 16:27:08 -0400 Subject: [PATCH] refactor: :recycle: Use real async in file.ts, change FileUploadSettings to match FilePointer properties --- src/server/lib/files.ts | 730 +++++++++++++------------ src/server/routes/api/v0/primaryApi.ts | 363 ++++++------ 2 files changed, 586 insertions(+), 507 deletions(-) diff --git a/src/server/lib/files.ts b/src/server/lib/files.ts index 259761e..e2d6997 100644 --- a/src/server/lib/files.ts +++ b/src/server/lib/files.ts @@ -1,14 +1,16 @@ -import axios from "axios"; -import Discord, { Client, TextBasedChannel } from "discord.js"; -import { readFile, writeFile } from "fs"; -import { Readable } from "node:stream"; -import crypto from "node:crypto"; -import { files } from "./accounts"; +import axios from "axios" +import Discord, { Client, Message, TextBasedChannel } from "discord.js" +import { readFile, writeFile } from "node:fs/promises" +import { Readable } from "node:stream" +import crypto from "node:crypto" +import { files } from "./accounts" -import * as Accounts from "./accounts"; +import * as Accounts from "./accounts" export let id_check_regex = /[A-Za-z0-9_\-\.\!\=\:\&\$\,\+\;\@\~\*\(\)\']+/ -export let alphanum = Array.from("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") +export let alphanum = Array.from( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" +) // bad solution but whatever @@ -19,72 +21,66 @@ export type FileVisibility = "public" | "anonymous" | "private" * @param length Length of the ID * @returns a random alphanumeric string */ -export function generateFileId(length:number=5) { +export function generateFileId(length: number = 5) { let fid = "" for (let i = 0; i < length; i++) { - fid += alphanum[crypto.randomInt(0,alphanum.length)] + fid += alphanum[crypto.randomInt(0, alphanum.length)] } return fid } -export interface FileUploadSettings { - name?: string, - mime: string, - uploadId?: string, - owner?:string -} +export type FileUploadSettings = Partial> & + Pick & { uploadId?: string } export interface Configuration { - maxDiscordFiles: number, - maxDiscordFileSize: number, - targetGuild: string, - targetChannel: string, - requestTimeout: number, - maxUploadIdLength: number, + maxDiscordFiles: number + maxDiscordFileSize: number + targetGuild: string + targetChannel: string + requestTimeout: number + maxUploadIdLength: number accounts: { - registrationEnabled: boolean, + registrationEnabled: boolean requiredForUpload: boolean - }, + } - trustProxy: boolean, + trustProxy: boolean forceSSL: boolean } export interface FilePointer { - filename:string, - mime:string, - messageids:string[], - owner?:string, - sizeInBytes?:number, - tag?:string, - visibility?:FileVisibility, - reserved?: boolean, + filename: string + mime: string + messageids: string[] + owner?: string + sizeInBytes?: number + tag?: string + visibility?: FileVisibility + reserved?: boolean chunkSize?: number } export interface StatusCodeError { - status: number, + status: number message: string } /* */ export default class Files { - config: Configuration client: Client - files: {[key:string]:FilePointer} = {} + files: { [key: string]: FilePointer } = {} uploadChannel?: TextBasedChannel constructor(client: Client, config: Configuration) { + this.config = config + this.client = client - this.config = config; - this.client = client; - - client.on("ready",() => { + client.on("ready", () => { console.log("Discord OK!") - + client.guilds.fetch(config.targetGuild).then((g) => { g.channels.fetch(config.targetChannel).then((a) => { if (a?.isTextBased()) { @@ -94,168 +90,163 @@ export default class Files { }) }) - readFile(process.cwd()+"/.data/files.json",(err,buf) => { - if (err) {console.log(err);return} - this.files = JSON.parse(buf.toString() || "{}") - }) - + readFile(process.cwd() + "/.data/files.json") + .then((buf) => { + this.files = JSON.parse(buf.toString() || "{}") + }) + .catch(console.error) } - + /** * @description Uploads a new file - * @param settings Settings for your new upload - * @param fBuffer Buffer containing file content + * @param metadata Settings for your new upload + * @param buffer Buffer containing file content * @returns Promise which resolves to the ID of the new file */ - uploadFile(settings:FileUploadSettings,fBuffer:Buffer):Promise { - return new Promise(async (resolve,reject) => { - if (!this.uploadChannel) { - reject({status:503,message:"server is not ready - please try again later"}) - return + async uploadFile( + metadata: FileUploadSettings, + buffer: Buffer + ): Promise { + if (!this.uploadChannel) + throw { + status: 503, + message: "server is not ready - please try again later", } - if (!settings.name || !settings.mime) { - reject({status:400,message:"missing name/mime"}); - return + if (!metadata.filename || !metadata.mime) + throw { status: 400, message: "missing filename/mime" } + + let uploadId = (metadata.uploadId || generateFileId()).toString() + + if ( + (uploadId.match(id_check_regex) || [])[0] != uploadId || + uploadId.length > this.config.maxUploadIdLength + ) + throw { status: 400, message: "invalid id" } + + if ( + this.files[uploadId] && + (metadata.owner + ? this.files[uploadId].owner != metadata.owner + : true) + ) + throw { + status: 400, + message: "you are not the owner of this file id", } - if (!settings.owner && this.config.accounts.requiredForUpload) { - reject({status:401,message:"an account is required for upload"}); - return - } - - let uploadId = (settings.uploadId || generateFileId()).toString(); - - if ((uploadId.match(id_check_regex) || [])[0] != uploadId || uploadId.length > this.config.maxUploadIdLength) { - reject({status:400,message:"invalid id"});return - } - - if (this.files[uploadId] && (settings.owner ? this.files[uploadId].owner != settings.owner : true)) { - reject({status:400,message:"you are not the owner of this file id"}); - return + if (this.files[uploadId] && this.files[uploadId].reserved) + throw { + status: 400, + message: + "already uploading this file. if your file is stuck in this state, contact an administrator", } - if (this.files[uploadId] && this.files[uploadId].reserved) { - reject({status:400,message:"already uploading this file. if your file is stuck in this state, contact an administrator"}); - return - } + if (metadata.filename.length > 128) + throw { status: 400, message: "name too long" } - if (settings.name.length > 128) { - reject({status:400,message:"name too long"}); - return - } + if (metadata.mime.length > 128) + throw { status: 400, message: "mime too long" } - if (settings.mime.length > 128) { - reject({status:400,message:"mime too long"}); - return - } + // reserve file, hopefully should prevent + // large files breaking - // reserve file, hopefully should prevent - // large files breaking + let existingFile = this.files[uploadId] - let ogf = this.files[uploadId] + // save - this.files[uploadId] = { - filename:settings.name, - messageids:[], - mime:settings.mime, - sizeInBytes:0, + if (metadata.owner) { + await files.index(metadata.owner, uploadId) + } - owner:settings.owner, - visibility: settings.owner ? "private" : "public", - reserved: true, + // get buffer + if ( + buffer.byteLength >= + this.config.maxDiscordFileSize * this.config.maxDiscordFiles + ) + throw { status: 400, message: "file too large" } - chunkSize: this.config.maxDiscordFileSize - } - - // save - - if (settings.owner) { - await files.index(settings.owner,uploadId) - } - - // get buffer - if (fBuffer.byteLength >= (this.config.maxDiscordFileSize*this.config.maxDiscordFiles)) { - reject({status:400,message:"file too large"}); - return - } - - // generate buffers to upload - let toUpload = [] - for (let i = 0; i < Math.ceil(fBuffer.byteLength/this.config.maxDiscordFileSize); i++) { - toUpload.push( - fBuffer.subarray( - i*this.config.maxDiscordFileSize, - Math.min( - fBuffer.byteLength, - (i+1)*this.config.maxDiscordFileSize - ) + // generate buffers to upload + let toUpload = [] + for ( + let i = 0; + i < Math.ceil(buffer.byteLength / this.config.maxDiscordFileSize); + i++ + ) { + toUpload.push( + buffer.subarray( + i * this.config.maxDiscordFileSize, + Math.min( + buffer.byteLength, + (i + 1) * this.config.maxDiscordFileSize ) ) + ) + } + + // begin uploading + let uploadTmplt: Discord.AttachmentBuilder[] = toUpload.map((e) => { + return new Discord.AttachmentBuilder(e).setName( + Math.random().toString().slice(2) + ) + }) + let uploadGroups = [] + + for (let i = 0; i < Math.ceil(uploadTmplt.length / 10); i++) { + uploadGroups.push(uploadTmplt.slice(i * 10, (i + 1) * 10)) + } + + let msgIds = [] + + for (const uploadGroup of uploadGroups) { + let message = await this.uploadChannel + .send({ + files: uploadGroup, + }) + .catch((e) => { + console.error(e) + }) + + if (message && message instanceof Message) { + msgIds.push(message.id) + } else { + if (!existingFile) delete this.files[uploadId] + else this.files[uploadId] = existingFile + throw { status: 500, message: "please try again" } } - - // begin uploading - let uploadTmplt:Discord.AttachmentBuilder[] = toUpload.map((e) => { - return new Discord.AttachmentBuilder(e) - .setName(Math.random().toString().slice(2)) - }) - let uploadGroups = [] - for (let i = 0; i < Math.ceil(uploadTmplt.length/10); i++) { - uploadGroups.push(uploadTmplt.slice(i*10,((i+1)*10))) + } + + // this code deletes the files from discord, btw + // if need be, replace with job queue system + + if (existingFile && this.uploadChannel) { + for (let x of existingFile.messageids) { + this.uploadChannel.messages + .delete(x) + .catch((err) => console.error(err)) } - - let msgIds = [] - - for (let i = 0; i < uploadGroups.length; i++) { + } - let ms = await this.uploadChannel.send({ - files:uploadGroups[i] - }).catch((e) => {console.error(e)}) + const { filename, mime, owner } = metadata + return this.writeFile(uploadId, { + filename, + messageids: msgIds, + mime, + owner, + sizeInBytes: buffer.byteLength, - if (ms) { - msgIds.push(ms.id) - } else { - if (!ogf) delete this.files[uploadId] - else this.files[uploadId] = ogf - reject({status:500,message:"please try again"}); return - } - } + visibility: existingFile + ? existingFile.visibility + : metadata.owner + ? Accounts.getFromId(metadata.owner)?.defaultFileVisibility + : undefined, + // so that json.stringify doesnt include tag:undefined + ...((existingFile || {}).tag ? { tag: existingFile.tag } : {}), - // this code deletes the files from discord, btw - // if need be, replace with job queue system - - if (ogf&&this.uploadChannel) { - for (let x of ogf.messageids) { - this.uploadChannel.messages.delete(x).catch(err => console.error(err)) - } - } - - resolve(await this.writeFile( - uploadId, - { - filename:settings.name, - messageids:msgIds, - mime:settings.mime, - sizeInBytes:fBuffer.byteLength, - - owner:settings.owner, - visibility: ogf ? ogf.visibility - : ( - settings.owner - ? Accounts.getFromId(settings.owner)?.defaultFileVisibility - : undefined - ), - // so that json.stringify doesnt include tag:undefined - ...((ogf||{}).tag ? {tag:ogf.tag} : {}), - - chunkSize: this.config.maxDiscordFileSize - } - )) - - + chunkSize: this.config.maxDiscordFileSize, }) } - + // fs /** @@ -264,24 +255,26 @@ export default class Files { * @param file FilePointer representing the new file * @returns Promise which resolves to the file's ID */ - writeFile(uploadId: string, file: FilePointer):Promise { - return new Promise((resolve, reject) => { + async writeFile(uploadId: string, file: FilePointer): Promise { + this.files[uploadId] = file - this.files[uploadId] = file - - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => { - - if (err) { - reject({status:500,message:"server may be misconfigured, contact admin for help"}); - delete this.files[uploadId]; - return + return writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify( + this.files, + null, + process.env.NODE_ENV === "development" ? 4 : undefined + ) + ) + .then(() => uploadId) + .catch(() => { + delete this.files[uploadId] + throw { + status: 500, + message: + "server may be misconfigured, contact admin for help", } - - resolve(uploadId) - }) - - }) } /** @@ -290,139 +283,183 @@ export default class Files { * @param range Byte range to get * @returns A `Readable` containing the file's contents */ - readFileStream(uploadId: string, range?: {start:number, end:number}):Promise { - return new Promise(async (resolve,reject) => { - if (!this.uploadChannel) { - reject({status:503,message:"server is not ready - please try again later"}) - return + async readFileStream( + uploadId: string, + range?: { start: number; end: number } + ): Promise { + if (!this.uploadChannel) { + throw { + status: 503, + message: "server is not ready - please try again later", + } + } + + if (this.files[uploadId]) { + let file = this.files[uploadId] + + let scan_msg_begin = 0, + scan_msg_end = file.messageids.length - 1, + scan_files_begin = 0, + scan_files_end = -1 + + let useRanges = range && file.chunkSize && file.sizeInBytes + + // todo: figure out how to get typesccript to accept useRanges + // i'm too tired to look it up or write whatever it wnats me to do + if (range && file.chunkSize && file.sizeInBytes) { + // Calculate where to start file scans... + + scan_files_begin = Math.floor(range.start / file.chunkSize) + scan_files_end = Math.ceil(range.end / file.chunkSize) - 1 + + scan_msg_begin = Math.floor(scan_files_begin / 10) + scan_msg_end = Math.ceil(scan_files_end / 10) } - if (this.files[uploadId]) { - let file = this.files[uploadId] + let attachments: Discord.Attachment[] = [] - let - scan_msg_begin = 0, - scan_msg_end = file.messageids.length-1, - scan_files_begin = 0, - scan_files_end = -1 + /* File updates */ + let file_updates: Pick = + {} + let atSIB: number[] = [] // kepes track of the size of each file... - let useRanges = range && file.chunkSize && file.sizeInBytes; - - // todo: figure out how to get typesccript to accept useRanges - // i'm too tired to look it up or write whatever it wnats me to do - if (range && file.chunkSize && file.sizeInBytes) { - - // Calculate where to start file scans... - - scan_files_begin = Math.floor(range.start / file.chunkSize) - scan_files_end = Math.ceil(range.end / file.chunkSize) - 1 - - scan_msg_begin = Math.floor(scan_files_begin / 10) - scan_msg_end = Math.ceil(scan_files_end / 10) - - } - - let attachments: Discord.Attachment[] = []; - - /* File updates */ - let file_updates: Pick = {} - let atSIB: number[] = [] // kepes track of the size of each file... - - for (let xi = scan_msg_begin; xi < scan_msg_end+1; xi++) { - - let msg = await this.uploadChannel.messages.fetch(file.messageids[xi]).catch(() => {return null}) - if (msg?.attachments) { - - let attach = Array.from(msg.attachments.values()) - for (let i = (useRanges && xi == scan_msg_begin ? ( scan_files_begin - (xi*10) ) : 0); i < (useRanges && xi == scan_msg_end ? ( scan_files_end - (xi*10) + 1 ) : attach.length); i++) { - - attachments.push(attach[i]) - atSIB.push(attach[i].size) - - } - - } - - } - - if (!file.sizeInBytes) file_updates.sizeInBytes = atSIB.reduce((a,b) => a+b, 0); - if (!file.chunkSize) file_updates.chunkSize = atSIB[0] - if (Object.keys(file_updates).length) { // if file_updates not empty - // i gotta do these weird workarounds, ts is weird sometimes - // originally i was gonna do key is keyof FilePointer but for some reason - // it ended up making typeof file[key] never??? so - // its 10pm and chinese people suck at being quiet so i just wanna get this over with - // chinese is the worst language in terms of volume lmao - let valid_fp_keys = ["sizeInBytes", "chunkSize"] - let isValidFilePointerKey = (key: string): key is "sizeInBytes" | "chunkSize" => valid_fp_keys.includes(key) - - for (let [key,value] of Object.entries(file_updates)) { - if (isValidFilePointerKey(key)) file[key] = value - } - - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => {}) - } - - let position = 0; - - let getNextChunk = async () => { - let scanning_chunk = attachments[position] - if (!scanning_chunk) { + for (let xi = scan_msg_begin; xi < scan_msg_end + 1; xi++) { + let msg = await this.uploadChannel.messages + .fetch(file.messageids[xi]) + .catch(() => { return null - } - - let d = await axios.get( - scanning_chunk.url, - { - responseType:"arraybuffer", - headers: { - ...(useRanges ? { - "Range": `bytes=${position == 0 && range && file.chunkSize ? range.start-(scan_files_begin*file.chunkSize) : "0"}-${position == attachments.length-1 && range && file.chunkSize ? range.end-(scan_files_end*file.chunkSize) : ""}` - } : {}) - } - } - ).catch((e:Error) => {console.error(e)}) - - position++; - - if (d) { - return d.data - } else { - reject({status:500,message:"internal server error"}) - return "__ERR" + }) + if (msg?.attachments) { + let attach = Array.from(msg.attachments.values()) + for ( + let i = + useRanges && xi == scan_msg_begin + ? scan_files_begin - xi * 10 + : 0; + i < + (useRanges && xi == scan_msg_end + ? scan_files_end - xi * 10 + 1 + : attach.length); + i++ + ) { + attachments.push(attach[i]) + atSIB.push(attach[i].size) } } - - let ord:number[] = [] - // hopefully this regulates it? - let lastChunkSent = true - - let dataStream = new Readable({ - read(){ - if (!lastChunkSent) return - lastChunkSent = false - getNextChunk().then(async (nextChunk) => { - if (nextChunk == "__ERR") {this.destroy(new Error("file read error")); return} - let response = this.push(nextChunk) - - if (!nextChunk) return // EOF - - while (response) { - let nextChunk = await getNextChunk() - response = this.push(nextChunk) - if (!nextChunk) return - } - lastChunkSent = true - }) - } - }) - - resolve(dataStream) - - } else { - reject({status:404,message:"not found"}) } - }) + + if (!file.sizeInBytes) + file_updates.sizeInBytes = atSIB.reduce((a, b) => a + b, 0) + if (!file.chunkSize) file_updates.chunkSize = atSIB[0] + if (Object.keys(file_updates).length) { + // if file_updates not empty + // i gotta do these weird workarounds, ts is weird sometimes + // originally i was gonna do key is keyof FilePointer but for some reason + // it ended up making typeof file[key] never??? so + // its 10pm and chinese people suck at being quiet so i just wanna get this over with + // chinese is the worst language in terms of volume lmao + let valid_fp_keys = ["sizeInBytes", "chunkSize"] + let isValidFilePointerKey = ( + key: string + ): key is "sizeInBytes" | "chunkSize" => + valid_fp_keys.includes(key) + + for (let [key, value] of Object.entries(file_updates)) { + if (isValidFilePointerKey(key)) file[key] = value + } + + // The original was a callback so I don't think I'm supposed to `await` this -Jack + writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify( + this.files, + null, + process.env.NODE_ENV === "development" ? 4 : undefined + ) + ) + } + + let position = 0 + + let getNextChunk = async () => { + let scanning_chunk = attachments[position] + if (!scanning_chunk) { + return null + } + + let d = await axios + .get(scanning_chunk.url, { + responseType: "arraybuffer", + headers: { + ...(useRanges + ? { + Range: `bytes=${ + position == 0 && + range && + file.chunkSize + ? range.start - + scan_files_begin * + file.chunkSize + : "0" + }-${ + position == attachments.length - 1 && + range && + file.chunkSize + ? range.end - + scan_files_end * file.chunkSize + : "" + }`, + } + : {}), + }, + }) + .catch((e: Error) => { + console.error(e) + }) + + position++ + + if (d) { + return d.data + } else { + throw { + status: 500, + message: "internal server error", + } + } + } + + let ord: number[] = [] + // hopefully this regulates it? + let lastChunkSent = true + + let dataStream = new Readable({ + read() { + if (!lastChunkSent) return + lastChunkSent = false + getNextChunk().then(async (nextChunk) => { + if (nextChunk == "__ERR") { + this.destroy(new Error("file read error")) + return + } + let response = this.push(nextChunk) + + if (!nextChunk) return // EOF + + while (response) { + let nextChunk = await getNextChunk() + response = this.push(nextChunk) + if (!nextChunk) return + } + lastChunkSent = true + }) + }, + }) + + return dataStream + } else { + throw { status: 404, message: "not found" } + } } /** @@ -430,33 +467,41 @@ export default class Files { * @param uploadId Target file's ID * @param noWrite Whether or not the change should be written to disk. Enable for bulk deletes */ - unlink(uploadId:string, noWrite: boolean = false):Promise { - return new Promise(async (resolve,reject) => { - let tmp = this.files[uploadId]; - if (!tmp) {resolve(); return} - if (tmp.owner) { - let id = files.deindex(tmp.owner,uploadId,noWrite); - if (id) await id - } - // this code deletes the files from discord, btw - // if need be, replace with job queue system + async unlink(uploadId: string, noWrite: boolean = false): Promise { + let tmp = this.files[uploadId] + if (!tmp) { + return + } + if (tmp.owner) { + let id = files.deindex(tmp.owner, uploadId, noWrite) + if (id) await id + } + // this code deletes the files from discord, btw + // if need be, replace with job queue system - if (!this.uploadChannel) {reject(); return} - for (let x of tmp.messageids) { - this.uploadChannel.messages.delete(x).catch(err => console.error(err)) - } - - delete this.files[uploadId]; - if (noWrite) {resolve(); return} - writeFile(process.cwd()+"/.data/files.json",JSON.stringify(this.files),(err) => { - if (err) { - this.files[uploadId] = tmp // !! this may not work, since tmp is a link to this.files[uploadId]? - reject() - } else { - resolve() - } - }) + if (!this.uploadChannel) { + return + } + for (let x of tmp.messageids) { + this.uploadChannel.messages + .delete(x) + .catch((err) => console.error(err)) + } + delete this.files[uploadId] + if (noWrite) { + return + } + return writeFile( + process.cwd() + "/.data/files.json", + JSON.stringify( + this.files, + null, + process.env.NODE_ENV === "development" ? 4 : undefined + ) + ).catch((err) => { + this.files[uploadId] = tmp // !! this may not work, since tmp is a link to this.files[uploadId]? + throw err }) } @@ -465,8 +510,7 @@ export default class Files { * @param uploadId Target file's ID * @returns FilePointer for the file */ - getFilePointer(uploadId:string):FilePointer { + getFilePointer(uploadId: string): FilePointer { return this.files[uploadId] } - } diff --git a/src/server/routes/api/v0/primaryApi.ts b/src/server/routes/api/v0/primaryApi.ts index afdcec8..30b98a6 100644 --- a/src/server/routes/api/v0/primaryApi.ts +++ b/src/server/routes/api/v0/primaryApi.ts @@ -1,203 +1,238 @@ -import bodyParser from "body-parser"; -import express, { Router } from "express"; -import * as Accounts from "../../../lib/accounts"; -import * as auth from "../../../lib/auth"; +import bodyParser from "body-parser" +import express, { Router } from "express" +import * as Accounts from "../../../lib/accounts" +import * as auth from "../../../lib/auth" import axios, { AxiosResponse } from "axios" -import { type Range } from "range-parser"; -import multer, {memoryStorage} from "multer" +import { type Range } from "range-parser" +import multer, { memoryStorage } from "multer" -import ServeError from "../../../lib/errors"; -import Files from "../../../lib/files"; -import { getAccount, requiresPermissions } from "../../../lib/middleware"; +import ServeError from "../../../lib/errors" +import Files from "../../../lib/files" +import { getAccount, requiresPermissions } from "../../../lib/middleware" let parser = bodyParser.json({ - type: ["text/plain","application/json"] + type: ["text/plain", "application/json"], }) -export let primaryApi = Router(); +export let primaryApi = Router() -const multerSetup = multer({storage:memoryStorage()}) +const multerSetup = multer({ storage: memoryStorage() }) let config = require(`${process.cwd()}/config.json`) -primaryApi.use(getAccount); +primaryApi.use(getAccount) -module.exports = function(files: Files) { +module.exports = function (files: Files) { + primaryApi.get( + ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], + async (req: express.Request, res: express.Response) => { + let acc = res.locals.acc as Accounts.Account - primaryApi.get(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], async (req:express.Request,res:express.Response) => { - - let acc = res.locals.acc as Accounts.Account + let file = files.getFilePointer(req.params.fileId) + res.setHeader("Access-Control-Allow-Origin", "*") + res.setHeader("Content-Security-Policy", "sandbox allow-scripts") + if (req.query.attachment == "1") + res.setHeader("Content-Disposition", "attachment") - let file = files.getFilePointer(req.params.fileId) - res.setHeader("Access-Control-Allow-Origin", "*") - res.setHeader("Content-Security-Policy","sandbox allow-scripts") - if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment") - - if (file) { - - if (file.visibility == "private") { - if (acc?.id != file.owner) { - ServeError(res,403,"you do not own this file") - return - } + if (file) { + if (file.visibility == "private") { + if (acc?.id != file.owner) { + ServeError(res, 403, "you do not own this file") + return + } - if (auth.getType(auth.tokenFor(req)) == "App" && auth.getPermissions(auth.tokenFor(req))?.includes("private")) { - ServeError(res,403,"insufficient permissions") - return - } - } - - let range: Range | undefined - - res.setHeader("Content-Type",file.mime) - if (file.sizeInBytes) { - res.setHeader("Content-Length",file.sizeInBytes) - - if (file.chunkSize) { - let rng = req.range(file.sizeInBytes) - if (rng) { - - // error handling - if (typeof rng == "number") { - res.status(rng == -1 ? 416 : 400).send() - return - } - if (rng.type != "bytes") { - res.status(400).send(); - return - } - - // set ranges var - let rngs = Array.from(rng) - if (rngs.length != 1) { res.status(400).send(); return } - range = rngs[0] - + if ( + auth.getType(auth.tokenFor(req)) == "App" && + auth + .getPermissions(auth.tokenFor(req)) + ?.includes("private") + ) { + ServeError(res, 403, "insufficient permissions") + return } } - } - // supports ranges - + let range: Range | undefined - files.readFileStream(req.params.fileId, range).then(async stream => { + res.setHeader("Content-Type", file.mime) + if (file.sizeInBytes) { + res.setHeader("Content-Length", file.sizeInBytes) - if (range) { - res.status(206) - res.header("Content-Length", (range.end-range.start + 1).toString()) - res.header("Content-Range", `bytes ${range.start}-${range.end}/${file.sizeInBytes}`) + if (file.chunkSize) { + let rng = req.range(file.sizeInBytes) + if (rng) { + // error handling + if (typeof rng == "number") { + res.status(rng == -1 ? 416 : 400).send() + return + } + if (rng.type != "bytes") { + res.status(400).send() + return + } + + // set ranges var + let rngs = Array.from(rng) + if (rngs.length != 1) { + res.status(400).send() + return + } + range = rngs[0] + } + } } - stream.pipe(res) - - }).catch((err) => { - ServeError(res,err.status,err.message) - }) - } else { - ServeError(res, 404, "file not found") - } - - }) + // supports ranges - primaryApi.head(["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], (req: express.Request, res:express.Response) => { - let file = files.getFilePointer(req.params.fileId) - - if ( - file.visibility == "private" - && ( - res.locals.acc?.id != file.owner - || (auth.getType(auth.tokenFor(req)) == "App" && auth.getPermissions(auth.tokenFor(req))?.includes("private")) - ) - ) { - res.status(403).send() - return - } - - res.setHeader("Access-Control-Allow-Origin", "*") - res.setHeader("Content-Security-Policy","sandbox allow-scripts") - - if (req.query.attachment == "1") res.setHeader("Content-Disposition", "attachment") - - if (!file) { - res.status(404) - res.send() - } else { - res.setHeader("Content-Type",file.mime) - if (file.sizeInBytes) { - res.setHeader("Content-Length",file.sizeInBytes) + files + .readFileStream(req.params.fileId, range) + .then(async (stream) => { + if (range) { + res.status(206) + res.header( + "Content-Length", + (range.end - range.start + 1).toString() + ) + res.header( + "Content-Range", + `bytes ${range.start}-${range.end}/${file.sizeInBytes}` + ) + } + stream.pipe(res) + }) + .catch((err) => { + ServeError(res, err.status, err.message) + }) + } else { + ServeError(res, 404, "file not found") } - if (file.chunkSize) { - res.setHeader("Accept-Ranges", "bytes") - } - res.send() } - }) + ) + + primaryApi.head( + ["/file/:fileId", "/cpt/:fileId/*", "/:fileId"], + (req: express.Request, res: express.Response) => { + let file = files.getFilePointer(req.params.fileId) + + if ( + file.visibility == "private" && + (res.locals.acc?.id != file.owner || + (auth.getType(auth.tokenFor(req)) == "App" && + auth + .getPermissions(auth.tokenFor(req)) + ?.includes("private"))) + ) { + res.status(403).send() + return + } + + res.setHeader("Access-Control-Allow-Origin", "*") + res.setHeader("Content-Security-Policy", "sandbox allow-scripts") + + if (req.query.attachment == "1") + res.setHeader("Content-Disposition", "attachment") + + if (!file) { + res.status(404) + res.send() + } else { + res.setHeader("Content-Type", file.mime) + if (file.sizeInBytes) { + res.setHeader("Content-Length", file.sizeInBytes) + } + if (file.chunkSize) { + res.setHeader("Accept-Ranges", "bytes") + } + res.send() + } + } + ) // upload handlers - primaryApi.post("/upload", requiresPermissions("upload"), multerSetup.single('file'), async (req,res) => { - - let acc = res.locals.acc as Accounts.Account + primaryApi.post( + "/upload", + requiresPermissions("upload"), + multerSetup.single("file"), + async (req, res) => { + let acc = res.locals.acc as Accounts.Account - if (req.file) { - try { - let prm = req.header("monofile-params") - let params:{[key:string]:any} = {} - if (prm) { - params = JSON.parse(prm) + if (req.file) { + try { + let prm = req.header("monofile-params") + let params: { [key: string]: any } = {} + if (prm) { + params = JSON.parse(prm) + } + + files + .uploadFile( + { + owner: acc?.id, + + uploadId: params.uploadId, + filename: req.file.originalname, + mime: req.file.mimetype, + }, + req.file.buffer + ) + .then((uID) => res.send(uID)) + .catch((stat) => { + res.status(stat.status) + res.send(`[err] ${stat.message}`) + }) + } catch { + res.status(400) + res.send("[err] bad request") } - - files.uploadFile({ - owner: acc?.id, - - uploadId:params.uploadId, - name:req.file.originalname, - mime:req.file.mimetype - },req.file.buffer) - .then((uID) => res.send(uID)) - .catch((stat) => { - res.status(stat.status); - res.send(`[err] ${stat.message}`) - }) - } catch { + } else { res.status(400) res.send("[err] bad request") } - } else { - res.status(400) - res.send("[err] bad request") } - }) + ) - primaryApi.post("/clone", requiresPermissions("upload"), bodyParser.json({type: ["text/plain","application/json"]}) ,(req,res) => { - - let acc = res.locals.acc as Accounts.Account + primaryApi.post( + "/clone", + requiresPermissions("upload"), + bodyParser.json({ type: ["text/plain", "application/json"] }), + (req, res) => { + let acc = res.locals.acc as Accounts.Account - try { - axios.get(req.body.url,{responseType:"arraybuffer"}).then((data:AxiosResponse) => { - - files.uploadFile({ - owner: acc?.id, - - name:req.body.url.split("/")[req.body.url.split("/").length-1] || "generic", - mime:data.headers["content-type"], - uploadId:req.body.uploadId - },Buffer.from(data.data)) - .then((uID) => res.send(uID)) - .catch((stat) => { - res.status(stat.status); - res.send(`[err] ${stat.message}`) + try { + axios + .get(req.body.url, { responseType: "arraybuffer" }) + .then((data: AxiosResponse) => { + files + .uploadFile( + { + owner: acc?.id, + filename: + req.body.url.split("/")[ + req.body.url.split("/").length - 1 + ] || "generic", + mime: data.headers["content-type"], + uploadId: req.body.uploadId, + }, + Buffer.from(data.data) + ) + .then((uID) => res.send(uID)) + .catch((stat) => { + res.status(stat.status) + res.send(`[err] ${stat.message}`) + }) }) - - }).catch((err) => { - console.log(err) - res.status(400) - res.send(`[err] failed to fetch data`) - }) - } catch { - res.status(500) - res.send("[err] an error occured") + .catch((err) => { + console.log(err) + res.status(400) + res.send(`[err] failed to fetch data`) + }) + } catch { + res.status(500) + res.send("[err] an error occured") + } } - }) + ) return primaryApi -} \ No newline at end of file +}