Skip to content

Commit bfe179e

Browse files
committed
additional TOML store support
1 parent 0f3a6c8 commit bfe179e

11 files changed

Lines changed: 1274 additions & 385 deletions

File tree

lib/group/store/toml.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import fs from 'node:fs/promises'
2+
import path from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
5+
import { parse, stringify } from 'smol-toml'
6+
7+
import GroupBase from './base.js'
8+
9+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
10+
11+
const defaultPermissions = {
12+
inherit: false,
13+
self_write: false,
14+
group: { create: false, write: false, delete: false },
15+
nameserver: { usable: [], create: false, write: false, delete: false },
16+
zone: { create: false, write: false, delete: false, delegate: false },
17+
zonerecord: { create: false, write: false, delete: false, delegate: false },
18+
user: { create: false, write: false, delete: false },
19+
}
20+
21+
function resolveStorePath(filename) {
22+
const base = process.env.NICTOOL_DATA_STORE_PATH
23+
if (base) return path.join(base, filename)
24+
return path.resolve(__dirname, '../../../conf.d', filename)
25+
}
26+
27+
class GroupRepoTOML extends GroupBase {
28+
constructor(args = {}) {
29+
super(args)
30+
this._filePath = resolveStorePath('group.toml')
31+
}
32+
33+
async _load() {
34+
try {
35+
const str = await fs.readFile(this._filePath, 'utf8')
36+
const data = parse(str)
37+
return Array.isArray(data.group) ? data.group : []
38+
} catch (err) {
39+
if (err.code === 'ENOENT') return []
40+
throw err
41+
}
42+
}
43+
44+
async _save(groups) {
45+
await fs.mkdir(path.dirname(this._filePath), { recursive: true })
46+
await fs.writeFile(this._filePath, stringify({ group: groups }))
47+
}
48+
49+
_postProcess(row, deletedArg) {
50+
const r = JSON.parse(JSON.stringify(row))
51+
r.deleted = Boolean(r.deleted)
52+
if (r.permissions?.nameserver && !Array.isArray(r.permissions.nameserver.usable)) {
53+
r.permissions.nameserver.usable = []
54+
}
55+
if (deletedArg === false) delete r.deleted
56+
return r
57+
}
58+
59+
// BFS over parent_gid relationships to collect all descendant group ids.
60+
_collectSubgroupIds(groups, gid) {
61+
const ids = []
62+
const queue = [gid]
63+
while (queue.length) {
64+
const cur = queue.shift()
65+
for (const g of groups) {
66+
if (g.parent_gid === cur && !ids.includes(g.id)) {
67+
ids.push(g.id)
68+
queue.push(g.id)
69+
}
70+
}
71+
}
72+
return ids
73+
}
74+
75+
async create(args) {
76+
args = JSON.parse(JSON.stringify(args))
77+
78+
if (args.id) {
79+
const existing = await this.get({ id: args.id })
80+
if (existing.length === 1) return existing[0].id
81+
}
82+
83+
const usable_ns = args.usable_ns ?? []
84+
delete args.usable_ns
85+
86+
const gid = args.id
87+
args.permissions = {
88+
...JSON.parse(JSON.stringify(defaultPermissions)),
89+
id: gid,
90+
name: `Group ${args.name} perms`,
91+
user: { id: gid, create: false, write: false, delete: false },
92+
group: { id: gid, create: false, write: false, delete: false },
93+
nameserver: {
94+
usable: Array.isArray(usable_ns) ? usable_ns : [],
95+
create: false,
96+
write: false,
97+
delete: false,
98+
},
99+
}
100+
101+
const groups = await this._load()
102+
groups.push(args)
103+
await this._save(groups)
104+
return gid
105+
}
106+
107+
async get(args_orig) {
108+
const args = JSON.parse(JSON.stringify(args_orig))
109+
const deletedArg = args.deleted ?? false
110+
const include_subgroups = args.include_subgroups === true
111+
112+
let groups = await this._load()
113+
114+
if (args.id !== undefined) {
115+
if (include_subgroups) {
116+
const subIds = this._collectSubgroupIds(groups, args.id)
117+
const allIds = [args.id, ...subIds]
118+
groups = groups.filter((g) => allIds.includes(g.id))
119+
} else {
120+
groups = groups.filter((g) => g.id === args.id)
121+
}
122+
}
123+
124+
if (args.parent_gid !== undefined) groups = groups.filter((g) => g.parent_gid === args.parent_gid)
125+
if (args.name !== undefined) groups = groups.filter((g) => g.name === args.name)
126+
127+
if (deletedArg === false) groups = groups.filter((g) => !g.deleted)
128+
else if (deletedArg !== undefined)
129+
groups = groups.filter((g) => Boolean(g.deleted) === Boolean(deletedArg))
130+
131+
return groups.map((g) => this._postProcess(g, deletedArg))
132+
}
133+
134+
async put(args) {
135+
if (!args.id) return false
136+
args = JSON.parse(JSON.stringify(args))
137+
const id = args.id
138+
delete args.id
139+
140+
const usable_ns = args.usable_ns
141+
delete args.usable_ns
142+
143+
const groups = await this._load()
144+
const idx = groups.findIndex((g) => g.id === id)
145+
if (idx === -1) return false
146+
147+
if (usable_ns !== undefined && groups[idx].permissions) {
148+
groups[idx].permissions.nameserver = {
149+
...groups[idx].permissions.nameserver,
150+
usable: Array.isArray(usable_ns) ? usable_ns : [],
151+
}
152+
}
153+
154+
if (Object.keys(args).length > 0) {
155+
groups[idx] = { ...groups[idx], ...args }
156+
}
157+
158+
await this._save(groups)
159+
return true
160+
}
161+
162+
async delete(args) {
163+
const groups = await this._load()
164+
const idx = groups.findIndex((g) => g.id === args.id)
165+
if (idx === -1) return false
166+
167+
groups[idx].deleted = args.deleted ?? true
168+
await this._save(groups)
169+
return true
170+
}
171+
172+
async destroy(args) {
173+
const groups = await this._load()
174+
const before = groups.length
175+
const filtered = groups.filter((g) => g.id !== args.id)
176+
if (filtered.length === before) return false
177+
await this._save(filtered)
178+
return true
179+
}
180+
}
181+
182+
export default GroupRepoTOML

