feat(webdav): webdav desteği ekle
webdav-server paketi kullanılarak WebDAV sunucusu entegre edildi. Film, TV ve Anime dizinleri WebDAV istemcileri (örn. Infuse) için otomatik olarak indekslenir ve sembolik bağlantılarla sunulur. Yapılandırma, Basic Auth ve salt-okuma modu için yeni ortam değişkenleri ve docker-compose ayarları eklendi.
This commit is contained in:
12
.env.example
12
.env.example
@@ -31,3 +31,15 @@ AUTO_PAUSE_ON_COMPLETE=0
|
|||||||
# Medya işleme adımlarını (ffprobe/ffmpeg, thumbnail ve TMDB/TVDB metadata) devre dışı bırakır;
|
# Medya işleme adımlarını (ffprobe/ffmpeg, thumbnail ve TMDB/TVDB metadata) devre dışı bırakır;
|
||||||
# CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır.
|
# CPU ve disk kullanımını düşürür, ancak kapalıyken medya bilgileri eksik kalır.
|
||||||
DISABLE_MEDIA_PROCESSING=0
|
DISABLE_MEDIA_PROCESSING=0
|
||||||
|
# WebDAV erişimi; Infuse gibi istemciler için salt-okuma paylaşımlar.
|
||||||
|
WEBDAV_ENABLED=1
|
||||||
|
# WebDAV Basic Auth kullanıcı adı.
|
||||||
|
WEBDAV_USERNAME=dupe
|
||||||
|
# WebDAV Basic Auth şifresi (güçlü bir parola kullanın).
|
||||||
|
WEBDAV_PASSWORD=superpassword
|
||||||
|
# WebDAV kök path'i (proxy üzerinden erişilecek).
|
||||||
|
WEBDAV_PATH=/webdav
|
||||||
|
# WebDAV salt-okuma modu.
|
||||||
|
WEBDAV_READONLY=1
|
||||||
|
# WebDAV index yeniden oluşturma süresi (ms).
|
||||||
|
WEBDAV_INDEX_TTL=60000
|
||||||
|
|||||||
@@ -19,3 +19,9 @@ services:
|
|||||||
DEBUG_CPU: ${DEBUG_CPU}
|
DEBUG_CPU: ${DEBUG_CPU}
|
||||||
AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE}
|
AUTO_PAUSE_ON_COMPLETE: ${AUTO_PAUSE_ON_COMPLETE}
|
||||||
DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING}
|
DISABLE_MEDIA_PROCESSING: ${DISABLE_MEDIA_PROCESSING}
|
||||||
|
WEBDAV_ENABLED: ${WEBDAV_ENABLED}
|
||||||
|
WEBDAV_USERNAME: ${WEBDAV_USERNAME}
|
||||||
|
WEBDAV_PASSWORD: ${WEBDAV_PASSWORD}
|
||||||
|
WEBDAV_PATH: ${WEBDAV_PATH}
|
||||||
|
WEBDAV_READONLY: ${WEBDAV_READONLY}
|
||||||
|
WEBDAV_INDEX_TTL: ${WEBDAV_INDEX_TTL}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"webdav-server": "^2.6.2",
|
||||||
"webtorrent": "^1.9.7",
|
"webtorrent": "^1.9.7",
|
||||||
"ws": "^8.18.0"
|
"ws": "^8.18.0"
|
||||||
}
|
}
|
||||||
|
|||||||
373
server/server.js
373
server/server.js
@@ -5,6 +5,7 @@ import WebTorrent from "webtorrent";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import mime from "mime-types";
|
import mime from "mime-types";
|
||||||
|
import { v2 as webdav } from "webdav-server";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { exec, spawn } from "child_process";
|
import { exec, spawn } from "child_process";
|
||||||
import crypto from "crypto"; // 🔒 basit token üretimi için
|
import crypto from "crypto"; // 🔒 basit token üretimi için
|
||||||
@@ -28,6 +29,16 @@ const PORT = process.env.PORT || 3001;
|
|||||||
const DEBUG_CPU = process.env.DEBUG_CPU === "1";
|
const DEBUG_CPU = process.env.DEBUG_CPU === "1";
|
||||||
const DISABLE_MEDIA_PROCESSING = process.env.DISABLE_MEDIA_PROCESSING === "1";
|
const DISABLE_MEDIA_PROCESSING = process.env.DISABLE_MEDIA_PROCESSING === "1";
|
||||||
const AUTO_PAUSE_ON_COMPLETE = process.env.AUTO_PAUSE_ON_COMPLETE === "1";
|
const AUTO_PAUSE_ON_COMPLETE = process.env.AUTO_PAUSE_ON_COMPLETE === "1";
|
||||||
|
const WEBDAV_ENABLED = ["1", "true", "yes", "on"].includes(
|
||||||
|
String(process.env.WEBDAV_ENABLED || "").toLowerCase()
|
||||||
|
);
|
||||||
|
const WEBDAV_USERNAME = process.env.WEBDAV_USERNAME || "";
|
||||||
|
const WEBDAV_PASSWORD = process.env.WEBDAV_PASSWORD || "";
|
||||||
|
const WEBDAV_PATH = process.env.WEBDAV_PATH || "/webdav";
|
||||||
|
const WEBDAV_READONLY = !["0", "false", "no", "off"].includes(
|
||||||
|
String(process.env.WEBDAV_READONLY || "1").toLowerCase()
|
||||||
|
);
|
||||||
|
const WEBDAV_INDEX_TTL = Number(process.env.WEBDAV_INDEX_TTL || 60000);
|
||||||
|
|
||||||
// --- İndirilen dosyalar için klasör oluştur ---
|
// --- İndirilen dosyalar için klasör oluştur ---
|
||||||
const DOWNLOAD_DIR = path.join(__dirname, "downloads");
|
const DOWNLOAD_DIR = path.join(__dirname, "downloads");
|
||||||
@@ -52,6 +63,7 @@ const MOVIE_DATA_ROOT = path.join(CACHE_DIR, "movie_data");
|
|||||||
const TV_DATA_ROOT = path.join(CACHE_DIR, "tv_data");
|
const TV_DATA_ROOT = path.join(CACHE_DIR, "tv_data");
|
||||||
const ANIME_DATA_ROOT = path.join(CACHE_DIR, "anime_data");
|
const ANIME_DATA_ROOT = path.join(CACHE_DIR, "anime_data");
|
||||||
const YT_DATA_ROOT = path.join(CACHE_DIR, "yt_data");
|
const YT_DATA_ROOT = path.join(CACHE_DIR, "yt_data");
|
||||||
|
const WEBDAV_ROOT = path.join(CACHE_DIR, "webdav");
|
||||||
const ANIME_ROOT_FOLDER = "_anime";
|
const ANIME_ROOT_FOLDER = "_anime";
|
||||||
const ROOT_TRASH_REGISTRY = path.join(CACHE_DIR, "root-trash.json");
|
const ROOT_TRASH_REGISTRY = path.join(CACHE_DIR, "root-trash.json");
|
||||||
const MUSIC_EXTENSIONS = new Set([
|
const MUSIC_EXTENSIONS = new Set([
|
||||||
@@ -73,7 +85,8 @@ for (const dir of [
|
|||||||
MOVIE_DATA_ROOT,
|
MOVIE_DATA_ROOT,
|
||||||
TV_DATA_ROOT,
|
TV_DATA_ROOT,
|
||||||
ANIME_DATA_ROOT,
|
ANIME_DATA_ROOT,
|
||||||
YT_DATA_ROOT
|
YT_DATA_ROOT,
|
||||||
|
WEBDAV_ROOT
|
||||||
]) {
|
]) {
|
||||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
@@ -2572,6 +2585,335 @@ function resolveTvDataAbsolute(relPath) {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeWebdavSegment(value) {
|
||||||
|
if (!value) return "Unknown";
|
||||||
|
return String(value)
|
||||||
|
.replace(/[\\/:*?"<>|]+/g, "_")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeUniqueWebdavName(base, used, suffix = "") {
|
||||||
|
let name = base || "Unknown";
|
||||||
|
if (!used.has(name)) {
|
||||||
|
used.add(name);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
const fallback = suffix ? `${name} [${suffix}]` : `${name} [${used.size}]`;
|
||||||
|
if (!used.has(fallback)) {
|
||||||
|
used.add(fallback);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
let counter = 2;
|
||||||
|
while (used.has(`${fallback} ${counter}`)) counter += 1;
|
||||||
|
const finalName = `${fallback} ${counter}`;
|
||||||
|
used.add(finalName);
|
||||||
|
return finalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDirSync(dirPath) {
|
||||||
|
if (!fs.existsSync(dirPath)) fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSymlinkSafe(targetPath, linkPath) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(linkPath)) return;
|
||||||
|
ensureDirSync(path.dirname(linkPath));
|
||||||
|
let stat = null;
|
||||||
|
try {
|
||||||
|
stat = fs.statSync(targetPath);
|
||||||
|
} catch (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stat?.isFile?.()) {
|
||||||
|
try {
|
||||||
|
fs.linkSync(targetPath, linkPath);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
if (err?.code !== "EXDEV") {
|
||||||
|
// Hardlink başarısızsa symlink'e düş
|
||||||
|
fs.symlinkSync(targetPath, linkPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.symlinkSync(targetPath, linkPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ WebDAV link oluşturulamadı (${linkPath}): ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMovieVideoAbsPath(metadata) {
|
||||||
|
const dupe = metadata?._dupe || {};
|
||||||
|
const rootFolder = sanitizeRelative(dupe.folder || "") || null;
|
||||||
|
let videoPath = dupe.videoPath || metadata?.videoPath || null;
|
||||||
|
if (!videoPath) return null;
|
||||||
|
videoPath = String(videoPath).replace(/\\/g, "/").replace(/^\/+/, "");
|
||||||
|
const hasRoot = rootFolder && videoPath.startsWith(`${rootFolder}/`);
|
||||||
|
const absPath = hasRoot
|
||||||
|
? path.join(DOWNLOAD_DIR, videoPath)
|
||||||
|
: rootFolder
|
||||||
|
? path.join(DOWNLOAD_DIR, rootFolder, videoPath)
|
||||||
|
: path.join(DOWNLOAD_DIR, videoPath);
|
||||||
|
return fs.existsSync(absPath) ? absPath : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveEpisodeAbsPath(rootFolder, episode) {
|
||||||
|
if (!episode) return null;
|
||||||
|
let videoPath = episode.videoPath || episode.file || "";
|
||||||
|
if (!videoPath) return null;
|
||||||
|
videoPath = String(videoPath).replace(/\\/g, "/").replace(/^\/+/, "");
|
||||||
|
const candidates = [];
|
||||||
|
|
||||||
|
if (rootFolder === ANIME_ROOT_FOLDER) {
|
||||||
|
candidates.push(path.join(DOWNLOAD_DIR, videoPath));
|
||||||
|
if (videoPath.includes("/")) {
|
||||||
|
candidates.push(path.join(DOWNLOAD_DIR, path.basename(videoPath)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (rootFolder && !videoPath.startsWith(`${rootFolder}/`)) {
|
||||||
|
candidates.push(path.join(DOWNLOAD_DIR, rootFolder, videoPath));
|
||||||
|
}
|
||||||
|
candidates.push(path.join(DOWNLOAD_DIR, videoPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (episode.file && episode.file !== videoPath) {
|
||||||
|
const fallbackFile = String(episode.file)
|
||||||
|
.replace(/\\/g, "/")
|
||||||
|
.replace(/^\/+/, "");
|
||||||
|
candidates.push(path.join(DOWNLOAD_DIR, fallbackFile));
|
||||||
|
if (rootFolder && !fallbackFile.startsWith(`${rootFolder}/`)) {
|
||||||
|
candidates.push(path.join(DOWNLOAD_DIR, rootFolder, fallbackFile));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const absPath of candidates) {
|
||||||
|
if (fs.existsSync(absPath)) return absPath;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildWebdavIndex() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(WEBDAV_ROOT)) {
|
||||||
|
fs.rmSync(WEBDAV_ROOT, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(WEBDAV_ROOT, { recursive: true });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ WebDAV kökü temizlenemedi (${WEBDAV_ROOT}): ${err.message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryDefs = [
|
||||||
|
{ label: "Movies" },
|
||||||
|
{ label: "TV Shows" },
|
||||||
|
{ label: "Anime" }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const cat of categoryDefs) {
|
||||||
|
const dir = path.join(WEBDAV_ROOT, cat.label);
|
||||||
|
ensureDirSync(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVideoExt = (value) =>
|
||||||
|
VIDEO_EXTS.includes(String(value || "").toLowerCase());
|
||||||
|
|
||||||
|
const shouldSkip = (relPath) => {
|
||||||
|
if (!relPath) return true;
|
||||||
|
const normalized = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
||||||
|
const segments = normalized.split("/").filter(Boolean);
|
||||||
|
if (!segments.length) return true;
|
||||||
|
return segments.some((seg) => seg.startsWith("yt_"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeEpisodeCode = (seasonNum, episodeNum) => {
|
||||||
|
const season = Number(seasonNum);
|
||||||
|
const episode = Number(episodeNum);
|
||||||
|
if (!Number.isFinite(season) || !Number.isFinite(episode)) return null;
|
||||||
|
return `S${String(season).padStart(2, "0")}E${String(episode).padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEpisodeNumber = (episodeKey, episode) => {
|
||||||
|
const direct =
|
||||||
|
episode?.episodeNumber ??
|
||||||
|
episode?.number ??
|
||||||
|
episode?.episode ??
|
||||||
|
null;
|
||||||
|
if (Number.isFinite(Number(direct))) return Number(direct);
|
||||||
|
const match = String(episodeKey || "").match(/(\d{1,4})/);
|
||||||
|
return match ? Number(match[1]) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Movies
|
||||||
|
try {
|
||||||
|
const movieDirs = fs.readdirSync(MOVIE_DATA_ROOT, { withFileTypes: true });
|
||||||
|
const usedMovieNames = new Set();
|
||||||
|
for (const dirent of movieDirs) {
|
||||||
|
if (!dirent.isDirectory()) continue;
|
||||||
|
const metaPath = path.join(MOVIE_DATA_ROOT, dirent.name, "metadata.json");
|
||||||
|
if (!fs.existsSync(metaPath)) continue;
|
||||||
|
let metadata = null;
|
||||||
|
try {
|
||||||
|
metadata = JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
||||||
|
} catch (err) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const absVideo = resolveMovieVideoAbsPath(metadata);
|
||||||
|
if (!absVideo) continue;
|
||||||
|
if (shouldSkip(absVideo.replace(DOWNLOAD_DIR, ""))) continue;
|
||||||
|
const title =
|
||||||
|
metadata?.title ||
|
||||||
|
metadata?.matched_title ||
|
||||||
|
metadata?._dupe?.displayName ||
|
||||||
|
dirent.name;
|
||||||
|
const year =
|
||||||
|
metadata?.release_date?.slice?.(0, 4) ||
|
||||||
|
metadata?.matched_year ||
|
||||||
|
metadata?.year ||
|
||||||
|
null;
|
||||||
|
const baseName = sanitizeWebdavSegment(
|
||||||
|
year ? `${title} (${year})` : title
|
||||||
|
);
|
||||||
|
const uniqueName = makeUniqueWebdavName(
|
||||||
|
baseName,
|
||||||
|
usedMovieNames,
|
||||||
|
metadata?.id || dirent.name
|
||||||
|
);
|
||||||
|
const movieDir = path.join(WEBDAV_ROOT, "Movies", uniqueName);
|
||||||
|
ensureDirSync(movieDir);
|
||||||
|
const ext = path.extname(absVideo);
|
||||||
|
const fileName = sanitizeWebdavSegment(`${uniqueName}${ext}`);
|
||||||
|
const linkPath = path.join(movieDir, fileName);
|
||||||
|
createSymlinkSafe(absVideo, linkPath);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ WebDAV movie index oluşturulamadı: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildSeriesCategory = (dataRoot, categoryLabel) => {
|
||||||
|
try {
|
||||||
|
const dirs = fs.readdirSync(dataRoot, { withFileTypes: true });
|
||||||
|
const usedShowNames = new Set();
|
||||||
|
for (const dirent of dirs) {
|
||||||
|
if (!dirent.isDirectory()) continue;
|
||||||
|
const seriesDir = path.join(dataRoot, dirent.name);
|
||||||
|
const seriesPath = path.join(seriesDir, "series.json");
|
||||||
|
if (!fs.existsSync(seriesPath)) continue;
|
||||||
|
let seriesData = null;
|
||||||
|
try {
|
||||||
|
seriesData = JSON.parse(fs.readFileSync(seriesPath, "utf-8"));
|
||||||
|
} catch (err) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { rootFolder } = parseTvSeriesKey(dirent.name);
|
||||||
|
const showTitle = sanitizeWebdavSegment(
|
||||||
|
seriesData?.name || seriesData?.title || dirent.name
|
||||||
|
);
|
||||||
|
const uniqueShow = makeUniqueWebdavName(
|
||||||
|
showTitle,
|
||||||
|
usedShowNames,
|
||||||
|
seriesData?.id || dirent.name
|
||||||
|
);
|
||||||
|
const showDir = path.join(WEBDAV_ROOT, categoryLabel, uniqueShow);
|
||||||
|
const seasons = seriesData?.seasons || {};
|
||||||
|
let createdSeasonCount = 0;
|
||||||
|
for (const seasonKey of Object.keys(seasons)) {
|
||||||
|
const season = seasons[seasonKey];
|
||||||
|
if (!season?.episodes) continue;
|
||||||
|
const seasonNumber =
|
||||||
|
season?.seasonNumber ?? Number(seasonKey) ?? null;
|
||||||
|
const seasonLabel = seasonNumber
|
||||||
|
? `Season ${String(seasonNumber).padStart(2, "0")}`
|
||||||
|
: "Season";
|
||||||
|
const seasonDir = path.join(showDir, seasonLabel);
|
||||||
|
let createdEpisodeCount = 0;
|
||||||
|
for (const episodeKey of Object.keys(season.episodes)) {
|
||||||
|
const episode = season.episodes[episodeKey];
|
||||||
|
const absVideo = resolveEpisodeAbsPath(rootFolder, episode);
|
||||||
|
if (!absVideo) continue;
|
||||||
|
if (shouldSkip(absVideo.replace(DOWNLOAD_DIR, ""))) continue;
|
||||||
|
const ext = path.extname(absVideo);
|
||||||
|
if (!isVideoExt(ext)) continue;
|
||||||
|
if (createdEpisodeCount === 0) {
|
||||||
|
ensureDirSync(seasonDir);
|
||||||
|
ensureDirSync(showDir);
|
||||||
|
createdSeasonCount += 1;
|
||||||
|
}
|
||||||
|
const episodeNumber = getEpisodeNumber(episodeKey, episode);
|
||||||
|
const code = makeEpisodeCode(seasonNumber, episodeNumber);
|
||||||
|
const safeCode =
|
||||||
|
code ||
|
||||||
|
`S${String(seasonNumber || 0).padStart(2, "0")}E${String(
|
||||||
|
Number(episodeNumber) || 0
|
||||||
|
).padStart(2, "0")}`;
|
||||||
|
const fileName = sanitizeWebdavSegment(
|
||||||
|
`${uniqueShow} - ${safeCode}${ext}`
|
||||||
|
);
|
||||||
|
const linkPath = path.join(seasonDir, fileName);
|
||||||
|
createSymlinkSafe(absVideo, linkPath);
|
||||||
|
createdEpisodeCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (createdSeasonCount === 0 && fs.existsSync(showDir)) {
|
||||||
|
fs.rmSync(showDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ WebDAV ${categoryLabel} index oluşturulamadı: ${err.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
buildSeriesCategory(TV_DATA_ROOT, "TV Shows");
|
||||||
|
buildSeriesCategory(ANIME_DATA_ROOT, "Anime");
|
||||||
|
}
|
||||||
|
|
||||||
|
let webdavIndexLast = 0;
|
||||||
|
let webdavIndexBuilding = false;
|
||||||
|
async function ensureWebdavIndexFresh() {
|
||||||
|
if (!WEBDAV_ENABLED) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (webdavIndexBuilding) return;
|
||||||
|
if (now - webdavIndexLast < WEBDAV_INDEX_TTL) return;
|
||||||
|
webdavIndexBuilding = true;
|
||||||
|
try {
|
||||||
|
rebuildWebdavIndex();
|
||||||
|
webdavIndexLast = Date.now();
|
||||||
|
} finally {
|
||||||
|
webdavIndexBuilding = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function webdavAuthMiddleware(req, res, next) {
|
||||||
|
if (!WEBDAV_ENABLED) return res.status(404).end();
|
||||||
|
const authHeader = req.headers.authorization || "";
|
||||||
|
if (!authHeader.startsWith("Basic ")) {
|
||||||
|
res.setHeader("WWW-Authenticate", "Basic realm=\"Dupe WebDAV\"");
|
||||||
|
return res.status(401).end();
|
||||||
|
}
|
||||||
|
const raw = Buffer.from(authHeader.slice(6), "base64")
|
||||||
|
.toString("utf-8")
|
||||||
|
.split(":");
|
||||||
|
const user = raw.shift() || "";
|
||||||
|
const pass = raw.join(":");
|
||||||
|
if (!WEBDAV_USERNAME || !WEBDAV_PASSWORD) {
|
||||||
|
return res.status(500).end();
|
||||||
|
}
|
||||||
|
if (user !== WEBDAV_USERNAME || pass !== WEBDAV_PASSWORD) {
|
||||||
|
res.setHeader("WWW-Authenticate", "Basic realm=\"Dupe WebDAV\"");
|
||||||
|
return res.status(401).end();
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
function webdavReadonlyGuard(req, res, next) {
|
||||||
|
if (!WEBDAV_READONLY) return next();
|
||||||
|
const allowed = new Set(["GET", "HEAD", "OPTIONS", "PROPFIND"]);
|
||||||
|
if (!allowed.has(req.method)) {
|
||||||
|
return res.status(403).end();
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
function serveCachedFile(req, res, filePath, { maxAgeSeconds = 86400 } = {}) {
|
function serveCachedFile(req, res, filePath, { maxAgeSeconds = 86400 } = {}) {
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
return res.status(404).send("Dosya bulunamadı");
|
return res.status(404).send("Dosya bulunamadı");
|
||||||
@@ -8479,6 +8821,35 @@ if (restored.length) {
|
|||||||
console.log(`♻️ ${restored.length} torrent yeniden eklendi.`);
|
console.log(`♻️ ${restored.length} torrent yeniden eklendi.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 📁 WebDAV (Infuse) ---
|
||||||
|
if (WEBDAV_ENABLED) {
|
||||||
|
const webdavServer = new webdav.WebDAVServer({
|
||||||
|
strictMode: false
|
||||||
|
});
|
||||||
|
const webdavBasePath = WEBDAV_PATH.startsWith("/")
|
||||||
|
? WEBDAV_PATH
|
||||||
|
: `/${WEBDAV_PATH}`;
|
||||||
|
const userManager = new webdav.SimpleUserManager();
|
||||||
|
if (WEBDAV_USERNAME && WEBDAV_PASSWORD) {
|
||||||
|
userManager.addUser(WEBDAV_USERNAME, WEBDAV_PASSWORD, false);
|
||||||
|
}
|
||||||
|
webdavServer.httpAuthentication = new webdav.HTTPBasicAuthentication(
|
||||||
|
userManager,
|
||||||
|
"Dupe WebDAV"
|
||||||
|
);
|
||||||
|
webdavServer.setFileSystem("/", new webdav.PhysicalFileSystem(WEBDAV_ROOT));
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
webdavBasePath,
|
||||||
|
webdavAuthMiddleware,
|
||||||
|
webdavReadonlyGuard,
|
||||||
|
async (req, res) => {
|
||||||
|
await ensureWebdavIndexFresh();
|
||||||
|
webdavServer.executeRequest(req, res);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(`📁 WebDAV aktif: ${webdavBasePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
// --- ✅ Client build (frontend) dosyalarını sun ---
|
// --- ✅ Client build (frontend) dosyalarını sun ---
|
||||||
const publicDir = path.join(__dirname, "public");
|
const publicDir = path.join(__dirname, "public");
|
||||||
|
|||||||
Reference in New Issue
Block a user