Skip to content

Commit 236c963

Browse files
authored
Add delete endpoint
1 parent d155f31 commit 236c963

File tree

1 file changed

+203
-77
lines changed

1 file changed

+203
-77
lines changed

api/index.ts

Lines changed: 203 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import fastify from 'fastify';
2-
import rateLimit from '@fastify/rate-limit';
3-
import multipart from '@fastify/multipart';
4-
import * as FileType from 'file-type';
5-
import * as stream from 'stream';
6-
import * as crypto from 'crypto';
7-
import * as Mega from 'megajs';
8-
9-
import config from '../config';
10-
import { formatBytes, runtime } from '../functions';
11-
import MegaClient from '../mega';
12-
import db from '../database';
13-
import { UploadResponse, UploadMode, DetectedFileType, UploadQuery } from '../types';
14-
import { FastifyRequest, FastifyReply } from 'fastify';
1+
import fastify from "fastify";
2+
import rateLimit from "@fastify/rate-limit";
3+
import multipart from "@fastify/multipart";
4+
import * as FileType from "file-type";
5+
import * as stream from "stream";
6+
import * as crypto from "crypto";
7+
import * as Mega from "megajs";
8+
import config from "../config";
9+
import { formatBytes, runtime } from "../functions";
10+
import MegaClient from "../mega";
11+
import db from "../database";
12+
import { UploadResponse, UploadMode, DetectedFileType, UploadQuery, } from "../types";
13+
import { FastifyRequest, FastifyReply } from "fastify";
1514

