storage.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import { Log } from "../util/log"
  2. import path from "path"
  3. import fs from "fs/promises"
  4. import { Global } from "../global"
  5. import { lazy } from "../util/lazy"
  6. import { Lock } from "../util/lock"
  7. import { $ } from "bun"
  8. export namespace Storage {
  9. const log = Log.create({ service: "storage" })
  10. type Migration = (dir: string) => Promise<void>
  11. const MIGRATIONS: Migration[] = [
  12. async (dir) => {
  13. const project = path.resolve(dir, "../project")
  14. for await (const projectDir of new Bun.Glob("*").scan({
  15. cwd: project,
  16. onlyFiles: false,
  17. })) {
  18. log.info(`migrating project ${projectDir}`)
  19. let projectID = projectDir
  20. const fullProjectDir = path.join(project, projectDir)
  21. let worktree = "/"
  22. if (projectID !== "global") {
  23. for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({
  24. cwd: path.join(project, projectDir),
  25. absolute: true,
  26. })) {
  27. const json = await Bun.file(msgFile).json()
  28. worktree = json.path?.root
  29. if (worktree) break
  30. }
  31. if (!worktree) continue
  32. if (!(await fs.exists(worktree))) continue
  33. const [id] = await $`git rev-list --max-parents=0 --all`
  34. .quiet()
  35. .nothrow()
  36. .cwd(worktree)
  37. .text()
  38. .then((x) =>
  39. x
  40. .split("\n")
  41. .filter(Boolean)
  42. .map((x) => x.trim())
  43. .toSorted(),
  44. )
  45. if (!id) continue
  46. projectID = id
  47. await Bun.write(
  48. path.join(dir, "project", projectID + ".json"),
  49. JSON.stringify({
  50. id,
  51. vcs: "git",
  52. worktree,
  53. time: {
  54. created: Date.now(),
  55. initialized: Date.now(),
  56. },
  57. }),
  58. )
  59. log.info(`migrating sessions for project ${projectID}`)
  60. for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
  61. cwd: fullProjectDir,
  62. absolute: true,
  63. })) {
  64. const dest = path.join(dir, "session", projectID, path.basename(sessionFile))
  65. log.info("copying", {
  66. sessionFile,
  67. dest,
  68. })
  69. const session = await Bun.file(sessionFile).json()
  70. await Bun.write(dest, JSON.stringify(session))
  71. log.info(`migrating messages for session ${session.id}`)
  72. for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
  73. cwd: fullProjectDir,
  74. absolute: true,
  75. })) {
  76. const dest = path.join(dir, "message", session.id, path.basename(msgFile))
  77. log.info("copying", {
  78. msgFile,
  79. dest,
  80. })
  81. const message = await Bun.file(msgFile).json()
  82. await Bun.write(dest, JSON.stringify(message))
  83. log.info(`migrating parts for message ${message.id}`)
  84. for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
  85. {
  86. cwd: fullProjectDir,
  87. absolute: true,
  88. },
  89. )) {
  90. const dest = path.join(dir, "part", message.id, path.basename(partFile))
  91. const part = await Bun.file(partFile).json()
  92. log.info("copying", {
  93. partFile,
  94. dest,
  95. })
  96. await Bun.write(dest, JSON.stringify(part))
  97. }
  98. }
  99. }
  100. }
  101. }
  102. },
  103. ]
  104. const state = lazy(async () => {
  105. const dir = path.join(Global.Path.data, "storage")
  106. const migration = await Bun.file(path.join(dir, "migration"))
  107. .json()
  108. .then((x) => parseInt(x))
  109. .catch(() => 0)
  110. for (let index = migration; index < MIGRATIONS.length; index++) {
  111. log.info("running migration", { index })
  112. const migration = MIGRATIONS[index]
  113. await migration(dir).catch((e) => {
  114. log.error("failed to run migration", { error: e, index })
  115. })
  116. await Bun.write(path.join(dir, "migration"), (index + 1).toString())
  117. }
  118. return {
  119. dir,
  120. }
  121. })
  122. export async function remove(key: string[]) {
  123. const dir = await state().then((x) => x.dir)
  124. const target = path.join(dir, ...key) + ".json"
  125. await fs.unlink(target).catch(() => {})
  126. }
  127. export async function read<T>(key: string[]) {
  128. const dir = await state().then((x) => x.dir)
  129. const target = path.join(dir, ...key) + ".json"
  130. using _ = await Lock.read(target)
  131. return Bun.file(target).json() as Promise<T>
  132. }
  133. export async function update<T>(key: string[], fn: (draft: T) => void) {
  134. const dir = await state().then((x) => x.dir)
  135. const target = path.join(dir, ...key) + ".json"
  136. using _ = await Lock.write("storage")
  137. const content = await Bun.file(target).json()
  138. fn(content)
  139. await Bun.write(target, JSON.stringify(content, null, 2))
  140. return content as T
  141. }
  142. export async function write<T>(key: string[], content: T) {
  143. const dir = await state().then((x) => x.dir)
  144. const target = path.join(dir, ...key) + ".json"
  145. using _ = await Lock.write("storage")
  146. await Bun.write(target, JSON.stringify(content, null, 2))
  147. }
  148. const glob = new Bun.Glob("**/*")
  149. export async function list(prefix: string[]) {
  150. const dir = await state().then((x) => x.dir)
  151. try {
  152. const result = await Array.fromAsync(
  153. glob.scan({
  154. cwd: path.join(dir, ...prefix),
  155. onlyFiles: true,
  156. }),
  157. ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)]))
  158. result.sort()
  159. return result
  160. } catch {
  161. return []
  162. }
  163. }
  164. }