| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- import { Log } from "../util/log"
- import path from "path"
- import fs from "fs/promises"
- import { Global } from "../global"
- import { lazy } from "../util/lazy"
- import { Lock } from "../util/lock"
- import { $ } from "bun"
- export namespace Storage {
- const log = Log.create({ service: "storage" })
- type Migration = (dir: string) => Promise<void>
- const MIGRATIONS: Migration[] = [
- async (dir) => {
- const project = path.resolve(dir, "../project")
- for await (const projectDir of new Bun.Glob("*").scan({
- cwd: project,
- onlyFiles: false,
- })) {
- log.info(`migrating project ${projectDir}`)
- let projectID = projectDir
- const fullProjectDir = path.join(project, projectDir)
- let worktree = "/"
- if (projectID !== "global") {
- for await (const msgFile of new Bun.Glob("storage/session/message/*/*.json").scan({
- cwd: path.join(project, projectDir),
- absolute: true,
- })) {
- const json = await Bun.file(msgFile).json()
- worktree = json.path?.root
- if (worktree) break
- }
- if (!worktree) continue
- if (!(await fs.exists(worktree))) continue
- const [id] = await $`git rev-list --max-parents=0 --all`
- .quiet()
- .nothrow()
- .cwd(worktree)
- .text()
- .then((x) =>
- x
- .split("\n")
- .filter(Boolean)
- .map((x) => x.trim())
- .toSorted(),
- )
- if (!id) continue
- projectID = id
- await Bun.write(
- path.join(dir, "project", projectID + ".json"),
- JSON.stringify({
- id,
- vcs: "git",
- worktree,
- time: {
- created: Date.now(),
- initialized: Date.now(),
- },
- }),
- )
- log.info(`migrating sessions for project ${projectID}`)
- for await (const sessionFile of new Bun.Glob("storage/session/info/*.json").scan({
- cwd: fullProjectDir,
- absolute: true,
- })) {
- const dest = path.join(dir, "session", projectID, path.basename(sessionFile))
- log.info("copying", {
- sessionFile,
- dest,
- })
- const session = await Bun.file(sessionFile).json()
- await Bun.write(dest, JSON.stringify(session))
- log.info(`migrating messages for session ${session.id}`)
- for await (const msgFile of new Bun.Glob(`storage/session/message/${session.id}/*.json`).scan({
- cwd: fullProjectDir,
- absolute: true,
- })) {
- const dest = path.join(dir, "message", session.id, path.basename(msgFile))
- log.info("copying", {
- msgFile,
- dest,
- })
- const message = await Bun.file(msgFile).json()
- await Bun.write(dest, JSON.stringify(message))
- log.info(`migrating parts for message ${message.id}`)
- for await (const partFile of new Bun.Glob(`storage/session/part/${session.id}/${message.id}/*.json`).scan(
- {
- cwd: fullProjectDir,
- absolute: true,
- },
- )) {
- const dest = path.join(dir, "part", message.id, path.basename(partFile))
- const part = await Bun.file(partFile).json()
- log.info("copying", {
- partFile,
- dest,
- })
- await Bun.write(dest, JSON.stringify(part))
- }
- }
- }
- }
- }
- },
- ]
- const state = lazy(async () => {
- const dir = path.join(Global.Path.data, "storage")
- const migration = await Bun.file(path.join(dir, "migration"))
- .json()
- .then((x) => parseInt(x))
- .catch(() => 0)
- for (let index = migration; index < MIGRATIONS.length; index++) {
- log.info("running migration", { index })
- const migration = MIGRATIONS[index]
- await migration(dir).catch((e) => {
- log.error("failed to run migration", { error: e, index })
- })
- await Bun.write(path.join(dir, "migration"), (index + 1).toString())
- }
- return {
- dir,
- }
- })
- export async function remove(key: string[]) {
- const dir = await state().then((x) => x.dir)
- const target = path.join(dir, ...key) + ".json"
- await fs.unlink(target).catch(() => {})
- }
- export async function read<T>(key: string[]) {
- const dir = await state().then((x) => x.dir)
- const target = path.join(dir, ...key) + ".json"
- using _ = await Lock.read(target)
- return Bun.file(target).json() as Promise<T>
- }
- export async function update<T>(key: string[], fn: (draft: T) => void) {
- const dir = await state().then((x) => x.dir)
- const target = path.join(dir, ...key) + ".json"
- using _ = await Lock.write("storage")
- const content = await Bun.file(target).json()
- fn(content)
- await Bun.write(target, JSON.stringify(content, null, 2))
- return content as T
- }
- export async function write<T>(key: string[], content: T) {
- const dir = await state().then((x) => x.dir)
- const target = path.join(dir, ...key) + ".json"
- using _ = await Lock.write("storage")
- await Bun.write(target, JSON.stringify(content, null, 2))
- }
- const glob = new Bun.Glob("**/*")
- export async function list(prefix: string[]) {
- const dir = await state().then((x) => x.dir)
- try {
- const result = await Array.fromAsync(
- glob.scan({
- cwd: path.join(dir, ...prefix),
- onlyFiles: true,
- }),
- ).then((results) => results.map((x) => [...prefix, ...x.slice(0, -5).split(path.sep)]))
- result.sort()
- return result
- } catch {
- return []
- }
- }
- }
|