storage.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import { Log } from "../util/log"
  2. import { App } from "../app/app"
  3. import { Bus } from "../bus"
  4. import path from "path"
  5. import z from "zod"
  6. import fs from "fs/promises"
  7. import { MessageV2 } from "../session/message-v2"
  8. export namespace Storage {
  9. const log = Log.create({ service: "storage" })
  10. export const Event = {
  11. Write: Bus.event("storage.write", z.object({ key: z.string(), content: z.any() })),
  12. }
  13. type Migration = (dir: string) => Promise<void>
  14. const MIGRATIONS: Migration[] = [
  15. async (dir: string) => {
  16. try {
  17. const files = new Bun.Glob("session/message/*/*.json").scanSync({
  18. cwd: dir,
  19. absolute: true,
  20. })
  21. for (const file of files) {
  22. const content = await Bun.file(file).json()
  23. if (!content.metadata) continue
  24. log.info("migrating to v2 message", { file })
  25. try {
  26. const result = MessageV2.fromV1(content)
  27. await Bun.write(file, JSON.stringify(result, null, 2))
  28. } catch (e) {
  29. await fs.rename(file, file.replace("storage", "broken"))
  30. }
  31. }
  32. } catch {}
  33. },
  34. ]
  35. const state = App.state("storage", async () => {
  36. const app = App.info()
  37. const dir = path.normalize(path.join(app.path.data, "storage"))
  38. await fs.mkdir(dir, { recursive: true })
  39. const migration = await Bun.file(path.join(dir, "migration"))
  40. .json()
  41. .then((x) => parseInt(x))
  42. .catch(() => 0)
  43. for (let index = migration; index < MIGRATIONS.length; index++) {
  44. log.info("running migration", { index })
  45. const migration = MIGRATIONS[index]
  46. await migration(dir)
  47. await Bun.write(path.join(dir, "migration"), (index + 1).toString())
  48. }
  49. return {
  50. dir,
  51. }
  52. })
  53. export async function remove(key: string) {
  54. const dir = await state().then((x) => x.dir)
  55. const target = path.join(dir, key + ".json")
  56. await fs.unlink(target).catch(() => {})
  57. }
  58. export async function removeDir(key: string) {
  59. const dir = await state().then((x) => x.dir)
  60. const target = path.join(dir, key)
  61. await fs.rm(target, { recursive: true, force: true }).catch(() => {})
  62. }
  63. export async function readJSON<T>(key: string) {
  64. const dir = await state().then((x) => x.dir)
  65. return Bun.file(path.join(dir, key + ".json")).json() as Promise<T>
  66. }
  67. export async function writeJSON<T>(key: string, content: T) {
  68. const dir = await state().then((x) => x.dir)
  69. const target = path.join(dir, key + ".json")
  70. const tmp = target + Date.now() + ".tmp"
  71. await Bun.write(tmp, JSON.stringify(content, null, 2))
  72. await fs.rename(tmp, target).catch(() => {})
  73. await fs.unlink(tmp).catch(() => {})
  74. Bus.publish(Event.Write, { key, content })
  75. }
  76. const glob = new Bun.Glob("**/*")
  77. export async function* list(prefix: string) {
  78. const dir = await state().then((x) => x.dir)
  79. try {
  80. for await (const item of glob.scan({
  81. cwd: path.join(dir, prefix),
  82. onlyFiles: true,
  83. })) {
  84. const result = path.join(prefix, item.slice(0, -5))
  85. yield result
  86. }
  87. } catch {
  88. return
  89. }
  90. }
  91. }