import fs from 'node:fs/promises'; import { session } from 'electron'; import { asAssetId } from '../../shared/types/ids'; import type { ZipProjectStore } from '../project/zipStore'; /** * Обслуживает `dnd://asset?...` — без этого `` в рендерере часто ломается. */ export function registerDndAssetProtocol(projectStore: ZipProjectStore): void { session.defaultSession.protocol.handle('dnd', async (request) => { const url = new URL(request.url); if (url.hostname !== 'asset') { return new Response(null, { status: 404 }); } const id = url.searchParams.get('id'); if (!id) { return new Response(null, { status: 404 }); } const info = projectStore.getAssetReadInfo(asAssetId(id)); if (!info) { return new Response(null, { status: 404 }); } try { const stat = await fs.stat(info.absPath); const total = stat.size; const range = request.headers.get('range') ?? request.headers.get('Range'); if (range) { const m = /^bytes=(\d+)-(\d+)?$/iu.exec(range.trim()); if (m) { const start = Number(m[1]); const endRaw = m[2] ? Number(m[2]) : total - 1; const end = Math.min(endRaw, total - 1); if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start) { return new Response(null, { status: 416 }); } const len = end - start + 1; const fh = await fs.open(info.absPath, 'r'); try { const buf = Buffer.alloc(len); await fh.read(buf, 0, len, start); return new Response(buf, { status: 206, headers: { 'Content-Type': info.mime, 'Accept-Ranges': 'bytes', 'Content-Range': `bytes ${String(start)}-${String(end)}/${String(total)}`, 'Content-Length': String(len), 'Cache-Control': 'public, max-age=300', }, }); } finally { await fh.close(); } } } const buf = await fs.readFile(info.absPath); return new Response(buf, { headers: { 'Content-Type': info.mime, 'Accept-Ranges': 'bytes', 'Content-Length': String(buf.length), 'Cache-Control': 'public, max-age=300', }, }); } catch { return new Response(null, { status: 404 }); } }); }