Initial commit
This commit is contained in:
commit
78f8d225ee
21173 changed files with 2907774 additions and 0 deletions
48
app/api/files/download-file/route.ts
Normal file
48
app/api/files/download-file/route.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const BASE_DIR = '/app/files';
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const raw = url.searchParams.get('path');
|
||||
if (!raw) {
|
||||
return NextResponse.json({ error: 'Missing path' }, { status: 400 });
|
||||
}
|
||||
const safe = path.normalize('/' + raw).replace(/^\/+/, '/');
|
||||
const target = path.resolve(BASE_DIR, '.' + safe);
|
||||
|
||||
if (!target.startsWith(BASE_DIR)) {
|
||||
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
||||
}
|
||||
|
||||
const st = await fs.stat(target).catch(() => null);
|
||||
if (!st || !st.isFile()) {
|
||||
return NextResponse.json({ error: 'Not a file' }, { status: 400 });
|
||||
}
|
||||
|
||||
const data = await fs.readFile(target);
|
||||
// naive content-type guess
|
||||
const ext = path.extname(target).toLowerCase();
|
||||
const ctype =
|
||||
ext === '.pdf' ? 'application/pdf' :
|
||||
ext === '.png' ? 'image/png' :
|
||||
ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' :
|
||||
ext === '.webp' ? 'image/webp' :
|
||||
ext === '.txt' ? 'text/plain; charset=utf-8' :
|
||||
'application/octet-stream';
|
||||
|
||||
return new Response(data, {
|
||||
headers: {
|
||||
'Content-Type': ctype,
|
||||
'Content-Length': String(data.byteLength),
|
||||
'Content-Disposition': `inline; filename="${path.basename(target)}"`,
|
||||
'Cache-Control': 'no-store',
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e?.message ?? 'Unknown error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
44
app/api/files/download/route.ts
Normal file
44
app/api/files/download/route.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { stat } from "fs/promises";
|
||||
import { createReadStream } from "fs";
|
||||
import path from "path";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
const ROOT = process.env.FILES_ROOT || "/app/files";
|
||||
|
||||
function safeJoin(root: string, p: string) {
|
||||
const raw = path.normalize("/" + (p || "/"));
|
||||
const abs = path.resolve(root, "." + raw);
|
||||
if (!abs.startsWith(path.resolve(root))) throw new Error("Invalid path");
|
||||
return abs;
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const p = searchParams.get("path");
|
||||
if (!p) return new Response(JSON.stringify({ error: "Missing path" }), { status: 400 });
|
||||
|
||||
const abs = safeJoin(ROOT, p);
|
||||
const s = await stat(abs);
|
||||
if (s.isDirectory()) {
|
||||
return new Response(JSON.stringify({ error: "Is a directory" }), { status: 400 });
|
||||
}
|
||||
|
||||
const stream = createReadStream(abs);
|
||||
const fileName = path.basename(abs);
|
||||
return new Response(stream as any, {
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": String(s.size),
|
||||
"Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`,
|
||||
"Cache-Control": "no-store",
|
||||
}
|
||||
});
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || "Not found";
|
||||
const code = msg === "Invalid path" ? 400 : 404;
|
||||
return new Response(JSON.stringify({ error: msg }), { status: code, headers: { "Cache-Control": "no-store" } });
|
||||
}
|
||||
}
|
||||
50
app/api/files/get/route.ts
Normal file
50
app/api/files/get/route.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const BASE = "/app/files";
|
||||
|
||||
function safeJoin(base: string, reqPath: string) {
|
||||
const rel = reqPath.startsWith("/") ? reqPath : `/${reqPath}`;
|
||||
const full = path.resolve(base, "." + rel);
|
||||
if (!full.startsWith(base)) throw new Error("Outside base");
|
||||
return full;
|
||||
}
|
||||
|
||||
const CONTENT_MAP: Record<string, string> = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".svg": "image/svg+xml",
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".pdf": "application/pdf",
|
||||
};
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const reqPath = url.searchParams.get("path");
|
||||
if (!reqPath) return NextResponse.json({ error: "Missing path" }, { status: 400 });
|
||||
|
||||
const full = safeJoin(BASE, reqPath);
|
||||
const stat = await fs.stat(full);
|
||||
if (!stat.isFile()) return NextResponse.json({ error: "Not a file" }, { status: 400 });
|
||||
|
||||
const data = await fs.readFile(full);
|
||||
const ext = path.extname(full).toLowerCase();
|
||||
const type = CONTENT_MAP[ext] ?? "application/octet-stream";
|
||||
|
||||
return new NextResponse(data, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": type, "Cache-Control": "public, max-age=300" },
|
||||
});
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e?.message ?? "Unknown" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
41
app/api/files/list-files/route.ts
Normal file
41
app/api/files/list-files/route.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const BASE_DIR = '/app/files';
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const raw = url.searchParams.get('path') ?? '/';
|
||||
const safe = path.normalize('/' + raw).replace(/^\/+/, '/'); // normalize & ensure leading slash
|
||||
const target = path.resolve(BASE_DIR, '.' + safe);
|
||||
|
||||
if (!target.startsWith(BASE_DIR)) {
|
||||
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
||||
}
|
||||
|
||||
const st = await fs.stat(target).catch(() => null);
|
||||
if (!st || !st.isDirectory()) {
|
||||
return NextResponse.json({ error: 'Not a directory' }, { status: 400 });
|
||||
}
|
||||
|
||||
const entries = await fs.readdir(target, { withFileTypes: true });
|
||||
const items = await Promise.all(entries.map(async (d) => {
|
||||
const full = path.join(target, d.name);
|
||||
const rel = path.posix.join(safe, d.name).replaceAll('\\', '/');
|
||||
const s = await fs.stat(full);
|
||||
return {
|
||||
name: d.name,
|
||||
path: rel,
|
||||
type: d.isDirectory() ? 'dir' : 'file',
|
||||
size: s.size,
|
||||
mtime: s.mtimeMs,
|
||||
};
|
||||
}));
|
||||
|
||||
return NextResponse.json({ path: safe, items });
|
||||
} catch (e: any) {
|
||||
return NextResponse.json({ error: e?.message ?? 'Unknown error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
43
app/api/files/list/route.ts
Normal file
43
app/api/files/list/route.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// /var/www/makearmy.io/app/app/api/files/list/route.ts
|
||||
import { NextResponse } from "next/server";
|
||||
import { promises as fs } from "fs";
|
||||
import { join, normalize } from "path";
|
||||
|
||||
const ROOT = "/app/files";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const input = searchParams.get("path") || "/";
|
||||
|
||||
// Normalize and lock to ROOT to prevent traversal
|
||||
const safeInput = input.startsWith("/") ? input : `/${input}`;
|
||||
const fullPath = normalize(join(ROOT, `.${safeInput}`));
|
||||
if (!fullPath.startsWith(ROOT)) {
|
||||
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
||||
|
||||
const items = await Promise.all(
|
||||
entries.map(async (d) => {
|
||||
const p = join(fullPath, d.name);
|
||||
const s = await fs.stat(p);
|
||||
return {
|
||||
name: d.name,
|
||||
isDir: d.isDirectory(),
|
||||
size: s.size,
|
||||
mtime: s.mtimeMs,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return NextResponse.json({ path: safeInput, items });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: err?.message ?? String(err) },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
39
app/api/files/raw/route.ts
Normal file
39
app/api/files/raw/route.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import fssync from "node:fs";
|
||||
import path from "node:path";
|
||||
import mime from "mime";
|
||||
|
||||
const BASE = "/app/files";
|
||||
|
||||
function safeJoin(base: string, reqPath: string) {
|
||||
const decoded = decodeURIComponent(reqPath || "/");
|
||||
const normalized = path.posix.normalize("/" + decoded).replace(/^(\.\.(\/|\\|$))+/g, "");
|
||||
const full = path.join(base, normalized);
|
||||
if (!full.startsWith(base)) throw new Error("Invalid path");
|
||||
return full;
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const p = searchParams.get("path");
|
||||
if (!p) return NextResponse.json({ ok: false, error: "Missing path" }, { status: 400 });
|
||||
|
||||
const abs = safeJoin(BASE, p);
|
||||
if (!fssync.existsSync(abs) || !fssync.statSync(abs).isFile()) {
|
||||
return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const stream = fssync.createReadStream(abs);
|
||||
const type = mime.getType(abs) || "application/octet-stream";
|
||||
return new Response(stream as any, {
|
||||
headers: {
|
||||
"Content-Type": type,
|
||||
"Cache-Control": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ ok: false, error: err?.message || "Error" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
50
app/api/files/route.ts
Normal file
50
app/api/files/route.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import fs from "node:fs/promises";
|
||||
import fssync from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const BASE = "/app/files"; // this is /var/www/makearmy.io/app/files on the host
|
||||
|
||||
function safeJoin(base: string, reqPath: string) {
|
||||
const decoded = decodeURIComponent(reqPath || "/");
|
||||
// normalize, strip traversal, and join under BASE
|
||||
const normalized = path.posix.normalize("/" + decoded).replace(/^(\.\.(\/|\\|$))+/g, "");
|
||||
const full = path.join(base, normalized);
|
||||
if (!full.startsWith(base)) throw new Error("Invalid path");
|
||||
return full;
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const p = searchParams.get("path") || "/";
|
||||
const abs = safeJoin(BASE, p);
|
||||
|
||||
const entries = await fs.readdir(abs, { withFileTypes: true });
|
||||
const rows = await Promise.all(entries.map(async (ent) => {
|
||||
const full = path.join(abs, ent.name);
|
||||
const stat = await fs.stat(full);
|
||||
const isDir = ent.isDirectory();
|
||||
return {
|
||||
name: ent.name,
|
||||
type: isDir ? "dir" : "file",
|
||||
size: isDir ? null : stat.size,
|
||||
mtime: stat.mtime.toISOString(),
|
||||
path: path.posix.join(p.endsWith("/") ? p : p + "/", ent.name),
|
||||
// raw download/view URL (served by /api/files/raw)
|
||||
url: isDir ? null : `/api/files/raw?path=${encodeURIComponent(path.posix.join(p, ent.name))}`,
|
||||
};
|
||||
}));
|
||||
|
||||
// Sort: directories first, then files alphabetically
|
||||
rows.sort((a, b) => {
|
||||
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
||||
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, path: p, items: rows });
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ ok: false, error: err?.message || "Error" }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue