gitoriaLog in with ident

ident

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Branchmain5fdbb6b2ident mission 009: report — scratch folder notemremain/tools/migrate-009.hl

8.5 KB

  1. // tools/migrate-009.hl — ONE-OFF MIGRATION (ticket ident #10 / old #39, mission 009,
  2. // 2026-09-24): the old `*id` tables (`storage/ident-<table>.*`) into the new ones
  3. // (`storage/mpackdb/<table>.*`, pk `@id` UUID), plus the session files.
  4. //
  5. // * every row of every table (accounts, identities, otp, sends, apps, connections,
  6. // requests, grants) is copied field by field; `id` becomes a new UUID and the old
  7. // PUBLIC id (old mpackdb id + 1 — what the pages, faces and sessions used) is kept
  8. // as `oldId`.
  9. // * references are rewritten to the new UUIDs: identities.account, apps.owner,
  10. // connections.app / .identity / .pair ('<app>:<identity>'), requests.app,
  11. // grants.app / .identity.
  12. // * WHAT APPS HOLD STAYS BYTE-IDENTICAL: apps.apiKey, apps.secretHash,
  13. // connections.appIdentity, grants.appIdentity/hash, requests.rid are copied as is.
  14. // * SESSIONS (optional, IDENT_SESSIONS_MIGRATE = the .sessions dir): a file whose
  15. // `"user":{"id":<old account number>}` names a migrated account gets that account's
  16. // UUID instead (only that substring changes). Anonymous sessions and ones already
  17. // migrated are left alone.
  18. //
  19. // IDEMPOTENT: a row whose oldId is already in its new table is not written again, a
  20. // session whose user id is already a string is skipped — a second run prints "0 new".
  21. // The OLD FILES are opened with compact = false and never written by this tool — but
  22. // hl:mpackdb rewrites a table's files on open (hybriel #40): RUN IT ON A COPY of them.
  23. //
  24. // STOP THE SERVER FIRST (it holds the tables and the sessions in memory), back up, then:
  25. //
  26. // IDENT_OLD_STORAGE=/abs/copy/ident IDENT_STORAGE=$PWD/storage/mpackdb \
  27. // IDENT_SESSIONS_MIGRATE=$PWD/.sessions bin/hybriel tools/migrate-009.hl
  28. //
  29. // All paths ABSOLUTE (Hybriel resolves relative paths against tools/).
  30. import { MPackDB } from 'hl:mpackdb'
  31. import { env } from 'hl:proc'
  32. import { exists, listDir, readFile, writeFile } from 'hl:fs'
  33. import { dir, countOf } from '../lib/util.hl'
  34. import { accountsTable } from '../lib/accounts.hl'
  35. import { identitiesTable } from '../lib/identities.hl'
  36. import { otpTable, sendsTable } from '../lib/login.hl'
  37. import { appsTable, connectionsTable, requestsTable, grantsTable } from '../lib/apps.hl'
  38. oldPrefix = env('IDENT_OLD_STORAGE')
  39. if (oldPrefix == null || !oldPrefix.startsWith('/') || env('IDENT_STORAGE') == null || !env('IDENT_STORAGE').startsWith('/')) {
  40. hlError('set IDENT_OLD_STORAGE (old table prefix, e.g. /abs/copy/ident) and IDENT_STORAGE (new table directory, e.g. $PWD/storage/mpackdb), both ABSOLUTE')
  41. }
  42. tableNames = ['accounts' 'identities' 'otp' 'sends' 'apps' 'connections' 'requests' 'grants']
  43. for (t of tableNames) {
  44. if (!exists(oldPrefix + '-' + t + '.mpack')) { hlError('no old table ' + oldPrefix + '-' + t + '.mpack') }
  45. }
  46. // the old tables, exactly as the old store.hl / apps.hl declared them; compact = false
  47. oldAccounts = new MPackDB(file = oldPrefix + '-accounts.db', primaryKey = '*id', indexes = ['!email'], compact = false)
  48. oldIdentities = new MPackDB(file = oldPrefix + '-identities.db', primaryKey = '*id', indexes = ['account'], compact = false)
  49. oldOtp = new MPackDB(file = oldPrefix + '-otp.db', primaryKey = '*id', indexes = ['email'], compact = false)
  50. oldSends = new MPackDB(file = oldPrefix + '-sends.db', primaryKey = '*id', indexes = ['email'], compact = false)
  51. oldApps = new MPackDB(file = oldPrefix + '-apps.db', primaryKey = '*id', indexes = ['owner' '!apiKey'], compact = false)
  52. oldConnections = new MPackDB(file = oldPrefix + '-connections.db', primaryKey = '*id', indexes = ['!pair' 'app' '!appIdentity'], compact = false)
  53. oldRequests = new MPackDB(file = oldPrefix + '-requests.db', primaryKey = '*id', indexes = ['!rid'], compact = false)
  54. oldGrants = new MPackDB(file = oldPrefix + '-grants.db', primaryKey = '*id', indexes = ['!hash'], compact = false)
  55. static all = (table) => {
  56. out = []
  57. found = table.find(null, null)
  58. if (countOf(found) > 0) { for (r of found) { out.push(r) } }
  59. return out
  60. }
  61. // old public id (as a string key) → new UUID, from the rows already migrated
  62. static mapOf = (table) => {
  63. m = {}
  64. for (r of all(table)) { if (r.oldId != null) { m['' + r.oldId] = r.id } }
  65. return m
  66. }
  67. // every field of `rec` but `id`, then `extra` on top
  68. static copied = (rec, extra) => {
  69. out = {}
  70. for (k of rec.keys()) { if (k != 'id') { out[k] = rec[k] } }
  71. for (k of extra.keys()) { out[k] = extra[k] }
  72. return out
  73. }
  74. failed = 0
  75. report = []
  76. // copy one old table: `refs(rec)` answers the extra fields (the rewritten references),
  77. // or { skip = reason } when a reference cannot be resolved
  78. migrateTable = (name, oldTable, newTable, refs) => {
  79. have = mapOf(newTable)
  80. let fresh = 0
  81. let known = 0
  82. for (r of all(oldTable)) {
  83. let oldId = r.id + 1
  84. if (have['' + oldId] != null) {
  85. known = known + 1
  86. } else {
  87. let extra = refs(r)
  88. if (extra.skip != null) {
  89. console.log('SKIP ' + name + ' old ' + oldId + ': ' + extra.skip)
  90. failed = failed + 1
  91. } else {
  92. extra.oldId = oldId
  93. let id = newTable.put(copied(r, extra))
  94. if (id == null) {
  95. console.log('FAIL ' + name + ' old ' + oldId + ': ' + newTable.lastError())
  96. failed = failed + 1
  97. } else {
  98. have['' + oldId] = id
  99. fresh = fresh + 1
  100. }
  101. }
  102. }
  103. }
  104. report.push(name + ' ' + fresh + ' new, ' + known + ' already there (old ' + oldTable.count() + ' → new ' + newTable.count() + ')')
  105. return have
  106. }
  107. none = (r) => { return {} }
  108. acc = migrateTable('accounts', oldAccounts, accountsTable, none)
  109. ids = migrateTable('identities', oldIdentities, identitiesTable, (r) => {
  110. a = acc['' + r.account]
  111. return a == null ? { skip = 'its account ' + r.account + ' is gone' } : { account = a }
  112. })
  113. migrateTable('otp', oldOtp, otpTable, none)
  114. migrateTable('sends', oldSends, sendsTable, none)
  115. apps = migrateTable('apps', oldApps, appsTable, (r) => {
  116. o = acc['' + r.owner]
  117. return o == null ? { skip = 'its owner ' + r.owner + ' is gone' } : { owner = o }
  118. })
  119. // a connection of a DELETED identity stays (as it did before: its id is still the app's),
  120. // its identity reads 'gone:<old id>' so the pair stays unique and matches no identity
  121. migrateTable('connections', oldConnections, connectionsTable, (r) => {
  122. a = apps['' + r.app]
  123. if (a == null) { return { skip = 'its app ' + r.app + ' is gone' } }
  124. let i = ids['' + r.identity]
  125. if (i == null) { i = 'gone:' + r.identity }
  126. return { app = a identity = i pair = a + ':' + i }
  127. })
  128. migrateTable('requests', oldRequests, requestsTable, (r) => {
  129. a = apps['' + r.app]
  130. return a == null ? { skip = 'its app ' + r.app + ' is gone' } : { app = a }
  131. })
  132. migrateTable('grants', oldGrants, grantsTable, (r) => {
  133. a = apps['' + r.app]
  134. i = ids['' + r.identity]
  135. if (a == null) { return { skip = 'its app ' + r.app + ' is gone' } }
  136. return { app = a identity = i == null ? 'gone:' + r.identity : i }
  137. })
  138. // ---- the session files --------------------------------------------------------------
  139. sessDir = env('IDENT_SESSIONS_MIGRATE')
  140. static digitsAt = (s, p0) => {
  141. let i = p0
  142. while (i < s.length && s.charCodeAt(i) >= 48 && s.charCodeAt(i) <= 57) { i = i + 1 }
  143. return i
  144. }
  145. if (sessDir != null && sessDir != '') {
  146. if (!sessDir.startsWith('/') || !exists(sessDir)) { hlError('IDENT_SESSIONS_MIGRATE must be an existing ABSOLUTE directory') }
  147. let moved = 0
  148. let already = 0
  149. let anonymous = 0
  150. let unknown = 0
  151. pre = '"user":{"id":'
  152. for (e of listDir(sessDir)) {
  153. let raw = readFile(e.path)
  154. let at = raw == null ? -1 : raw.indexOf(pre)
  155. if (at < 0) {
  156. anonymous = anonymous + 1
  157. } else {
  158. let p0 = at + pre.length
  159. let end = digitsAt(raw, p0)
  160. if (end == p0) {
  161. already = already + 1 // a string id: migrated before
  162. } else if (raw[end] != '}') {
  163. console.log('SESSION ' + e.name + ': unexpected user shape, left alone')
  164. unknown = unknown + 1
  165. } else {
  166. let n = raw.slice(p0, end)
  167. let to = acc[n]
  168. if (to == null) {
  169. console.log('SESSION ' + e.name + ': account ' + n + ' is not migrated, left alone (it reads as signed out)')
  170. unknown = unknown + 1
  171. } else {
  172. let next = raw.slice(0, p0) + '"' + to + '"' + raw.slice(end, raw.length)
  173. let check = JSON.parse(next)
  174. if (check == null || check.user == null || check.user.id != to) {
  175. console.log('SESSION ' + e.name + ': rewrite did not read back, left alone')
  176. failed = failed + 1
  177. } else {
  178. writeFile(e.path, next, 384)
  179. moved = moved + 1
  180. }
  181. }
  182. }
  183. }
  184. }
  185. report.push('sessions ' + moved + ' new, ' + already + ' already there, ' + anonymous + ' anonymous, ' + unknown + ' left alone (' + sessDir + ')')
  186. }
  187. for (line of report) { console.log('migrate: ' + line) }
  188. console.log('migrate: ' + failed + ' failed (' + oldPrefix + ' → ' + dir + ')')

