关于 railway-storage
railway-storage 是使用 AWS SDK 操作 Railway S3 兼容存储的参考技能,涵盖配置、上传、文件读取和常见错误。Railway 存储桶为私有,需公开访问文件时可采用代理端点或预签名 URL。
主要功能
- 01使用 Railway 环境变量配置 S3 客户端
- 02说明延迟初始化客户端
- 03上传和读取存储桶对象
- 04通过代理端点提供私有文件
- 05介绍文件访问用的预签名 URL
- 06涵盖常见配置错误
适用场景
- 01配置应用使用 Railway 存储桶
- 02通过 API 路由提供存储文件
- 03排查上传或端点配置问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ blink-new/ claude --skill railway-storage安装后在 Claude Code 中输入/railway-storage确认已加载。
详细介绍
Railway 兼容 S3 的存储
Railway 提供兼容 S3 的存储桶,可与标准 AWS SDK 配合使用。不过,它与 AWS S3 存在一些关键差异。
重要:仅支持私有存储桶
Railway 存储桶默认是私有的,不支持公开存储桶。 ACL: "public-read" 设置会被忽略。
要公开提供文件:
- 代理端点(推荐)——通过 API 路由从 S3 获取文件并提供给客户端
- 预签名 URL ——生成有时限的签名 URL,以便直接访问
环境变量
将存储桶关联到服务时,Railway 会自动注入这些变量。请使用以下准确名称:
AWS_ENDPOINT_URL=https://storage.railway.app
AWS_DEFAULT_REGION=auto
AWS_S3_BUCKET_NAME=your-bucket-name
AWS_ACCESS_KEY_ID=tid_xxx
AWS_SECRET_ACCESS_KEY=tsec_xxx
重要: Railway 默认使用带 AWS_* 前缀的名称。不要使用 S3_* 前缀,因为这些名称与 Railway 注入的变量不匹配。
S3 客户端设置
使用延迟初始化,避免构建时出错(Docker 构建期间无法获取环境变量):
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
let s3Client: S3Client | null = null;
function getS3Client(): S3Client {
if (!s3Client) {
s3Client = new S3Client({
endpoint: process.env.AWS_ENDPOINT_URL,
region: process.env.AWS_DEFAULT_REGION ?? "auto",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
forcePathStyle: true, // Required for Railway
});
}
return s3Client;
}
要点:
- 必须设置
forcePathStyle: true - 切勿在模块级别访问
process.env - 区域通常为
"auto"
上传实现
export async function uploadToS3(key: string, body: Buffer, contentType: string): Promise<string> {
const client = getS3Client();
const bucket = process.env.S3_BUCKET_NAME!;
await client.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: body,
ContentType: contentType,
// Note: ACL is ignored - Railway buckets are always private
}));
// Return proxy URL (not direct S3 URL)
return `/uploads/${key}`;
}
代理端点模式
创建 API 路由,以提供 S3 中的文件:
// src/app/api/uploads/[...path]/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params;
const key = path.join("/");
const result = await getS3Object(key);
if (!result) {
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
return new NextResponse(result.body, {
headers: {
"Content-Type": result.contentType,
"Content-Length": result.contentLength.toString(),
"Cache-Control": "public, max-age=31536000, immutable",
},
});
}
用于从 S3 读取文件的辅助函数:
export async function getS3Object(key: string) {
const client = getS3Client();
const response = await client.send(
new GetObjectCommand({ Bucket: process.env.AWS_S3_BUCKET_NAME!, Key: key })
);
if (!response.Body) return null;
return {
body: response.Body.transformToWebStream(),
contentType: response.ContentType || "application/octet-stream",
contentLength: response.ContentLength || 0,
};
}
Next.js 重写规则
// next.config.ts
const nextConfig: NextConfig = {
async rewrites() {
return [{ source: "/uploads/:path*", destination: "/api/uploads/:path*" }];
},
};
依赖项
bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 文件上传成功但返回 403 | Railway 会忽略 ACL | 使用代理端点 |
| 构建时因缺少环境变量而失败 | S3 客户端在模块级别初始化 | 使用延迟初始化 |
| 出现“Invalid endpoint”错误 | 缺少 forcePathStyle | 添加 forcePathStyle: true |
| 上传后图片未更新 | 浏览器或 React Query 缓存 | 添加 invalidateQueries() |
URL 格式
在数据库中存储代理 URL,而非直接存储 S3 URL:
- 正确:
/uploads/{teamId}/avatar/{filename} - 错误:
https://storage.railway.app/bucket/{key}
评论
支持 Markdown