mcpField
R
railway-storage

railway-storage

github.com/blink-new云与 DevOps50 次安装
代码仓库

关于 railway-storage

railway-storage 是使用 AWS SDK 操作 Railway S3 兼容存储的参考技能,涵盖配置、上传、文件读取和常见错误。Railway 存储桶为私有,需公开访问文件时可采用代理端点或预签名 URL。

主要功能

  1. 01使用 Railway 环境变量配置 S3 客户端
  2. 02说明延迟初始化客户端
  3. 03上传和读取存储桶对象
  4. 04通过代理端点提供私有文件
  5. 05介绍文件访问用的预签名 URL
  6. 06涵盖常见配置错误

适用场景

  1. 01配置应用使用 Railway 存储桶
  2. 02通过 API 路由提供存储文件
  3. 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" 设置会被忽略。

要公开提供文件:

  1. 代理端点(推荐)——通过 API 路由从 S3 获取文件并提供给客户端
  2. 预签名 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

常见问题

问题原因解决方案
文件上传成功但返回 403Railway 会忽略 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