gitoriaLog in with ident

ident

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commita3a7d21aa3a7d21aident: Hybriel master 8efba065 (#126 GC by bytes, #48 lambda params copy); session-writing lambdas take &sessionmrea3a7d21a/tools/migrate-009.hl

8.4 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, accountsTable, identitiesTable, otpTable, sendsTable, countOf } from '../store.hl'
  34. import { appsTable, connectionsTable, requestsTable, grantsTable } from '../apps.hl'
  35. oldPrefix = env('IDENT_OLD_STORAGE')
  36. if (oldPrefix == null || !oldPrefix.startsWith('/') || env('IDENT_STORAGE') == null || !env('IDENT_STORAGE').startsWith('/')) {
  37. 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')
  38. }
  39. tableNames = ['accounts' 'identities' 'otp' 'sends' 'apps' 'connections' 'requests' 'grants']
  40. for (t of tableNames) {
  41. if (!exists(oldPrefix + '-' + t + '.mpack')) { hlError('no old table ' + oldPrefix + '-' + t + '.mpack') }
  42. }
  43. // the old tables, exactly as the old store.hl / apps.hl declared them; compact = false
  44. oldAccounts = new MPackDB(file = oldPrefix + '-accounts.db', primaryKey = '*id', indexes = ['!email'], compact = false)
  45. oldIdentities = new MPackDB(file = oldPrefix + '-identities.db', primaryKey = '*id', indexes = ['account'], compact = false)
  46. oldOtp = new MPackDB(file = oldPrefix + '-otp.db', primaryKey = '*id', indexes = ['email'], compact = false)
  47. oldSends = new MPackDB(file = oldPrefix + '-sends.db', primaryKey = '*id', indexes = ['email'], compact = false)
  48. oldApps = new MPackDB(file = oldPrefix + '-apps.db', primaryKey = '*id', indexes = ['owner' '!apiKey'], compact = false)
  49. oldConnections = new MPackDB(file = oldPrefix + '-connections.db', primaryKey = '*id', indexes = ['!pair' 'app' '!appIdentity'], compact = false)
  50. oldRequests = new MPackDB(file = oldPrefix + '-requests.db', primaryKey = '*id', indexes = ['!rid'], compact = false)
  51. oldGrants = new MPackDB(file = oldPrefix + '-grants.db', primaryKey = '*id', indexes = ['!hash'], compact = false)
  52. static all = (table) => {
  53. let out = []
  54. let found = table.find(null, null)
  55. if (countOf(found) > 0) { for (r of found) { out.push(r) } }
  56. return out
  57. }
  58. // old public id (as a string key) → new UUID, from the rows already migrated
  59. static mapOf = (table) => {
  60. let m = {}
  61. for (r of all(table)) { if (r.oldId != null) { m['' + r.oldId] = r.id } }
  62. return m
  63. }
  64. // every field of `rec` but `id`, then `extra` on top
  65. static copied = (rec, extra) => {
  66. let out = {}
  67. for (k of rec.keys()) { if (k != 'id') { out[k] = rec[k] } }
  68. for (k of extra.keys()) { out[k] = extra[k] }
  69. return out
  70. }
  71. failed = 0
  72. report = []
  73. // copy one old table: `refs(rec)` answers the extra fields (the rewritten references),
  74. // or { skip = reason } when a reference cannot be resolved
  75. migrateTable = (name, oldTable, newTable, refs) => {
  76. let have = mapOf(newTable)
  77. let fresh = 0
  78. let known = 0
  79. for (r of all(oldTable)) {
  80. let oldId = r.id + 1
  81. if (have['' + oldId] != null) {
  82. known = known + 1
  83. } else {
  84. let extra = refs(r)
  85. if (extra.skip != null) {
  86. console.log('SKIP ' + name + ' old ' + oldId + ': ' + extra.skip)
  87. failed = failed + 1
  88. } else {
  89. extra.oldId = oldId
  90. let id = newTable.put(copied(r, extra))
  91. if (id == null) {
  92. console.log('FAIL ' + name + ' old ' + oldId + ': ' + newTable.lastError())
  93. failed = failed + 1
  94. } else {
  95. have['' + oldId] = id
  96. fresh = fresh + 1
  97. }
  98. }
  99. }
  100. }
  101. report.push(name + ' ' + fresh + ' new, ' + known + ' already there (old ' + oldTable.count() + ' → new ' + newTable.count() + ')')
  102. return have
  103. }
  104. none = (r) => { return {} }
  105. acc = migrateTable('accounts', oldAccounts, accountsTable, none)
  106. ids = migrateTable('identities', oldIdentities, identitiesTable, (r) => {
  107. let a = acc['' + r.account]
  108. return a == null ? { skip = 'its account ' + r.account + ' is gone' } : { account = a }
  109. })
  110. migrateTable('otp', oldOtp, otpTable, none)
  111. migrateTable('sends', oldSends, sendsTable, none)
  112. apps = migrateTable('apps', oldApps, appsTable, (r) => {
  113. let o = acc['' + r.owner]
  114. return o == null ? { skip = 'its owner ' + r.owner + ' is gone' } : { owner = o }
  115. })
  116. // a connection of a DELETED identity stays (as it did before: its id is still the app's),
  117. // its identity reads 'gone:<old id>' so the pair stays unique and matches no identity
  118. migrateTable('connections', oldConnections, connectionsTable, (r) => {
  119. let a = apps['' + r.app]
  120. if (a == null) { return { skip = 'its app ' + r.app + ' is gone' } }
  121. let i = ids['' + r.identity]
  122. if (i == null) { i = 'gone:' + r.identity }
  123. return { app = a identity = i pair = a + ':' + i }
  124. })
  125. migrateTable('requests', oldRequests, requestsTable, (r) => {
  126. let a = apps['' + r.app]
  127. return a == null ? { skip = 'its app ' + r.app + ' is gone' } : { app = a }
  128. })
  129. migrateTable('grants', oldGrants, grantsTable, (r) => {
  130. let a = apps['' + r.app]
  131. let i = ids['' + r.identity]
  132. if (a == null) { return { skip = 'its app ' + r.app + ' is gone' } }
  133. return { app = a identity = i == null ? 'gone:' + r.identity : i }
  134. })
  135. // ---- the session files --------------------------------------------------------------
  136. sessDir = env('IDENT_SESSIONS_MIGRATE')
  137. static digitsAt = (s, p0) => {
  138. let i = p0
  139. while (i < s.length && s.charCodeAt(i) >= 48 && s.charCodeAt(i) <= 57) { i = i + 1 }
  140. return i
  141. }
  142. if (sessDir != null && sessDir != '') {
  143. if (!sessDir.startsWith('/') || !exists(sessDir)) { hlError('IDENT_SESSIONS_MIGRATE must be an existing ABSOLUTE directory') }
  144. let moved = 0
  145. let already = 0
  146. let anonymous = 0
  147. let unknown = 0
  148. let pre = '"user":{"id":'
  149. for (e of listDir(sessDir)) {
  150. let raw = readFile(e.path)
  151. let at = raw == null ? -1 : raw.indexOf(pre)
  152. if (at < 0) {
  153. anonymous = anonymous + 1
  154. } else {
  155. let p0 = at + pre.length
  156. let end = digitsAt(raw, p0)
  157. if (end == p0) {
  158. already = already + 1 // a string id: migrated before
  159. } else if (raw[end] != '}') {
  160. console.log('SESSION ' + e.name + ': unexpected user shape, left alone')
  161. unknown = unknown + 1
  162. } else {
  163. let n = raw.slice(p0, end)
  164. let to = acc[n]
  165. if (to == null) {
  166. console.log('SESSION ' + e.name + ': account ' + n + ' is not migrated, left alone (it reads as signed out)')
  167. unknown = unknown + 1
  168. } else {
  169. let next = raw.slice(0, p0) + '"' + to + '"' + raw.slice(end, raw.length)
  170. let check = JSON.parse(next)
  171. if (check == null || check.user == null || check.user.id != to) {
  172. console.log('SESSION ' + e.name + ': rewrite did not read back, left alone')
  173. failed = failed + 1
  174. } else {
  175. writeFile(e.path, next, 384)
  176. moved = moved + 1
  177. }
  178. }
  179. }
  180. }
  181. }
  182. report.push('sessions ' + moved + ' new, ' + already + ' already there, ' + anonymous + ' anonymous, ' + unknown + ' left alone (' + sessDir + ')')
  183. }
  184. for (line of report) { console.log('migrate: ' + line) }
  185. console.log('migrate: ' + failed + ' failed (' + oldPrefix + ' → ' + dir + ')')

Branches

Latest commits

  • 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