diff --git a/README.md b/README.md index ecb273c..1ef3fc0 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ $$ n \in [r_{min}, r]; n = r_{min} + t \mod (r - r_{min}) $$ - [x] REST API - [x] Visual Web Interface - [x] CLI - - [ ] Bump counter to avoid collissions after restoring backups (where ASNs + - [x] Bump counter to avoid collissions after restoring backups (where ASNs could have been generated after the time of the backup) - [x] Analyze time between ASNs, providing the possibility to specify a duration and a maximum collision probability for the bump (e.g. 1 hour, diff --git a/lib/cli/bump.ts b/lib/cli/bump.ts new file mode 100644 index 0000000..22e1fd1 --- /dev/null +++ b/lib/cli/bump.ts @@ -0,0 +1,70 @@ +import z from "@collinhacks/zod"; +import { + allManagedNamespaces, + CONFIG, + generateASN, + isManagedNamespace, +} from "$common/mod.ts"; + +const bumpArgs = z.object({ + namespace: z.number({ coerce: true }).optional(), + _: z.tuple([z.literal("bump"), z.number({ coerce: true }).min(1).int()]), +}); + +/** + * Prints statistics about the ASNs. + * @param args the arguments to the command + * @param args.count the number of ASNs to generate (default: 1) + */ +export async function runBump(args: unknown) { + const parsedParams = bumpArgs.parse(args); + + if (parsedParams.namespace && !isManagedNamespace(parsedParams.namespace)) { + console.error( + `Namespace ${parsedParams.namespace} (${CONFIG.ASN_PREFIX}${parsedParams.namespace}XXX) is not managed by the system.`, + ); + console.error("It therefore cannot be bumped."); + console.error( + "Managed namespace numbers are:", + allManagedNamespaces().join(", "), + ); + Deno.exit(1); + } + + const namespaces = parsedParams.namespace + ? [parsedParams.namespace] + : allManagedNamespaces(); + + const bumpDelta = parsedParams._[1]; + const bumpedAt = new Date().toISOString(); + + console.log( + "Bumping namespaces:", + namespaces.map((n) => `${CONFIG.ASN_PREFIX}${n}XXX`).join(", "), + ); + console.log("---"); + await Promise.all( + namespaces.map((namespace) => + generateASN( + { + bumpedBy: bumpDelta, + bumpedAt, + }, + namespace, + bumpDelta, + ).then((asn) => { + console.log( + "Bumped", + `${CONFIG.ASN_PREFIX}${namespace}XXX. ` + + `Next registered ASN will be ${CONFIG.ASN_PREFIX}${namespace}${ + (asn.counter + 1).toString().padStart(3, "0") + }.`, + ); + }) + ), + ); + console.log("---"); + console.log("Done."); + + await Promise.resolve(); +} diff --git a/lib/common/all-managed-namespaces.ts b/lib/common/all-managed-namespaces.ts index 41d0ef5..f40e9fa 100644 --- a/lib/common/all-managed-namespaces.ts +++ b/lib/common/all-managed-namespaces.ts @@ -6,10 +6,8 @@ import { CONFIG } from "$common/mod.ts"; * @returns all managed namespaces */ export function allManagedNamespaces() { - const minGeneric = Number.parseInt( - "1" + "0".repeat(CONFIG.ASN_NAMESPACE_RANGE.toString().length - 1), - ); - const maxGeneric = CONFIG.ASN_NAMESPACE_RANGE - 1; + const minGeneric = getMinimumGenericRangeNamespace(); + const maxGeneric = getMaximumGenericRangeNamespace(); return [ ...Array.from( @@ -19,3 +17,25 @@ export function allManagedNamespaces() { ...CONFIG.ADDITIONAL_MANAGED_NAMESPACES.map((v) => v.namespace), ]; } + +function getMaximumGenericRangeNamespace() { + return CONFIG.ASN_NAMESPACE_RANGE - 1; +} + +function getMinimumGenericRangeNamespace() { + return Number.parseInt( + "1" + "0".repeat(CONFIG.ASN_NAMESPACE_RANGE.toString().length - 1), + ); +} + +export function isManagedNamespace(namespace: number) { + if (namespace < getMinimumGenericRangeNamespace()) { + return false; + } + if (namespace <= getMaximumGenericRangeNamespace()) { + return true; + } + return CONFIG.ADDITIONAL_MANAGED_NAMESPACES.some((v) => + v.namespace === namespace + ); +} diff --git a/lib/common/asn.ts b/lib/common/asn.ts index 4cff917..8f61ac1 100644 --- a/lib/common/asn.ts +++ b/lib/common/asn.ts @@ -78,19 +78,28 @@ function getRange() { export async function generateASN( metadata: Record = {}, namespace?: number, + deltaCounter = 1, ): Promise { + if (deltaCounter < 1) { + throw new Error("Delta counter must be at least 1"); + } + + if (deltaCounter % 1 !== 0) { + throw new Error("Delta counter must be an integer"); + } + metadata = { ...metadata, generatedAt: new Date().toISOString() }; namespace = namespace ?? getCurrentNamespace(); let counter = 0; await performAtomicTransaction(async (db) => { const counterRes = await db.get(["namespace", namespace]); - counter = counterRes.value ?? 1; + counter = (counterRes.value ?? 0) + deltaCounter; return db.atomic() .check(counterRes) .set( ["namespace", namespace], - counter + 1, + counter, ) .set( ["metadata", namespace, counter], @@ -105,7 +114,7 @@ export async function generateASN( }`, namespace, prefix: CONFIG.ASN_PREFIX, - counter, + counter: counter, metadata, }; diff --git a/main.ts b/main.ts index b7cab34..0c96075 100644 --- a/main.ts +++ b/main.ts @@ -41,6 +41,7 @@ import { runServer } from "$cli/server.ts"; import { printHelp } from "$cli/help.ts"; import { runGenerate } from "$cli/generate.ts"; import { runStats } from "$cli/stats.ts"; +import { runBump } from "$cli/bump.ts"; export * from "$common/mod.ts"; export * from "$http/mod.tsx"; @@ -63,6 +64,10 @@ if (import.meta.main) { await runStats(args); } + if (args._[0] === "bump") { + await runBump(args); + } + if (args._[0] === "server" || args._.length === 0) { await runServer(args); }