|
@@ -1,10 +1,4 @@
|
|
|
-import {
|
|
|
|
|
- S3Client,
|
|
|
|
|
- PutObjectCommand,
|
|
|
|
|
- GetObjectCommand,
|
|
|
|
|
- DeleteObjectCommand,
|
|
|
|
|
- ListObjectsV2Command,
|
|
|
|
|
-} from "@aws-sdk/client-s3"
|
|
|
|
|
|
|
+import { AwsClient } from "aws4fetch"
|
|
|
import { lazy } from "@opencode-ai/util/lazy"
|
|
import { lazy } from "@opencode-ai/util/lazy"
|
|
|
|
|
|
|
|
export namespace Storage {
|
|
export namespace Storage {
|
|
@@ -15,81 +9,68 @@ export namespace Storage {
|
|
|
list(prefix: string): Promise<string[]>
|
|
list(prefix: string): Promise<string[]>
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- function createAdapter(client: S3Client, bucket: string): Adapter {
|
|
|
|
|
|
|
+ function createAdapter(client: AwsClient, endpoint: string, bucket: string): Adapter {
|
|
|
|
|
+ const base = `${endpoint}/${bucket}`
|
|
|
return {
|
|
return {
|
|
|
async read(path: string): Promise<string | undefined> {
|
|
async read(path: string): Promise<string | undefined> {
|
|
|
- try {
|
|
|
|
|
- const command = new GetObjectCommand({
|
|
|
|
|
- Bucket: bucket,
|
|
|
|
|
- Key: path,
|
|
|
|
|
- })
|
|
|
|
|
- const response = await client.send(command)
|
|
|
|
|
- if (!response.Body) return undefined
|
|
|
|
|
- return response.Body.transformToString()
|
|
|
|
|
- } catch (e: any) {
|
|
|
|
|
- if (e.name === "NoSuchKey") return undefined
|
|
|
|
|
- throw e
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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> {
|
|
async write(path: string, value: string): Promise<void> {
|
|
|
- const command = new PutObjectCommand({
|
|
|
|
|
- Bucket: bucket,
|
|
|
|
|
- Key: path,
|
|
|
|
|
- Body: value,
|
|
|
|
|
- ContentType: "application/json",
|
|
|
|
|
|
|
+ const response = await client.fetch(`${base}/${path}`, {
|
|
|
|
|
+ method: "PUT",
|
|
|
|
|
+ body: value,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ "Content-Type": "application/json",
|
|
|
|
|
+ },
|
|
|
})
|
|
})
|
|
|
- await client.send(command)
|
|
|
|
|
|
|
+ if (!response.ok) throw new Error(`Failed to write ${path}: ${response.status}`)
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
async remove(path: string): Promise<void> {
|
|
async remove(path: string): Promise<void> {
|
|
|
- const command = new DeleteObjectCommand({
|
|
|
|
|
- Bucket: bucket,
|
|
|
|
|
- Key: path,
|
|
|
|
|
|
|
+ const response = await client.fetch(`${base}/${path}`, {
|
|
|
|
|
+ method: "DELETE",
|
|
|
})
|
|
})
|
|
|
- await client.send(command)
|
|
|
|
|
|
|
+ if (!response.ok) throw new Error(`Failed to remove ${path}: ${response.status}`)
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
async list(prefix: string): Promise<string[]> {
|
|
async list(prefix: string): Promise<string[]> {
|
|
|
- const command = new ListObjectsV2Command({
|
|
|
|
|
- Bucket: bucket,
|
|
|
|
|
- Prefix: prefix,
|
|
|
|
|
- })
|
|
|
|
|
- const response = await client.send(command)
|
|
|
|
|
- return response.Contents?.map((c) => c.Key!) || []
|
|
|
|
|
|
|
+ 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 {
|
|
function s3(): Adapter {
|
|
|
const bucket = process.env.OPENCODE_STORAGE_BUCKET!
|
|
const bucket = process.env.OPENCODE_STORAGE_BUCKET!
|
|
|
- const client = new S3Client({
|
|
|
|
|
- region: process.env.OPENCODE_STORAGE_REGION,
|
|
|
|
|
- credentials: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID
|
|
|
|
|
- ? {
|
|
|
|
|
- accessKeyId: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!,
|
|
|
|
|
- secretAccessKey: process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!,
|
|
|
|
|
- }
|
|
|
|
|
- : undefined,
|
|
|
|
|
|
|
+ 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, bucket)
|
|
|
|
|
|
|
+ return createAdapter(client, `https://s3.${region}.amazonaws.com`, bucket)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function r2() {
|
|
function r2() {
|
|
|
const accountId = process.env.OPENCODE_STORAGE_ACCOUNT_ID!
|
|
const accountId = process.env.OPENCODE_STORAGE_ACCOUNT_ID!
|
|
|
- const accessKeyId = process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!
|
|
|
|
|
- const secretAccessKey = process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!
|
|
|
|
|
- const bucket = process.env.OPENCODE_STORAGE_BUCKET!
|
|
|
|
|
-
|
|
|
|
|
- const client = new S3Client({
|
|
|
|
|
- region: "auto",
|
|
|
|
|
- endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
|
|
|
- credentials: {
|
|
|
|
|
- accessKeyId,
|
|
|
|
|
- secretAccessKey,
|
|
|
|
|
- },
|
|
|
|
|
|
|
+ const client = new AwsClient({
|
|
|
|
|
+ accessKeyId: process.env.OPENCODE_STORAGE_ACCESS_KEY_ID!,
|
|
|
|
|
+ secretAccessKey: process.env.OPENCODE_STORAGE_SECRET_ACCESS_KEY!,
|
|
|
})
|
|
})
|
|
|
- return createAdapter(client, bucket)
|
|
|
|
|
|
|
+ return createAdapter(client, `https://${accountId}.r2.cloudflarestorage.com`, process.env.OPENCODE_STORAGE_BUCKET!)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const adapter = lazy(() => {
|
|
const adapter = lazy(() => {
|