first commit
This commit is contained in:
46
apps/server/src/torrent/torrent.archive.ts
Normal file
46
apps/server/src/torrent/torrent.archive.ts
Normal 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);
|
||||
};
|
||||
41
apps/server/src/torrent/torrent.generator.ts
Normal file
41
apps/server/src/torrent/torrent.generator.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
};
|
||||
121
apps/server/src/torrent/torrent.routes.ts
Normal file
121
apps/server/src/torrent/torrent.routes.ts
Normal 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;
|
||||
Reference in New Issue
Block a user