Files
siujamo 97ad09ad8a feat: add dnsmasq DNS record admin UI
- store records in SQLite via built-in node:sqlite, no native deps
- wrap DB reads in await io() so they stay out of the static shell
- add CRUD pages backed by Server Actions with per-request data
- generate managed config and restart container via Docker socket
- add /resolve page to query dnsmasq for A and CNAME records
- add Dockerfile and compose setup with shared config volume
2026-10-09 16:47:31 +08:00

53 lines
1.5 KiB
TypeScript

import { mkdirSync, renameSync, writeFileSync } from "node:fs"
import path from "node:path"
import type { DnsRecord } from "./db"
import { env } from "./env"
const HEADER = "# Generated by dnsmasq-admin — 请勿手动编辑,每次「应用并重启」都会覆盖此文件"
/**
* dnsmasq's `address=/domain/ip` already answers for the domain *and* every subdomain, so
* there is no need for a separate wildcard syntax.
*/
export function renderRecord(record: DnsRecord): string {
return record.type === "A"
? `address=/${record.domain}/${record.value}`
: `cname=${record.domain},${record.value}`
}
export function renderConfig(records: DnsRecord[]): string {
const lines = records
.filter((record) => record.enabled)
.map(renderRecord)
if (lines.length === 0) {
return `${HEADER}\n\n# 当前没有启用的记录\n`
}
return `${HEADER}\n\n${lines.join("\n")}\n`
}
/**
* Regenerate the managed config file for the given records.
*
* Written to a sibling temp file first and then renamed, so dnsmasq can never read a
* half-written file.
*/
export async function writeManagedConfig(records: DnsRecord[]): Promise<{
path: string
recordCount: number
}> {
const dir = path.dirname(env.confPath)
mkdirSync(dir, { recursive: true })
const content = renderConfig(records)
const tempPath = `${env.confPath}.${process.pid}.tmp`
writeFileSync(tempPath, content, "utf8")
renameSync(tempPath, env.confPath)
return {
path: env.confPath,
recordCount: records.filter((record) => record.enabled).length,
}
}