1615
const app = fastify({
1716
logger: false,
@@ -21,7 +20,7 @@ const app = fastify({
2120
});
2221

2322
app.register(require("@fastify/static"), {
24-
root: require("path").join(process.cwd(), 'public'),
23+
root: require("path").join(process.cwd(), "public"),
2524
prefix: "/",
2625
});
2726

@@ -33,83 +32,121 @@ app.register(multipart, {
3332
},
3433
});
3534

36-
app.addHook('onSend', (req, rep, _payload, done) => {
37-
if (req.url.startsWith('/media') || req.url.startsWith('/file')) {
38-
rep.header('Cache-Control', `public, max-age=${config.server.cacheTTL}`);
35+
app.addHook("onSend", (req, rep, _payload, done) => {
36+
if (req.url.startsWith("/media") || req.url.startsWith("/file")) {
37+
rep.header("Cache-Control", `public, max-age=${config.server.cacheTTL}`);
3938
}
4039
done();
4140
});
4241

4342
const authOK = (req: FastifyRequest): boolean => {
4443
if (!config.auth?.enable) return true;
45-
const h = req.headers['authorization'];
44+
const h = req.headers["authorization"];
4645
if (!h) return false;
47-
const [t, token] = h.split(' ');
48-
return t === 'Bearer' && config.auth.keys.includes(token);
46+
const [t, token] = h.split(" ");
47+
return t === "Bearer" && config.auth.keys.includes(token);
4948
};
5049

5150
const origin = (req: FastifyRequest): string => {
52-
const s = (req.raw.socket as any).encrypted ? 'https' : 'http';
51+
const s = (req.raw.socket as any).encrypted ? "https" : "http";
5352
const host = req.hostname;
5453
const port = (req.raw.socket as any).localPort;
55-
return host === 'localhost' ? `${s}://${host}:${port}` : `${s}://${host}`;
54+
return host === "localhost" ? `${s}://${host}:${port}` : `${s}://${host}`;
5655
};
5756

5857
const parseParts = async (req: FastifyRequest) => {
59-
let mode = 'single', query: UploadQuery | undefined = undefined;
58+
let mode = "single",
59+
query: UploadQuery | undefined = undefined;
6060
const files: any[] = [];
6161

6262
for await (const part of req.parts()) {
63-
if (part.type === 'field') {
64-
if (part.fieldname === 'mode') mode = part.value || 'single';
65-
if (part.fieldname === 'email' && mode === 'dual') query = { email: part.value };
63+
if (part.type === "field") {
64+
if (part.fieldname === "mode") mode = part.value || "single";
65+
if (part.fieldname === "email" && mode === "dual")
66+
query = { email: part.value };
6667
continue;
6768
}
6869
if (!part.file) continue;
6970

7071
const buf = await part.toBuffer();
71-
const ft = await FileType.fromBuffer(buf) as DetectedFileType | undefined;
72-
if (!ft || !config.server.allowedTypes.includes(ft.mime)) throw new Error('Invalid: ' + ft?.mime);
72+
const ft = (await FileType.fromBuffer(buf)) as DetectedFileType | undefined;
73+
if (!ft || !config.server.allowedTypes.includes(ft.mime))
74+
throw new Error("Invalid: " + ft?.mime);
7375
const d = new Date();
7476
const name = `${d.getDate()}_${d.getMonth() + 1}_${d.getFullYear()}_${Math.random().toString(36).slice(2, 8)}.${ft.ext}`;
75-
const strm = new stream.PassThrough(); strm.end(buf);
76-
files.push({ filename: name, stream: strm, mime: ft.mime, extension: ft.ext });
77+
const strm = new stream.PassThrough();
78+
strm.end(buf);
79+
files.push({
80+
filename: name,
81+
stream: strm,
82+
mime: ft.mime,
83+
extension: ft.ext,
84+
});
7785
}
7886
return { mode, query, files };
7987
};
8088

8189
const mega = new MegaClient(config);
8290

83-
app.post('/upload', async (req: FastifyRequest, rep: FastifyReply) => {
91+
app.post("/upload", async (req: FastifyRequest, rep: FastifyReply) => {
8492
if (!authOK(req)) {
85-
const h = req.headers['authorization'];
86-
return rep.code(h ? 403 : 401).send({ error: h ? 'Invalid Auth' : 'Missing Auth' });
93+
const h = req.headers["authorization"];
94+
return rep
95+
.code(h ? 403 : 401)
96+
.send({ error: h ? "Invalid Auth" : "Missing Auth" });
8797
}
8898
try {
8999
const { mode, query, files } = await parseParts(req);
90-
const ups = await Promise.all(files.map(f => mega.uploadFile(f.filename, f.stream, mode as UploadMode, query)));
100+
const ups = await Promise.all(
101+
files.map((f) =>
102+
mega.uploadFile(f.filename, f.stream, mode as UploadMode, query),
103+
),
104+
);
91105

92106
if (config.autoDelete?.enable) {
93-
ups.forEach(u => mega.scheduleDelete(u.name, config.autoDelete!.minutes));
107+
ups.forEach((u) =>
108+
mega.scheduleDelete(u.name, config.autoDelete!.minutes),
109+
);
94110
}
95111

96112
const o = origin(req);
97113
const out: UploadResponse = { success: true, files: [] };
98114

99115
for (let i = 0; i < ups.length; i++) {
100-
const u = ups[i], f = files[i];
116+
const u = ups[i],
117+
f = files[i];
101118
if (config.FILENAMES && db.supportsCustomFilenames()) {
102-
const custom = crypto.randomBytes(4).toString('hex') + '.' + f.extension;
103-
await db.saveCustomFile({ customFileName: custom, originalMegaUrl: u.url, fileExtension: f.extension });
104-
out.files.push({ url: `${o}/file/${custom}`, name: u.name, size: u.size, formattedSize: formatBytes(u.size), mime: u.mime });
119+
const custom =
120+
crypto.randomBytes(4).toString("hex") + "." + f.extension;
121+
await db.saveCustomFile({
122+
customFileName: custom,
123+
originalMegaUrl: u.url,
124+
fileExtension: f.extension,
125+
});
126+
out.files.push({
127+
url: `${o}/file/${custom}`,
128+
name: u.name,
129+
size: u.size,
130+
formattedSize: formatBytes(u.size),
131+
mime: u.mime,
132+
});
105133
} else {
106-
const url = `${o}/media/${u.url.replace(/^https:\/\/mega\.nz\/file\//, '').replace('#', '@')}`;
107-
out.files.push({ url, name: u.name, size: u.size, formattedSize: formatBytes(u.size), mime: u.mime });
134+
const url = `${o}/media/${u.url.replace(/^https:\/\/mega\.nz\/file\//, "").replace("#", "@")}`;
135+
out.files.push({
136+
url,
137+
name: u.name,
138+
size: u.size,
139+
formattedSize: formatBytes(u.size),
140+
mime: u.mime,
141+
});
108142
}
109143
}
110144
if (config.autoDelete?.enable) {
111145
const sec = config.autoDelete.minutes * 60;
112-
out.files.forEach(f => { f.expires = `${sec}s`; f.formattedExpires = runtime(sec); });
146+
out.files.forEach((f) => {
147+
f.expires = `${sec}s`;
148+
f.formattedExpires = runtime(sec);
149+
});
113150
}
114151
rep.send(out);
115152
} catch (err: any) {
@@ -118,64 +155,153 @@ app.post('/upload', async (req: FastifyRequest, rep: FastifyReply) => {
118155
}
119156
});
120157

121-
app.get('/file/:filename', async (req, rep) => {
158+
app.get("/file/:filename", async (req, rep) => {
122159
const { filename } = req.params as any;
123-
if (!config.FILENAMES || !db.supportsCustomFilenames()) return rep.code(404).send({ error: 'Not enabled' });
160+
if (!config.FILENAMES || !db.supportsCustomFilenames())
161+
return rep.code(404).send({ error: "Not enabled" });
124162
const cf = await db.getCustomFile(filename);
125-
if (!cf) return rep.code(404).send({ error: 'Not found' });
126-
const h = cf.originalMegaUrl.replace(/^https:\/\/mega\.nz\/file\//, '').replace('#', '@').replace('@', '#');
127-
const url = 'https://mega.nz/file/' + h;
163+
if (!cf) return rep.code(404).send({ error: "Not found" });
164+
const h = cf.originalMegaUrl
165+
.replace(/^https:\/\/mega\.nz\/file\//, "")
166+
.replace("#", "@")
167+
.replace("@", "#");
168+
const url = "https://mega.nz/file/" + h;
128169
const file = Mega.File.fromURL(url);
129170
await file.loadAttributes();
130-
rep.header('Content-Type', (file as any).mime || 'application/octet-stream');
131-
rep.header('Content-Disposition', `inline; filename="${filename}"`);
171+
rep.header("Content-Type", (file as any).mime || "application/octet-stream");
172+
rep.header("Content-Disposition", `inline; filename="${filename}"`);
132173
return rep.send(file.download({}));
133174
});
134175

135-
app.get('/media/*', async (req, rep) => {
136-
const h = (req.params as any)['*'].replace('@', '#');
137-
const file = Mega.File.fromURL('https://mega.nz/file/' + h);
176+
app.get("/media/*", async (req, rep) => {
177+
const h = (req.params as any)["*"].replace("@", "#");
178+
const file = Mega.File.fromURL("https://mega.nz/file/" + h);
138179
await file.loadAttributes();
139-
rep.header('Content-Type', (file as any).mime || 'application/octet-stream');
140-
rep.header('Content-Disposition', `inline; filename="${file.name}"`);
180+
rep.header("Content-Type", (file as any).mime || "application/octet-stream");
181+
rep.header("Content-Disposition", `inline; filename="${file.name}"`);
141182
return rep.send(file.download({}));
142183
});
143184

144-
app.get('/info', (_, rep) => rep.send({
145-
request_limit: config.rateLimit.max,
146-
rate_limit: config.rateLimit.timeWindow,
147-
file_size: config.server.maxFileSize,
148-
max_files: config.server.maxFiles,
149-
...(config.autoDelete?.enable && { auto_delete_time: config.autoDelete.minutes }),
150-
}));
151-
152-
app.get('/health', (_, rep) => rep.send({
153-
status: 'ok',
154-
timestamp: new Date().toISOString(),
155-
database: db.isConnected() ? 'connected' : 'disconnected',
156-
database_type: db.getDbType(),
157-
custom_filenames: config.FILENAMES && db.supportsCustomFilenames(),
158-
}));
185+
app.delete("/delete/*", async (req: FastifyRequest, rep: FastifyReply) => {
186+
if (!authOK(req)) {
187+
const h = req.headers["authorization"];
188+
return rep
189+
.code(h ? 403 : 401)
190+
.send({ error: h ? "Invalid Auth" : "Missing Auth" });
191+
}
192+
try {
193+
const path = (req.params as any)["*"];
194+
let fileName: string;
195+
let dltdb = false;
196+
197+
if (path.startsWith("file/")) {
198+
const id = path.replace("file/", "");
199+
200+
if (config.FILENAMES && db.supportsCustomFilenames()) {
201+
const customFile = await db.getCustomFile(id);
202+
if (!customFile) {
203+
return rep.code(404).send({ error: "File not found in database" });
204+
}
205+
206+
try {
207+
fileName = await mega.getFileNameFromUrl(customFile.originalMegaUrl);
208+
const fileDeleted = await mega.deleteFileByName(fileName);
209+
210+
if (!fileDeleted) {
211+
await db.deleteCustomFile(id);
212+
return rep
213+
.code(404)
214+
.send({
215+
error: "File not found in any acc deleted it from database",
216+
});
217+
}
218+
219+
await db.deleteCustomFile(id);
220+
dltdb = true;
221+
} catch (error) {
222+
await db.deleteCustomFile(id);
223+
return rep
224+
.code(404)
225+
.send({ error: "File not found on acc deleted it from database" });
226+
}
227+
} else {
228+
return rep.code(400).send({ error: "Custom filenames not enabled" });
229+
}
230+
} else if (path.startsWith("media/")) {
231+
const hash = path.replace("media/", "").replace("@", "#");
232+
const megaUrl = `https://mega.nz/file/${hash}`;
233+
234+
try {
235+
fileName = await mega.getFileNameFromUrl(megaUrl);
236+
const fileDeleted = await mega.deleteFileByName(fileName);
237+
if (!fileDeleted) {
238+
return rep.code(404).send({ error: "File not found in any account" });
239+
}
240+
} catch (error) {
241+
return rep.code(404).send({ error: "File not found on Mega" });
242+
}
243+
} else {
244+
return rep.code(400).send({ error: "Invalid path." });
245+
}
246+
247+
rep.send({
248+
success: true,
249+
message: "File deleted successfully",
250+
deletedFrom: { megaAcc: true, database: dltdb },
251+
});
252+
} catch (err: any) {
253+
console.error("Delete error:", err);
254+
rep.code(400).send({ error: err.message || "Failed to delete file" });
255+
}
256+
});
257+
258+
app.get("/info", (_, rep) =>
259+
rep.send({
260+
request_limit: config.rateLimit.max,
261+
rate_limit: config.rateLimit.timeWindow,
262+
file_size: config.server.maxFileSize,
263+
max_files: config.server.maxFiles,
264+
...(config.autoDelete?.enable && {
265+
auto_delete_time: config.autoDelete.minutes,
266+
}),
267+
}),
268+
);
269+
270+
app.get("/health", (_, rep) =>
271+
rep.send({
272+
status: "ok",
273+
timestamp: new Date().toISOString(),
274+
database: db.isConnected() ? "connected" : "disconnected",
275+
database_type: db.getDbType(),
276+
custom_filenames: config.FILENAMES && db.supportsCustomFilenames(),
277+
}),
278+
);
159279

160280
const shutdown = async (s: string) => {
161281
console.log(`Got ${s}, shutting down...`);
162-
try { await mega.cleanup(); await app.close(); process.exit(0); }
163-
catch (err) { console.error(err); process.exit(1); }
282+
try {
283+
await mega.cleanup();
284+
await app.close();
285+
process.exit(0);
286+
} catch (err) {
287+
console.error(err);
288+
process.exit(1);
289+
}
164290
};
165291

166-
process.on('SIGTERM', () => shutdown('SIGTERM'));
167-
process.on('SIGINT', () => shutdown('SIGINT'));
292+
process.on("SIGTERM", () => shutdown("SIGTERM"));
293+
process.on("SIGINT", () => shutdown("SIGINT"));
168294

169295
const start = async () => {
170296
try {
171297
await mega.initialize();
172-
console.log('Instance ready');
173-
await app.listen({ port: config.server.port, host: '0.0.0.0' });
298+
console.log("Instance ready");
299+
await app.listen({ port: config.server.port, host: "0.0.0.0" });
174300
console.log(`Server at:${config.server.port}`);
175301
} catch (err) {
176302
console.error(err);
177303
process.exit(1);
178304
}
179305
};
180306

181-
start();
307+
start();

0 commit comments

Comments
 (0)