Branches

  • mainmain branch

Latest commits

  • 5fdbb6b2ident mission 009: report — scratch folder notemre
  • 74235815ident mission 009 (4/4): docs (README files map + same-output test, STATUS, LOG), report, tests/letcount.py + realdata-baseline/comparemre
  • fe183516ident mission 009 (3/4): let only where reassigned — 293 never-reassigned lets are plain declarations; kept: reassigned, loop bodies, names of a file member, a name declared twice in one function; same outputmre
  • d2e7f91bident mission 009 (2/4): one lib file per topic (login, accounts, identities, apps, invites, selector, notify + helpers, util), function routes as thin wrappers in lib/api.hl, project.hl = the map; same outputmre
  • 91017164ident mission 009 (1/4): file moves — the root .hl files into lib/ (api.hl → lib/api-helpers.hl), styles.hl → components/styles.hl; imports adjusted, no other changemre
  • f8bdcbc2ident: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); all gates greenmre
  • ff78726cident: Hybriel master 190aa11d (fc838894 GC correctness, #127, #126 closure scopes); gates all greenmre
  • a3a7d21aident: Hybriel master 8efba065 (#126 GC by bytes, #48 lambda params copy); session-writing lambdas take &sessionmre
  • 98226b41antcolony#40: mission references point to the moved missionsmre
  • ff805b9aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 51a7bcdfident: Hybriel master 73267707 (#122); /code uses the new page() signature; pending address passed as parameter; once-checksmre
  • 836f644fident#24: installable app (manifest, service worker, data-free offline /start), own iconmre
  • 8bebbbf2deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • cc063ea2deploy.sh: never send .git or .gitignore to Byrodinmre
  • 81b15b7bState of 2026-09-27, before the move to gitoriamre