115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
import { AwsClient } from "aws4fetch"
|
|
import { lazy } from "@opencode-ai/util/lazy"
|
|
|
|
export namespace Storage {
|
|
export interface Adapter {
|
|
read(path: string): Promise<string | undefined>
|
|
write(path: string, value: string): Promise<void>
|
|
remove(path: string): Promise<void>
|
|
list(prefix: string): Promise<string[]>
|
|
}
|
|
|
|
function createAdapter(client: AwsClient, endpoint: string, bucket: string): Adapter {
|
|
const base = `${endpoint}/${bucket}`
|
|
return {
|
|
async read(path: string): Promise<string | undefined> {
|
|
const response = await client.fetch(`${base}/${path}`)
|
|
if (response.status === 404) return undefined
|
|
if (!response.ok) throw new Error(`Failed to read ${path}: ${response.status}`)
|
|
return response.text()
|
|
},
|
|
|
|
async write(path: string, value: string): Promise<void> {
|
|
const response = await client.fetch(`${base}/${path}`, {
|
|
method: "PUT",
|
|
body: value,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
})
|
|
if (!response.ok) throw new Error(`Failed to write ${path}: ${response.status}`)
|
|
},
|
|
|
|
async remove(path: string): Promise<void> {
|
|
const response = await client.fetch(`${base}/${path}`, {
|
|
method: "DELETE",
|
|
})
|
|
if (!response.ok) throw new Error(`Failed to remove ${path}: ${response.status}`)
|
|
},
|
|
|
|
async list(prefix: string): Promise<string[]> {
|
|
const params = new URLSearchParams({ "list-type": "2", prefix })
|
|
const response = await client.fetch(`${base}?${params}`)
|
|
if (!response.ok) throw new Error(`Failed to list ${prefix}: ${response.status}`)
|
|
const xml = await response.text()
|
|
const keys: string[] = []
|
|
const regex = /<Key>([^<]+)<\/Key>/g
|
|
let match
|
|
while ((match = regex.exec(xml)) !== null) {
|
|
keys.push(match[1])
|
|
}
|
|
return keys
|
|
},
|
|
}
|
|
}
|
|
|
|
function s3(): Adapter {
|
|
const bucket = process.env.OPENCODE_STORAGE_BUCKET!
|
|
const region = process.env.OPENCODE_STORAGE_REGION || "us-east-1"
|
|
const client = new AwsClient({
|
|
region,
|
|
accessKeyId: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!,
|
|
secretAccessKey: process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!,
|
|
})
|
|
return createAdapter(client, `https://s3.${region}.amazonaws.com`, bucket)
|
|
}
|
|
|
|
function r2() {
|
|
const accountId = process.env.OPENCODE_STORAGE_ACCOUNT_ID!
|
|
const client = new AwsClient({
|
|
accessKeyId: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!,
|
|
secretAccessKey: process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!,
|
|
})
|
|
return createAdapter(client, `https://${accountId}.r2.cloudflarestorage.com`, process.env.OPENCODE_STORAGE_BUCKET!)
|
|
}
|
|
|
|
const adapter = lazy(() => {
|
|
const type = process.env.OPENCODE_STORAGE_ADAPTER
|
|
if (type === "r2") return r2()
|
|
if (type === "s3") return s3()
|
|
throw new Error("No storage adapter configured")
|
|
})
|
|
|
|
function resolve(key: string[]) {
|
|
return key.join("/") + ".json"
|
|
}
|
|
|
|
export async function read<T>(key: string[]) {
|
|
const result = await adapter().read(resolve(key))
|
|
if (!result) return undefined
|
|
return JSON.parse(result) as T
|
|
}
|
|
|
|
export function write<T>(key: string[], value: T) {
|
|
return adapter().write(resolve(key), JSON.stringify(value))
|
|
}
|
|
|
|
export function remove(key: string[]) {
|
|
return adapter().remove(resolve(key))
|
|
}
|
|
|
|
export async function list(prefix: string[]) {
|
|
const p = prefix.join("/") + (prefix.length ? "/" : "")
|
|
const result = await adapter().list(p)
|
|
return result.map((x) => x.replace(/\.json$/, "").split("/"))
|
|
}
|
|
|
|
export async function update<T>(key: string[], fn: (draft: T) => void) {
|
|
const val = await read<T>(key)
|
|
if (!val) throw new Error("Not found")
|
|
fn(val)
|
|
await write(key, val)
|
|
return val
|
|
}
|
|
}
|