lib/nameserver/store/toml.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import fs from 'node:fs/promises'
2+
import path from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
5+
import { parse, stringify } from 'smol-toml'
6+
7+
import NameserverBase from './base.js'
8+
9+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
10+
11+
const boolFields = ['deleted']
12+
13+
// Fields that default to empty string when absent or null
14+
const emptyStringFields = ['description', 'address6', 'remote_login', 'logdir', 'datadir']
15+
16+
function resolveStorePath(filename) {
17+
const base = process.env.NICTOOL_DATA_STORE_PATH
18+
if (base) return path.join(base, filename)
19+
return path.resolve(__dirname, '../../../conf.d', filename)
20+
}
21+
22+
class NameserverRepoTOML extends NameserverBase {
23+
constructor(args = {}) {
24+
super(args)
25+
this._filePath = resolveStorePath('nameserver.toml')
26+
}
27+
28+
async _load() {
29+
try {
30+
const str = await fs.readFile(this._filePath, 'utf8')
31+
const data = parse(str)
32+
return Array.isArray(data.nameserver) ? data.nameserver : []
33+
} catch (err) {
34+
if (err.code === 'ENOENT') return []
35+
throw err
36+
}
37+
}
38+
39+
async _save(nameservers) {
40+
await fs.mkdir(path.dirname(this._filePath), { recursive: true })
41+
await fs.writeFile(this._filePath, stringify({ nameserver: nameservers }))
42+
}
43+
44+
_postProcess(row, deletedArg) {
45+
const r = JSON.parse(JSON.stringify(row))
46+
47+
for (const b of boolFields) r[b] = Boolean(r[b])
48+
for (const f of emptyStringFields) {
49+
if ([null, undefined].includes(r[f])) r[f] = ''
50+
}
51+
52+
// Ensure the nested export object is always present and well-formed.
53+
// Unlike MySQL (which joins nt_nameserver_export_type), TOML stores the
54+
// type name inline, so no translation is needed.
55+
if (!r.export || typeof r.export !== 'object') r.export = {}
56+
if ([null, undefined].includes(r.export.type)) r.export.type = ''
57+
if ([null, undefined].includes(r.export.interval)) r.export.interval = 0
58+
if ([null, undefined].includes(r.export.status)) r.export.status = ''
59+
r.export.serials = Boolean(r.export.serials)
60+
61+
if (deletedArg === false) delete r.deleted
62+
return r
63+
}
64+
65+
async create(args) {
66+
if (args.id) {
67+
const existing = await this.get({ id: args.id })
68+
if (existing.length === 1) return existing[0].id
69+
}
70+
71+
const nameservers = await this._load()
72+
nameservers.push(JSON.parse(JSON.stringify(args)))
73+
await this._save(nameservers)
74+
return args.id
75+
}
76+
77+
async get(args) {
78+
args = JSON.parse(JSON.stringify(args))
79+
const deletedArg = args.deleted ?? false
80+
81+
let nameservers = await this._load()
82+
83+
if (args.id !== undefined) nameservers = nameservers.filter((n) => n.id === args.id)
84+
if (args.gid !== undefined) nameservers = nameservers.filter((n) => n.gid === args.gid)
85+
if (args.name !== undefined) nameservers = nameservers.filter((n) => n.name === args.name)
86+
if (deletedArg === false) nameservers = nameservers.filter((n) => !n.deleted)
87+
else if (deletedArg !== undefined)
88+
nameservers = nameservers.filter((n) => Boolean(n.deleted) === Boolean(deletedArg))
89+
90+
return nameservers.map((n) => this._postProcess(n, deletedArg))
91+
}
92+
93+
async put(args) {
94+
if (!args.id) return false
95+
const nameservers = await this._load()
96+
const idx = nameservers.findIndex((n) => n.id === args.id)
97+
if (idx === -1) return false
98+
99+
nameservers[idx] = { ...nameservers[idx], ...JSON.parse(JSON.stringify(args)) }
100+
await this._save(nameservers)
101+
return true
102+
}
103+
104+
async delete(args) {
105+
const nameservers = await this._load()
106+
const idx = nameservers.findIndex((n) => n.id === args.id)
107+
if (idx === -1) return false
108+
109+
nameservers[idx].deleted = args.deleted ?? true
110+
await this._save(nameservers)
111+
return true
112+
}
113+
114+
async destroy(args) {
115+
const nameservers = await this._load()
116+
const before = nameservers.length
117+
const filtered = nameservers.filter((n) => n.id !== args.id)
118+
if (filtered.length === before) return false
119+
await this._save(filtered)
120+
return true
121+
}
122+
}
123+
124+
export default NameserverRepoTOML

0 commit comments

Comments
 (0)