storage.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { AwsClient } from "aws4fetch"
  2. import { lazy } from "@opencode-ai/util/lazy"
  3. export namespace Storage {
  4. export interface Adapter {
  5. read(path: string): Promise<string | undefined>
  6. write(path: string, value: string): Promise<void>
  7. remove(path: string): Promise<void>
  8. list(prefix: string): Promise<string[]>
  9. }
  10. function createAdapter(client: AwsClient, endpoint: string, bucket: string): Adapter {
  11. const base = `${endpoint}/${bucket}`
  12. return {
  13. async read(path: string): Promise<string | undefined> {
  14. const response = await client.fetch(`${base}/${path}`)
  15. if (response.status === 404) return undefined
  16. if (!response.ok) throw new Error(`Failed to read ${path}: ${response.status}`)
  17. return response.text()
  18. },
  19. async write(path: string, value: string): Promise<void> {
  20. const response = await client.fetch(`${base}/${path}`, {
  21. method: "PUT",
  22. body: value,
  23. headers: {
  24. "Content-Type": "application/json",
  25. },
  26. })
  27. if (!response.ok) throw new Error(`Failed to write ${path}: ${response.status}`)
  28. },
  29. async remove(path: string): Promise<void> {
  30. const response = await client.fetch(`${base}/${path}`, {
  31. method: "DELETE",
  32. })
  33. if (!response.ok) throw new Error(`Failed to remove ${path}: ${response.status}`)
  34. },
  35. async list(prefix: string): Promise<string[]> {
  36. const params = new URLSearchParams({ "list-type": "2", prefix })
  37. const response = await client.fetch(`${base}?${params}`)
  38. if (!response.ok) throw new Error(`Failed to list ${prefix}: ${response.status}`)
  39. const xml = await response.text()
  40. const keys: string[] = []
  41. const regex = /<Key>([^<]+)<\/Key>/g
  42. let match
  43. while ((match = regex.exec(xml)) !== null) {
  44. keys.push(match[1])
  45. }
  46. return keys
  47. },
  48. }
  49. }
  50. function s3(): Adapter {
  51. const bucket = process.env.OPENCODE_STORAGE_BUCKET!
  52. const region = process.env.OPENCODE_STORAGE_REGION || "us-east-1"
  53. const client = new AwsClient({
  54. region,
  55. accessKeyId: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!,
  56. secretAccessKey: process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!,
  57. })
  58. return createAdapter(client, `https://s3.${region}.amazonaws.com`, bucket)
  59. }
  60. function r2() {
  61. const accountId = process.env.OPENCODE_STORAGE_ACCOUNT_ID!
  62. const client = new AwsClient({
  63. accessKeyId: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!,
  64. secretAccessKey: process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!,
  65. })
  66. return createAdapter(client, `https://${accountId}.r2.cloudflarestorage.com`, process.env.OPENCODE_STORAGE_BUCKET!)
  67. }
  68. const adapter = lazy(() => {
  69. const type = process.env.OPENCODE_STORAGE_ADAPTER
  70. if (type === "r2") return r2()
  71. if (type === "s3") return s3()
  72. throw new Error("No storage adapter configured")
  73. })
  74. function resolve(key: string[]) {
  75. return key.join("/") + ".json"
  76. }
  77. export async function read<T>(key: string[]) {
  78. const result = await adapter().read(resolve(key))
  79. if (!result) return undefined
  80. return JSON.parse(result) as T
  81. }
  82. export function write<T>(key: string[], value: T) {
  83. return adapter().write(resolve(key), JSON.stringify(value))
  84. }
  85. export function remove(key: string[]) {
  86. return adapter().remove(resolve(key))
  87. }
  88. export async function list(prefix: string[]) {
  89. const p = prefix.join("/") + (prefix.length ? "/" : "")
  90. const result = await adapter().list(p)
  91. return result.map((x) => x.replace(/\.json$/, "").split("/"))
  92. }
  93. export async function update<T>(key: string[], fn: (draft: T) => void) {
  94. const val = await read<T>(key)
  95. if (!val) throw new Error("Not found")
  96. fn(val)
  97. await write(key, val)
  98. return val
  99. }
  100. }