mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(tools): added s3 tools/block (#297)
* s3 block * updated logo, bg color * added docs * added helper to parse s3 uri --------- Co-authored-by: Rishabh Shinde <rishabhsshinde27@gmail.com>
This commit is contained in:
co-authored by
Rishabh Shinde
parent
d13eeccbbb
commit
2749cebe31
@@ -25,6 +25,7 @@
|
||||
"perplexity",
|
||||
"pinecone",
|
||||
"reddit",
|
||||
"s3",
|
||||
"serper",
|
||||
"slack",
|
||||
"stagehand",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: S3
|
||||
description: View S3 files
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="s3"
|
||||
color="#E0E0E0"
|
||||
icon={true}
|
||||
iconSvg={`<svg className="block-icon" preserveAspectRatio="xMidYMid" viewBox="0 0 256 310" xmlns="http://www.w3.org/2000/svg"><path d="m20.624 53.686-20.624 10.314v181.02l20.624 10.254.124-.149v-201.297z" fill="#8c3123"/><path d="m131 229-110.376 26.274v-201.588l110.376 25.701z" fill="#e05243"/><path d="m81.178 187.866 46.818 5.96.294-.678.263-76.77-.557-.6-46.818 5.874z" fill="#8c3123"/><path d="m127.996 229.295 107.371 26.035.169-.269-.003-201.195-.17-.18-107.367 25.996z" fill="#8c3123"/><path d="m174.827 187.866-46.831 5.96v-78.048l46.831 5.874z" fill="#e05243"/><path d="m174.827 89.631-46.831 8.535-46.818-8.535 46.759-12.256z" fill="#5e1f18"/><path d="m174.827 219.801-46.831-8.591-46.818 8.591 46.761 13.053z" fill="#f2b0a9"/><path d="m81.178 89.631 46.818-11.586.379-.117v-77.615l-.379-.313-46.818 23.413z" fill="#8c3123"/><path d="m174.827 89.631-46.831-11.586v-78.045l46.831 23.413z" fill="#e05243"/><path d="m127.996 309.428-46.823-23.405v-66.217l46.823 11.582.689.783-.187 75.906z" fill="#8c3123"/><g fill="#e05243"><path d="m127.996 309.428 46.827-23.405v-66.217l-46.827 11.582z"/><path d="m235.367 53.686 20.633 10.314v181.02l-20.633 10.31z"/></g></svg>`}
|
||||
/>
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Retrieve and view files from Amazon S3 buckets using presigned URLs.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `get_object`
|
||||
|
||||
|
||||
|
||||
|
||||
## Block Configuration
|
||||
|
||||
### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `accessKeyId` | string | Yes | Access Key ID - Enter your AWS Access Key ID |
|
||||
|
||||
|
||||
|
||||
### Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| `response` | object | Output from response |
|
||||
| ↳ `url` | string | url of the response |
|
||||
| ↳ `metadata` | json | metadata of the response |
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Category: `tools`
|
||||
- Type: `s3`
|
||||
+14
-3
@@ -43,7 +43,18 @@ const copyToClipboard = (data: any) => {
|
||||
|
||||
// Helper function to check if an object contains an image URL
|
||||
const isImageData = (obj: any): boolean => {
|
||||
return obj && typeof obj === 'object' && 'url' in obj && typeof obj.url === 'string'
|
||||
if (!obj || typeof obj !== 'object' || !('url' in obj) || typeof obj.url !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if we have metadata with file type
|
||||
if (obj.metadata?.fileType) {
|
||||
return obj.metadata.fileType.startsWith('image/')
|
||||
}
|
||||
|
||||
// Fallback to checking URL extension
|
||||
const url = obj.url.toLowerCase()
|
||||
return url.match(/\.(jpg|jpeg|png|gif|webp|svg)(\?|$)/) !== null
|
||||
}
|
||||
|
||||
// Helper function to check if an object contains an audio URL
|
||||
@@ -180,7 +191,7 @@ const ImagePreview = ({
|
||||
// Determine the source for the image
|
||||
const imageSrc =
|
||||
isBase64 && imageData && imageData.length > 0
|
||||
? `data:image/png;base64,${imageData}`
|
||||
? `data:image/pngbase64,${imageData}`
|
||||
: imageUrl || ''
|
||||
|
||||
return (
|
||||
@@ -638,4 +649,4 @@ export const JSONView = ({ data, level = 0, initiallyExpanded = false }: JSONVie
|
||||
<span className="text-muted-foreground">{isArray ? ']' : '}'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { S3Icon } from '@/components/icons'
|
||||
import { S3Response } from '@/tools/s3/types'
|
||||
import { BlockConfig } from '../types'
|
||||
|
||||
export const S3Block: BlockConfig<S3Response> = {
|
||||
type: 's3',
|
||||
name: 'S3',
|
||||
description: 'View S3 files',
|
||||
longDescription:
|
||||
'Retrieve and view files from Amazon S3 buckets using presigned URLs.',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: S3Icon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'accessKeyId',
|
||||
title: 'Access Key ID',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter your AWS Access Key ID',
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
id: 'secretAccessKey',
|
||||
title: 'Secret Access Key',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'Enter your AWS Secret Access Key',
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
id: 's3Uri',
|
||||
title: 'S3 Object URL',
|
||||
type: 'short-input',
|
||||
layout: 'full',
|
||||
placeholder: 'e.g., https://bucket-name.s3.region.amazonaws.com/path/to/file',
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: ['get_object'],
|
||||
config: {
|
||||
tool: () => 'get_object',
|
||||
params: (params) => {
|
||||
// Validate required fields
|
||||
if (!params.accessKeyId) {
|
||||
throw new Error('Access Key ID is required')
|
||||
}
|
||||
if (!params.secretAccessKey) {
|
||||
throw new Error('Secret Access Key is required')
|
||||
}
|
||||
if (!params.s3Uri) {
|
||||
throw new Error('S3 Object URL is required')
|
||||
}
|
||||
|
||||
// Parse S3 URI
|
||||
try {
|
||||
const url = new URL(params.s3Uri)
|
||||
const hostname = url.hostname
|
||||
|
||||
// Extract bucket name from hostname
|
||||
const bucketName = hostname.split('.')[0]
|
||||
|
||||
// Extract region from hostname
|
||||
const regionMatch = hostname.match(/s3[.-]([^.]+)\.amazonaws\.com/)
|
||||
const region = regionMatch ? regionMatch[1] : 'us-east-1'
|
||||
|
||||
// Extract object key from pathname (remove leading slash)
|
||||
const objectKey = url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname
|
||||
|
||||
if (!bucketName) {
|
||||
throw new Error('Could not extract bucket name from URL')
|
||||
}
|
||||
|
||||
if (!objectKey) {
|
||||
throw new Error('No object key found in URL')
|
||||
}
|
||||
|
||||
return {
|
||||
accessKeyId: params.accessKeyId,
|
||||
secretAccessKey: params.secretAccessKey,
|
||||
region,
|
||||
bucketName,
|
||||
objectKey,
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Invalid S3 Object URL format. Expected format: https://bucket-name.s3.region.amazonaws.com/path/to/file')
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
accessKeyId: { type: 'string', required: true },
|
||||
secretAccessKey: { type: 'string', required: true },
|
||||
s3Uri: { type: 'string', required: true },
|
||||
},
|
||||
outputs: {
|
||||
response: {
|
||||
type: {
|
||||
url: 'string',
|
||||
metadata: 'json'
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import { XBlock } from './blocks/x'
|
||||
import { YouTubeBlock } from './blocks/youtube'
|
||||
import { BlockConfig } from './types'
|
||||
import { Mem0Block } from './blocks/mem0'
|
||||
import { S3Block } from './blocks/s3'
|
||||
|
||||
// Export blocks for ease of use
|
||||
export {
|
||||
@@ -95,6 +96,7 @@ export {
|
||||
ThinkingBlock,
|
||||
StagehandBlock,
|
||||
StagehandAgentBlock,
|
||||
S3Block,
|
||||
}
|
||||
|
||||
// Registry of all block configurations, alphabetically sorted
|
||||
@@ -130,6 +132,7 @@ const blocks: Record<string, BlockConfig> = {
|
||||
pinecone: PineconeBlock,
|
||||
reddit: RedditBlock,
|
||||
router: RouterBlock,
|
||||
s3: S3Block,
|
||||
serper: SerperBlock,
|
||||
stagehand: StagehandBlock,
|
||||
stagehand_agent: StagehandAgentBlock,
|
||||
|
||||
@@ -1306,33 +1306,9 @@ export function GoogleSheetsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function S3Icon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 80 80"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="s3-gradient">
|
||||
<stop stopColor="#1B660F" offset="0%"></stop>
|
||||
<stop stopColor="#6CAE3E" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||
<g fill="url(#s3-gradient)">
|
||||
<rect x="0" y="0" width="80" height="80"></rect>
|
||||
</g>
|
||||
<g transform="translate(8.000000, 8.000000)" fill="#FFFFFF">
|
||||
<path d="M52.8359,34.8926 L53.2199,32.1886 C56.7609,34.3096 56.8069,35.1856 56.8059132,35.2096 C56.7999,35.2146 56.1959,35.7186 52.8359,34.8926 L52.8359,34.8926 Z M50.8929,34.3526 C44.7729,32.5006 36.2499,28.5906 32.8009,26.9606 C32.8009,26.9466 32.8049,26.9336 32.8049,26.9196 C32.8049,25.5946 31.7269,24.5166 30.4009,24.5166 C29.0769,24.5166 27.9989,25.5946 27.9989,26.9196 C27.9989,28.2446 29.0769,29.3226 30.4009,29.3226 C30.9829,29.3226 31.5109,29.1056 31.9279,28.7606 C35.9859,30.6816 44.4429,34.5346 50.6079,36.3546 L48.1699,53.5606 C48.1629,53.6076 48.1599,53.6546 48.1599,53.7016 C48.1599,55.2166 41.4529,57.9996 30.4939,57.9996 C19.4189,57.9996 12.6409,55.2166 12.6409,53.7016 C12.6409,53.6556 12.6379,53.6106 12.6319,53.5656 L7.5379,16.3586 C11.9469,19.3936 21.4299,20.9996 30.4999,20.9996 C39.5559,20.9996 49.0229,19.3996 53.4409,16.3736 L50.8929,34.3526 Z M6.9999,12.4776 C7.0719,11.1616 14.6339,5.9996 30.4999,5.9996 C46.3639,5.9996 53.9269,11.1606 53.9999,12.4776 L53.9999,12.9266 C53.1299,15.8776 43.3299,18.9996 30.4999,18.9996 C17.6479,18.9996 7.8429,15.8676 6.9999,12.9126 L6.9999,12.4776 Z M55.9999,12.4996 C55.9999,9.0346 46.0659,3.9996 30.4999,3.9996 C14.9339,3.9996 4.9999,9.0346 4.9999,12.4996 L5.0939,13.2536 L10.6419,53.7776 C10.7749,58.3096 22.8609,59.9996 30.4939,59.9996 C39.9659,59.9996 50.0289,57.8216 50.1589,53.7806 L52.5549,36.8836 C53.8879,37.2026 54.9849,37.3656 55.8659,37.3656 C57.0489,37.3656 57.8489,37.0766 58.3339,36.4986 C58.7319,36.0246 58.8839,35.4506 58.7699,34.8396 C58.5109,33.4556 56.8679,31.9636 53.5219,30.0546 L55.8979,13.2926 L55.9999,12.4996 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export const S3Icon = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg {...props} height="310" preserveAspectRatio="xMidYMid" viewBox="0 0 256 310" width="256" xmlns="http://www.w3.org/2000/svg"><path d="m20.624 53.686-20.624 10.314v181.02l20.624 10.254.124-.149v-201.297z" fill="#8c3123"/><path d="m131 229-110.376 26.274v-201.588l110.376 25.701z" fill="#e05243"/><path d="m81.178 187.866 46.818 5.96.294-.678.263-76.77-.557-.6-46.818 5.874z" fill="#8c3123"/><path d="m127.996 229.295 107.371 26.035.169-.269-.003-201.195-.17-.18-107.367 25.996z" fill="#8c3123"/><path d="m174.827 187.866-46.831 5.96v-78.048l46.831 5.874z" fill="#e05243"/><path d="m174.827 89.631-46.831 8.535-46.818-8.535 46.759-12.256z" fill="#5e1f18"/><path d="m174.827 219.801-46.831-8.591-46.818 8.591 46.761 13.053z" fill="#f2b0a9"/><path d="m81.178 89.631 46.818-11.586.379-.117v-77.615l-.379-.313-46.818 23.413z" fill="#8c3123"/><path d="m174.827 89.631-46.831-11.586v-78.045l46.831 23.413z" fill="#e05243"/><path d="m127.996 309.428-46.823-23.405v-66.217l46.823 11.582.689.783-.187 75.906z" fill="#8c3123"/><g fill="#e05243"><path d="m127.996 309.428 46.827-23.405v-66.217l-46.827 11.582z"/><path d="m235.367 53.686 20.633 10.314v181.02l-20.633 10.31z"/></g></svg>
|
||||
)
|
||||
|
||||
export function GoogleIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
|
||||
Generated
+6350
-1736
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,7 @@ import { xReadTool, xSearchTool, xUserTool, xWriteTool } from './x'
|
||||
import { youtubeSearchTool } from './youtube'
|
||||
import { elevenLabsTtsTool } from './elevenlabs'
|
||||
import { ToolConfig } from './types'
|
||||
import { s3GetObjectTool } from './s3'
|
||||
|
||||
// Registry of all available tools
|
||||
export const tools: Record<string, ToolConfig> = {
|
||||
@@ -118,4 +119,5 @@ export const tools: Record<string, ToolConfig> = {
|
||||
mem0_search_memories: mem0SearchMemoriesTool,
|
||||
mem0_get_memories: mem0GetMemoriesTool,
|
||||
elevenlabs_tts: elevenLabsTtsTool,
|
||||
get_object: s3GetObjectTool,
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { ToolConfig } from '../types'
|
||||
import crypto from 'crypto'
|
||||
|
||||
// Function to encode S3 path components
|
||||
function encodeS3PathComponent(pathComponent: string): string {
|
||||
return encodeURIComponent(pathComponent).replace(/%2F/g, '/')
|
||||
}
|
||||
|
||||
// Function to calculate AWS signature key
|
||||
function getSignatureKey(key: string, dateStamp: string, regionName: string, serviceName: string): Buffer {
|
||||
if (!key || typeof key !== 'string') {
|
||||
throw new Error('Invalid key provided to getSignatureKey')
|
||||
}
|
||||
const kDate = crypto.createHmac('sha256', 'AWS4' + key).update(dateStamp).digest()
|
||||
const kRegion = crypto.createHmac('sha256', kDate).update(regionName).digest()
|
||||
const kService = crypto.createHmac('sha256', kRegion).update(serviceName).digest()
|
||||
const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest()
|
||||
return kSigning
|
||||
}
|
||||
|
||||
function parseS3Uri(s3Uri: string): { bucketName: string; region: string; objectKey: string } {
|
||||
try {
|
||||
const url = new URL(s3Uri)
|
||||
const hostname = url.hostname
|
||||
const bucketName = hostname.split('.')[0]
|
||||
const regionMatch = hostname.match(/s3[.-]([^.]+)\.amazonaws\.com/)
|
||||
const region = regionMatch ? regionMatch[1] : 'us-east-1'
|
||||
const objectKey = url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname
|
||||
|
||||
if (!bucketName || !objectKey) {
|
||||
throw new Error('Invalid S3 URI format')
|
||||
}
|
||||
|
||||
return { bucketName, region, objectKey }
|
||||
} catch (error) {
|
||||
throw new Error('Invalid S3 Object URL format. Expected format: https://bucket-name.s3.region.amazonaws.com/path/to/file')
|
||||
}
|
||||
}
|
||||
|
||||
// Function to generate a pre-signed URL
|
||||
function generatePresignedUrl(params: any, expiresIn: number = 3600): string {
|
||||
const date = new Date()
|
||||
const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
||||
const dateStamp = amzDate.slice(0, 8)
|
||||
const encodedPath = encodeS3PathComponent(params.objectKey)
|
||||
|
||||
// Set expiration time
|
||||
const expires = Math.floor(Date.now() / 1000) + expiresIn
|
||||
|
||||
// Create the canonical request
|
||||
const method = 'GET'
|
||||
const canonicalUri = `/${encodedPath}`
|
||||
const canonicalQueryString = `X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=${encodeURIComponent(params.accessKeyId + '/' + dateStamp + '/' + params.region + '/s3/aws4_request')}&X-Amz-Date=${amzDate}&X-Amz-Expires=${expiresIn}&X-Amz-SignedHeaders=host`
|
||||
const canonicalHeaders = 'host:' + params.bucketName + '.s3.' + params.region + '.amazonaws.com\n'
|
||||
const signedHeaders = 'host'
|
||||
const payloadHash = 'UNSIGNED-PAYLOAD'
|
||||
|
||||
const canonicalRequest = method + '\n' +
|
||||
canonicalUri + '\n' +
|
||||
canonicalQueryString + '\n' +
|
||||
canonicalHeaders + '\n' +
|
||||
signedHeaders + '\n' +
|
||||
payloadHash
|
||||
|
||||
// Create string to sign
|
||||
const algorithm = 'AWS4-HMAC-SHA256'
|
||||
const credentialScope = dateStamp + '/' + params.region + '/s3/aws4_request'
|
||||
const stringToSign = algorithm + '\n' +
|
||||
amzDate + '\n' +
|
||||
credentialScope + '\n' +
|
||||
crypto.createHash('sha256').update(canonicalRequest).digest('hex')
|
||||
|
||||
// Calculate signature
|
||||
const signingKey = getSignatureKey(params.secretAccessKey, dateStamp, params.region, 's3')
|
||||
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex')
|
||||
|
||||
// Create signed URL
|
||||
return `https://${params.bucketName}.s3.${params.region}.amazonaws.com/${encodedPath}?${canonicalQueryString}&X-Amz-Signature=${signature}`
|
||||
}
|
||||
|
||||
// Get Object Tool
|
||||
export const s3GetObjectTool: ToolConfig = {
|
||||
id: 'get_object',
|
||||
name: 'S3 Get Object',
|
||||
description: 'Retrieve an object from an AWS S3 bucket',
|
||||
version: '2.0.0',
|
||||
params: {
|
||||
accessKeyId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Your AWS Access Key ID',
|
||||
},
|
||||
secretAccessKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Your AWS Secret Access Key',
|
||||
},
|
||||
s3Uri: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'S3 Object URL (e.g., https://bucket-name.s3.region.amazonaws.com/path/to/file)',
|
||||
}
|
||||
},
|
||||
request: {
|
||||
url: (params) => {
|
||||
try {
|
||||
const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri)
|
||||
|
||||
params.bucketName = bucketName
|
||||
params.region = region
|
||||
params.objectKey = objectKey
|
||||
|
||||
return `https://${bucketName}.s3.${region}.amazonaws.com/${encodeS3PathComponent(objectKey)}`
|
||||
} catch (error) {
|
||||
throw new Error('Invalid S3 Object URL format. Expected format: https://bucket-name.s3.region.amazonaws.com/path/to/file')
|
||||
}
|
||||
},
|
||||
method: 'HEAD',
|
||||
headers: (params) => {
|
||||
try {
|
||||
// Parse S3 URI if not already parsed
|
||||
if (!params.bucketName || !params.region || !params.objectKey) {
|
||||
const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri)
|
||||
params.bucketName = bucketName
|
||||
params.region = region
|
||||
params.objectKey = objectKey
|
||||
}
|
||||
|
||||
const date = new Date()
|
||||
const amzDate = date.toISOString().replace(/[:-]|\.\d{3}/g, '')
|
||||
const dateStamp = amzDate.slice(0, 8)
|
||||
|
||||
const method = 'HEAD'
|
||||
const encodedPath = encodeS3PathComponent(params.objectKey)
|
||||
const canonicalUri = `/${encodedPath}`
|
||||
const canonicalQueryString = ''
|
||||
const payloadHash = crypto.createHash('sha256').update('').digest('hex')
|
||||
const canonicalHeaders =
|
||||
'host:' + params.bucketName + '.s3.' + params.region + '.amazonaws.com' + '\n' +
|
||||
'x-amz-content-sha256:' + payloadHash + '\n' +
|
||||
'x-amz-date:' + amzDate + '\n'
|
||||
const signedHeaders = 'hostx-amz-content-sha256x-amz-date'
|
||||
const canonicalRequest = method + '\n' + canonicalUri + '\n' + canonicalQueryString + '\n' +
|
||||
canonicalHeaders + '\n' + signedHeaders + '\n' + payloadHash
|
||||
|
||||
const algorithm = 'AWS4-HMAC-SHA256'
|
||||
const credentialScope = dateStamp + '/' + params.region + '/s3/aws4_request'
|
||||
const stringToSign = algorithm + '\n' + amzDate + '\n' + credentialScope + '\n' +
|
||||
crypto.createHash('sha256').update(canonicalRequest).digest('hex')
|
||||
|
||||
const signingKey = getSignatureKey(params.secretAccessKey, dateStamp, params.region, 's3')
|
||||
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex')
|
||||
|
||||
const authorizationHeader = algorithm + ' ' +
|
||||
'Credential=' + params.accessKeyId + '/' + credentialScope + ', ' +
|
||||
'SignedHeaders=' + signedHeaders + ', ' +
|
||||
'Signature=' + signature
|
||||
|
||||
return {
|
||||
'X-Amz-Content-Sha256': payloadHash,
|
||||
'X-Amz-Date': amzDate,
|
||||
'Authorization': authorizationHeader
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
throw new Error('Failed to generate request headers: ' + errorMessage)
|
||||
}
|
||||
}
|
||||
},
|
||||
transformResponse: async (response: Response, params) => {
|
||||
try {
|
||||
if (!response.ok) {
|
||||
throw new Error(`S3 request failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
// Parse S3 URI if not already parsed
|
||||
if (!params.bucketName || !params.region || !params.objectKey) {
|
||||
const { bucketName, region, objectKey } = parseS3Uri(params.s3Uri)
|
||||
params.bucketName = bucketName
|
||||
params.region = region
|
||||
params.objectKey = objectKey
|
||||
}
|
||||
|
||||
// Get file metadata
|
||||
const contentType = response.headers.get('content-type') || 'application/octet-stream'
|
||||
const contentLength = parseInt(response.headers.get('content-length') || '0', 10)
|
||||
const lastModified = response.headers.get('last-modified') || new Date().toISOString()
|
||||
const fileName = params.objectKey.split('/').pop() || params.objectKey
|
||||
|
||||
// Generate pre-signed URL for download
|
||||
const url = generatePresignedUrl(params, 3600)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
url,
|
||||
metadata: {
|
||||
fileType: contentType,
|
||||
size: contentLength,
|
||||
name: fileName,
|
||||
lastModified: lastModified
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
return {
|
||||
success: false,
|
||||
output: {
|
||||
url: '',
|
||||
metadata: {
|
||||
fileType: 'error',
|
||||
size: 0,
|
||||
name: params.objectKey?.split('/').pop() || 'unknown',
|
||||
error: errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { s3GetObjectTool } from './get_object'
|
||||
|
||||
export { s3GetObjectTool }
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ToolResponse } from '../types'
|
||||
|
||||
export interface S3Response extends ToolResponse {
|
||||
output: {
|
||||
url: string
|
||||
metadata: {
|
||||
fileType: string
|
||||
size: number
|
||||
name: string
|
||||
lastModified: string
|
||||
error?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user