first commit

This commit is contained in:
2026-01-02 15:49:01 +03:00
commit 4348f76a7c
80 changed files with 10133 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
import fs from "node:fs/promises";
import path from "node:path";
import { readDb, writeDb } from "../storage/jsondb";
import { ArchiveStatus } from "../types";
import { nowIso } from "../utils/time";
import { config } from "../config";
export const setArchiveStatus = async (status: ArchiveStatus) => {
const db = await readDb();
db.archives[status.hash] = status;
await writeDb(db);
return status;
};
export const getArchiveStatus = async (hash: string) => {
const db = await readDb();
const existing = db.archives[hash];
const torrentPath = path.join(config.torrentArchiveDir, `${hash}.torrent`);
try {
await fs.access(torrentPath);
if (!existing || existing.status !== "READY") {
const updated: ArchiveStatus = {
hash,
status: "READY",
torrentFilePath: torrentPath,
source: existing?.source ?? "manual",
updatedAt: nowIso(),
};
db.archives[hash] = updated;
await writeDb(db);
return updated;
}
} catch (error) {
// File does not exist; fall back to stored status.
}
return existing;
};
export const createPendingArchive = async (hash: string) => {
const status: ArchiveStatus = {
hash,
status: "PENDING",
updatedAt: nowIso(),
};
return setArchiveStatus(status);
};

View File

@@ -0,0 +1,41 @@
import fs from "node:fs/promises";
import path from "node:path";
import { config } from "../config";
import { logger } from "../utils/logger";
export const generateTorrentFile = async (
magnet: string,
hash: string
): Promise<string> => {
const targetPath = path.join(config.torrentArchiveDir, `${hash}.torrent`);
const { default: WebTorrent } = await import("webtorrent");
const client = new WebTorrent();
return new Promise((resolve, reject) => {
const torrent = client.add(magnet, { path: config.dataDir });
const timeout = setTimeout(() => {
client.destroy();
reject(new Error("Metadata fetch timeout"));
}, 120_000);
torrent.on("metadata", async () => {
clearTimeout(timeout);
try {
const buffer = torrent.torrentFile;
await fs.writeFile(targetPath, buffer);
resolve(targetPath);
} catch (error) {
reject(error);
} finally {
client.destroy();
}
});
torrent.on("error", (error) => {
logger.error({ error }, "Torrent metadata error");
clearTimeout(timeout);
client.destroy();
reject(error);
});
});
};

View File

@@ -0,0 +1,121 @@
import { Router } from "express";
import multer from "multer";
import path from "node:path";
import fs from "node:fs/promises";
import { getQbitClient } from "../qbit/qbit.context";
import { getArchiveStatus, setArchiveStatus } from "./torrent.archive";
import { nowIso } from "../utils/time";
import { appendAuditLog, logger } from "../utils/logger";
import { config } from "../config";
const router = Router();
const upload = multer({ dest: "/tmp" });
router.post("/select", async (req, res) => {
const { hash } = req.body ?? {};
if (!hash) {
return res.status(400).json({ error: "Missing hash" });
}
const existing = await getArchiveStatus(hash);
if (existing?.status === "READY") {
return res.json({ ok: true, hash, archive: existing });
}
const qbit = getQbitClient();
const torrents = await qbit.getTorrentsInfo();
const torrent = torrents.find((t) => t.hash === hash);
if (!torrent) {
return res.status(404).json({ error: "Torrent not found" });
}
await setArchiveStatus({
hash,
status: "MISSING",
updatedAt: nowIso(),
});
res.json({ ok: true, hash, archive: { status: "MISSING" } });
});
router.post("/archive/from-selected", async (req, res) => {
const { hash } = req.body ?? {};
if (!hash) {
return res.status(400).json({ error: "Missing hash" });
}
const existing = await getArchiveStatus(hash);
if (existing?.status === "READY") {
return res.json({ ok: true, torrentFilePath: existing.torrentFilePath, source: existing.source });
}
await setArchiveStatus({
hash,
status: "MISSING",
lastError: "Magnet export disabled; upload .torrent manually.",
updatedAt: nowIso(),
});
await appendAuditLog({
level: "WARN",
event: "ARCHIVE_FAIL",
message: `Archive generation disabled for ${hash}; manual upload required`,
});
return res.status(400).json({ error: "Magnet export disabled; upload .torrent manually." });
});
router.post("/archive/upload", upload.single("file"), async (req, res) => {
const { hash } = req.body ?? {};
if (!hash || !req.file) {
return res.status(400).json({ error: "Missing hash or file" });
}
const inputHash = String(hash).toLowerCase();
const buffer = await fs.readFile(req.file.path);
let warning: string | undefined;
try {
const { default: parseTorrent } = await import("parse-torrent");
const parsed = parseTorrent(buffer);
const infoHash = String(parsed.infoHash ?? "").toLowerCase();
if (infoHash && infoHash !== inputHash) {
await fs.unlink(req.file.path);
return res.status(400).json({
error: "Torrent hash uyuşmuyor. Doğru .torrent dosyasını seçin.",
expected: inputHash,
actual: infoHash,
});
}
} catch (error) {
warning = "Torrent dosyası okunamadı; yine de arşive kaydedildi.";
logger.warn({ error, hash: inputHash }, "Torrent parse failed; storing archive anyway");
}
const targetPath = path.join(config.torrentArchiveDir, `${hash}.torrent`);
await fs.writeFile(targetPath, buffer);
await fs.unlink(req.file.path);
await setArchiveStatus({
hash,
status: "READY",
torrentFilePath: targetPath,
source: "manual",
updatedAt: nowIso(),
});
try {
const qbit = getQbitClient();
await qbit.addTorrentByFile(targetPath);
return res.json({
ok: true,
torrentFilePath: targetPath,
added: true,
});
} catch (error) {
return res.json({
ok: true,
torrentFilePath: targetPath,
added: false,
});
}
});
router.get("/archive/status/:hash", async (req, res) => {
const status = await getArchiveStatus(req.params.hash);
if (!status) {
return res.json({ hash: req.params.hash, status: "MISSING" });
}
return res.json(status);
});
export default router;