import { FileState, FileType, fileTypeExt, fileTypeMime } from "../types/file";
import Snowflake from "./snowflake";
import User from "./user";
import Collection from "@discordjs/collection";
import { SnowflakeResolvable, SnowflakeString } from "../types/snowflake";
import { s3 } from "../lib/s3";
import { DeleteObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import Config from "./config";
import { RequestContext } from "./requestContext";
import { DBFile, DBFileFull, prisma } from "../../prisma";
import { UserType } from "../types/basicUser";
/**
* File data class
*/
class File {
id: Snowflake;
ctx: RequestContext;
author: User;
name: string;
description: string;
tags: string[];
state: FileState;
type: FileType;
length: number;
/**
* @param {object} param0
*/
constructor({
id = Snowflake.newSnowflake(),
ctx,
author,
name = "",
state,
description = "",
tags = [],
type = FileType.IMAGE,
length = 0
} : {
id?: Snowflake,
ctx: RequestContext,
author: User,
name?: string,
state: FileState,
description?: string,
tags?: string[],
type?: FileType,
length?: number
}) {
this.id = id;
this.ctx = ctx;
this.name = name;
this.state = state;
this.type = type;
this.author = author;
this.description = description;
this.tags = tags;
this.length = length;
}
/**
* @param {DBFile | DBFileFull} data
* @param {RequestContext} ctx
*/
static async fromDatabase(data: DBFile | DBFileFull, ctx: RequestContext) {
if(ctx.files.has(data.snowflake.toString())) {
return ctx.files.get(data.snowflake.toString())!;
}
if(data.state === FileState.DELETED && !ctx.user?.comparePosition(UserType.MODERATOR)) {
throw new Error("File does not exist");
}
return new File({
ctx,
id: new Snowflake(data.snowflake),
author: "author" in data ?
await User.fromDatabase(data.author, ctx) :
await User.getUser(data.authorId, ctx),
name: data.name,
state: data.state,
type: data.type,
description: data.description,
tags: JSON.parse(data.tags),
length: data.length
});
}
/**
* @param {SnowflakeResolvable} id
* @param {RequestContext} ctx
* @return {Promise<User>}
*/
static async load(id: SnowflakeResolvable, ctx: RequestContext): Promise<File> {
const fid = (new Snowflake(id)).toBigInt();
if(ctx.files.has(fid.toString())) return ctx.files.get(fid.toString())!;
const file = await prisma.file.findUniqueOrThrow({
where: {
snowflake: fid
}
});
return await File.fromDatabase(file, ctx);
}
/**
* @param {object} param0 file data
* @return {Promise<File>} file
*/
static async newFile({
id = Snowflake.newSnowflake(),
ctx,
author,
name = "",
description = "",
tags = [],
state,
type = FileType.IMAGE,
length = 0
}: {
id?: Snowflake,
ctx: RequestContext,
author: User,
name?: string,
description?: string,
tags?: string[],
state: number,
type?: FileType,
length?: number
}): Promise<File> {
if(!ctx.client) throw new Error("Cannot create file without client");
const file = new File({
id,
ctx,
author,
name,
type,
description,
state,
tags,
length
});
await prisma.file.create({
data: {
snowflake: id.toBigInt(),
author: {
connect: {
snowflake: author.id.toBigInt()
}
},
name,
type,
description,
state,
tags: JSON.stringify(tags),
length
}
});
return file;
}
/**
* Searches files
* @param {RequestContext} ctx
* @param {any} options
* @return {Promise<Collection<SnowflakeString, File>>}
*/
static search(
ctx: RequestContext,
options: { limit?: number, offset?: number, tags?: string[], query?: string }
): Promise<Collection<SnowflakeString, File>> {
return new Promise((resolve, reject) => {
let query = "SELECT * FROM files WHERE state != 3";
const variables = [];
if(options.tags) {
query += " AND json_contains(`tags`, ?)";
variables.push(JSON.stringify(options.tags));
}
if(options.query) {
query += " AND MATCH (name, description, tags) AGAINST (? IN BOOLEAN MODE)";
let query2 = options.query;
query2 = `(${query2.split(" ")
.map((t) => t + "*").join(" ")}) ("${query2.replace(/"/g, "")}")`;
variables.push(query2);
}
query += " ORDER BY snowflake DESC";
if(options.limit) {
query += " LIMIT " + options.limit;
}
if(options.offset) {
query += " OFFSET " + options.offset;
}
ctx.conn.query(query, variables, async (err, results) => {
if(err) return reject(err);
var col = new Collection<SnowflakeString, File>();
for(var result of results) {
col.set(result.snowflake, new File({
id: new Snowflake(BigInt(result.snowflake)),
ctx,
author: await User.getUser(result.author, ctx),
name: result.name,
description: result.description,
tags: JSON.parse(result.tags),
type: result.type,
state: result.state,
length: result.length
}));
}
resolve(col);
});
});
}
/**
* Returns the number of files that match the search
* @param {RequestContext} ctx
* @param {any} options
* @return {Promise<number>}
*/
static searchCount(ctx: RequestContext, options: { tags?: string[], query?: string }):
Promise<number> {
return new Promise((resolve, reject) => {
let query = "SELECT COUNT(*) as count FROM files WHERE state != 3";
const variables = [];
if(options.tags) {
query += " AND json_contains(`tags`, ?)";
variables.push(JSON.stringify(options.tags));
}
if(options.query) {
query += " AND MATCH (name, description, tags) AGAINST (? IN BOOLEAN MODE)";
variables.push(options.query);
}
query += " ORDER BY snowflake DESC";
ctx.conn.query(query, variables, async (err, results) => {
if(err) return reject(err);
resolve(results[0].count);
});
});
}
/**
* Updates file (sync DB)
* @return {Promise<void>}
*/
async update(): Promise<void> {
await prisma.file.update({
where: {
snowflake: this.id.toBigInt()
},
data: {
name: this.name,
type: this.type,
description: this.description,
state: this.state,
tags: JSON.stringify(this.tags)
}
});
}
/**
* Deletes file (user)
*/
async delete(): Promise<void> {
this.state = FileState.DELETED;
await this.update();
}
/**
* Hard deletes file (moderator nuke)
*/
async hardDelete(): Promise<void> {
await prisma.file.delete({
where: {
snowflake: this.id.toBigInt()
}
});
await s3.send(new DeleteObjectCommand({
Key: `${this.id.toString()}.${fileTypeExt[this.type]}`,
Bucket: Config.s3.bucket
}));
}
/**
* Formats URL (for server)
* @return {string}
*/
formatURL() {
return `https://cdn.animasher.net/file/${Config.s3.bucket}/${
this.id.toString()}.${fileTypeExt[this.type]}`;
}
/**
* Formats public URL (for client)
* @return {string}
*/
formatPublicURL() {
return `${Config.domains.files ?
(!Config.isDev ? "https://" : "http://") + Config.domains.files :
"https://static.animasher.net"
}/v1/file/${this.id.toString()}`;
}
/**
* Formats public URL with extension (for client)
* @return {string}
*/
formatPublicURLWithExt() {
return this.formatPublicURL() + "." + fileTypeExt[this.type];
}
/**
* Formats public URL (for client)
* @param {SnowflakeString} id
* @return {string}
*/
static formatPublicURL(id: SnowflakeString) {
return `${Config.domains.files ?
(!Config.isDev ? "https://" : "http://") + Config.domains.files :
"https://static.animasher.net"
}/v1/file/${id}`;
}
/**
* Creates S3 upload command
* @return {PutObjectCommand}
*/
uploadCommand() {
return new PutObjectCommand({
Key: `${this.id.toString()}.${fileTypeExt[this.type]}`,
Bucket: Config.s3.bucket,
ContentType: fileTypeMime[this.type],
CacheControl: "public, max-age=604800, immutable, stale-while-revalidate=86400"
});
}
/**
* Creates a JSON-compatible object
* @return {Object}
*/
toJSON() {
return {
id: this.id.getSnowflake(),
author: this.author.toJSON(),
name: this.name,
url: this.formatPublicURL(),
type: this.type,
description: this.description,
tags: this.tags,
state: this.state,
length: this.length
};
}
}
export default File;
Source