File size: 4,669 Bytes
0dbc9de | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | import { AwsClient } from "aws4fetch"
import { lazy } from "@opencode-ai/core/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(options?: { prefix?: string; limit?: number; after?: string; before?: 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(options?: { prefix?: string; limit?: number; after?: string; before?: string }): Promise<string[]> {
const prefix = options?.prefix || ""
const params = new URLSearchParams({ "list-type": "2", prefix })
if (options?.limit) params.set("max-keys", options.limit.toString())
if (options?.after) {
const afterPath = prefix + options.after + ".json"
params.set("start-after", afterPath)
}
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])
}
if (options?.before) {
const beforePath = prefix + options.before + ".json"
return keys.filter((key) => key < beforePath)
}
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(options?: { prefix?: string[]; limit?: number; after?: string; before?: string }) {
const p = options?.prefix ? options.prefix.join("/") + (options.prefix.length ? "/" : "") : ""
const result = await adapter().list({
prefix: p,
limit: options?.limit,
after: options?.after,
before: options?.before,
})
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
}
}
|