diff --git a/functions/infra/s3_download.go b/functions/infra/s3_download.go new file mode 100644 index 00000000..a5397823 --- /dev/null +++ b/functions/infra/s3_download.go @@ -0,0 +1,17 @@ +package infra + +import ( + "fmt" + "io" +) + +// S3Download descarga el objeto identificado por key desde el bucket S3-compatible +// y escribe su contenido en dst. +// +// STUB: la implementacion real requiere github.com/aws/aws-sdk-go-v2. +func S3Download(cfg S3Config, key string, dst io.Writer) error { + _ = cfg + _ = key + _ = dst + return fmt.Errorf("s3_download: not implemented (requiere github.com/aws/aws-sdk-go-v2)") +} diff --git a/functions/infra/s3_download.md b/functions/infra/s3_download.md new file mode 100644 index 00000000..00fe16d4 --- /dev/null +++ b/functions/infra/s3_download.md @@ -0,0 +1,81 @@ +--- +name: s3_download +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func S3Download(cfg S3Config, key string, dst io.Writer) error" +description: "STUB. Descarga el objeto key desde un bucket S3-compatible y escribe su contenido en dst. Permite streaming directo a disco o a un HTTP response. Requiere github.com/aws/aws-sdk-go-v2." +tags: [s3, download, storage, cloud, stub, infra] +uses_functions: [] +uses_types: [S3Config_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [fmt, io] +params: + - name: cfg + desc: "S3Config con endpoint, bucket, credenciales y region" + - name: key + desc: "key del objeto a descargar (ej: \"images/foto.png\")" + - name: dst + desc: "writer donde se escribe el contenido (ej: os.File, http.ResponseWriter, bytes.Buffer)" +output: "nil si la descarga fue exitosa, error si fallo. STUB actual retorna siempre error \"not implemented\"" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/s3_download.go" +--- + +## Estado + +**Stub**. Para activar la implementacion real: + +1. Añadir las mismas dependencias que `s3_upload`. +2. Reemplazar el cuerpo del stub por: + ```go + func S3Download(cfg S3Config, key string, dst io.Writer) error { + ctx := context.Background() + awsCfg, err := awscfg.LoadDefaultConfig(ctx, + awscfg.WithRegion(cfg.Region), + awscfg.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + cfg.AccessKey, cfg.SecretKey, "")), + ) + if err != nil { + return fmt.Errorf("s3_download: aws config: %w", err) + } + client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.UsePathStyle = true + if cfg.Endpoint != "" { + scheme := "http" + if cfg.UseSSL { + scheme = "https" + } + o.BaseEndpoint = aws.String(fmt.Sprintf("%s://%s", scheme, cfg.Endpoint)) + } + }) + out, err := client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(cfg.Bucket), + Key: aws.String(key), + }) + if err != nil { + return fmt.Errorf("s3_download: get object: %w", err) + } + defer out.Body.Close() + _, err = io.Copy(dst, out.Body) + return err + } + ``` + +## Ejemplo (con implementacion real) + +```go +f, _ := os.Create("./local/photo.png") +defer f.Close() +err := S3Download(cfg, "images/photo.png", f) +``` + +## Notas + +Soporta streaming — el `dst` puede ser un `os.File`, `http.ResponseWriter`, `bytes.Buffer`, o cualquier `io.Writer`. Para archivos grandes, escribir directamente al cliente HTTP evita cargarlo todo en memoria. diff --git a/functions/infra/s3_presign_url.go b/functions/infra/s3_presign_url.go new file mode 100644 index 00000000..4e82b5ff --- /dev/null +++ b/functions/infra/s3_presign_url.go @@ -0,0 +1,17 @@ +package infra + +import ( + "fmt" + "time" +) + +// S3PresignURL genera una URL presignada para download (GET) del objeto key, +// valida durante expiry. +// +// STUB: la implementacion real requiere github.com/aws/aws-sdk-go-v2 (s3.PresignClient). +func S3PresignURL(cfg S3Config, key string, expiry time.Duration) (string, error) { + _ = cfg + _ = key + _ = expiry + return "", fmt.Errorf("s3_presign_url: not implemented (requiere github.com/aws/aws-sdk-go-v2)") +} diff --git a/functions/infra/s3_presign_url.md b/functions/infra/s3_presign_url.md new file mode 100644 index 00000000..3b88be98 --- /dev/null +++ b/functions/infra/s3_presign_url.md @@ -0,0 +1,80 @@ +--- +name: s3_presign_url +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func S3PresignURL(cfg S3Config, key string, expiry time.Duration) (string, error)" +description: "STUB. Genera una URL presignada para download (GET) del objeto key en un bucket S3-compatible, valida durante expiry. Util para descargas directas sin pasar por el servidor. Requiere github.com/aws/aws-sdk-go-v2." +tags: [s3, presign, url, storage, cloud, stub, infra] +uses_functions: [] +uses_types: [S3Config_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [fmt, time] +params: + - name: cfg + desc: "S3Config con endpoint, bucket, credenciales y region" + - name: key + desc: "key del objeto en el bucket (ej: \"images/foto.png\")" + - name: expiry + desc: "duracion de validez de la URL (ej: time.Hour)" +output: "URL presignada como string. Empty + error si falla la generacion. STUB actual retorna siempre error \"not implemented\"" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/s3_presign_url.go" +--- + +## Estado + +**Stub**. Para activar la implementacion real: + +1. Añadir las mismas dependencias que `s3_upload`. +2. Reemplazar el cuerpo del stub por: + ```go + func S3PresignURL(cfg S3Config, key string, expiry time.Duration) (string, error) { + ctx := context.Background() + awsCfg, err := awscfg.LoadDefaultConfig(ctx, + awscfg.WithRegion(cfg.Region), + awscfg.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + cfg.AccessKey, cfg.SecretKey, "")), + ) + if err != nil { + return "", fmt.Errorf("s3_presign_url: aws config: %w", err) + } + client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.UsePathStyle = true + if cfg.Endpoint != "" { + scheme := "http" + if cfg.UseSSL { + scheme = "https" + } + o.BaseEndpoint = aws.String(fmt.Sprintf("%s://%s", scheme, cfg.Endpoint)) + } + }) + psClient := s3.NewPresignClient(client) + req, err := psClient.PresignGetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(cfg.Bucket), + Key: aws.String(key), + }, s3.WithPresignExpires(expiry)) + if err != nil { + return "", fmt.Errorf("s3_presign_url: presign: %w", err) + } + return req.URL, nil + } + ``` + +## Ejemplo (con implementacion real) + +```go +url, err := S3PresignURL(cfg, "images/foto.png", time.Hour) +// url es valida 1 hora; el cliente puede descargar sin credenciales +fmt.Println(url) +``` + +## Notas + +Para uploads directos (PUT/POST presigned), se usaria `psClient.PresignPutObject` analogamente. Las URLs presignadas heredan los permisos de las credenciales que las generaron — no incrementan privilegios. diff --git a/functions/infra/s3_upload.go b/functions/infra/s3_upload.go new file mode 100644 index 00000000..336ccb58 --- /dev/null +++ b/functions/infra/s3_upload.go @@ -0,0 +1,20 @@ +package infra + +import ( + "fmt" + "io" +) + +// S3Upload sube data a un bucket S3-compatible bajo la key dada, con el +// Content-Type indicado. +// +// STUB: la implementacion real requiere github.com/aws/aws-sdk-go-v2 (S3 client + +// credentials provider). Ver el .md para el codigo completo a habilitar cuando se +// añada la dependencia. +func S3Upload(cfg S3Config, key string, data io.Reader, contentType string) error { + _ = cfg + _ = key + _ = data + _ = contentType + return fmt.Errorf("s3_upload: not implemented (requiere github.com/aws/aws-sdk-go-v2)") +} diff --git a/functions/infra/s3_upload.md b/functions/infra/s3_upload.md new file mode 100644 index 00000000..79eeae93 --- /dev/null +++ b/functions/infra/s3_upload.md @@ -0,0 +1,102 @@ +--- +name: s3_upload +kind: function +lang: go +domain: infra +version: "1.0.0" +purity: impure +signature: "func S3Upload(cfg S3Config, key string, data io.Reader, contentType string) error" +description: "STUB. Sube data a un bucket S3-compatible (AWS S3, MinIO, etc) bajo la key indicada con el Content-Type dado. La implementacion real requiere github.com/aws/aws-sdk-go-v2." +tags: [s3, upload, storage, cloud, stub, infra] +uses_functions: [] +uses_types: [S3Config_go_infra] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [fmt, io, time] +params: + - name: cfg + desc: "S3Config con endpoint, bucket, credenciales y region" + - name: key + desc: "key del objeto en el bucket (ej: \"images/foto.png\")" + - name: data + desc: "reader con el contenido binario a subir" + - name: contentType + desc: "MIME type del objeto (ej: \"image/png\")" +output: "nil si la subida fue exitosa, error si fallo. STUB actual retorna siempre error \"not implemented\"" +tested: false +tests: [] +test_file_path: "" +file_path: "functions/infra/s3_upload.go" +--- + +## Estado + +**Stub**. Para activar la implementacion real: + +1. Anadir dependencias: + ```bash + go get github.com/aws/aws-sdk-go-v2/aws + go get github.com/aws/aws-sdk-go-v2/config + go get github.com/aws/aws-sdk-go-v2/credentials + go get github.com/aws/aws-sdk-go-v2/service/s3 + ``` +2. Reemplazar el cuerpo del stub por: + ```go + import ( + "context" + "github.com/aws/aws-sdk-go-v2/aws" + awscfg "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + ) + + func S3Upload(cfg S3Config, key string, data io.Reader, contentType string) error { + ctx := context.Background() + awsCfg, err := awscfg.LoadDefaultConfig(ctx, + awscfg.WithRegion(cfg.Region), + awscfg.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + cfg.AccessKey, cfg.SecretKey, "")), + ) + if err != nil { + return fmt.Errorf("s3_upload: aws config: %w", err) + } + client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { + o.UsePathStyle = true + if cfg.Endpoint != "" { + scheme := "http" + if cfg.UseSSL { + scheme = "https" + } + o.BaseEndpoint = aws.String(fmt.Sprintf("%s://%s", scheme, cfg.Endpoint)) + } + }) + _, err = client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(cfg.Bucket), + Key: aws.String(key), + Body: data, + ContentType: aws.String(contentType), + }) + return err + } + ``` + +## Ejemplo (con implementacion real) + +```go +cfg := S3Config{ + Endpoint: "s3.amazonaws.com", + Bucket: "mi-bucket", + AccessKey: os.Getenv("S3_ACCESS_KEY"), + SecretKey: os.Getenv("S3_SECRET_KEY"), + Region: "us-east-1", + UseSSL: true, +} +f, _ := os.Open("./uploads/photo.png") +defer f.Close() +err := S3Upload(cfg, "images/photo.png", f, "image/png") +``` + +## Notas + +Compatible con AWS S3, MinIO, Wasabi y otros S3-compatible. El campo `UsePathStyle = true` es necesario para MinIO y para algunos endpoints custom; AWS S3 nativo soporta tanto path-style como virtual-hosted-style.