零度AI
高级16 分钟阅读

Claude Code 文件存储

使用 Claude Code 实现本地文件存储、云存储(OSS/S3)集成、文件上传下载

Claude Code文件存储OSSS3上传下载

文件存储概述

Claude Code 可以帮助你实现各种文件存储场景,从简单的本地存储到云服务集成。

存储选择

小型项目可用本地存储,需要高可用和 CDN 加速时建议使用云存储(OSS/S3)。

本地文件存储

文件上传服务

bash
帮我创建一个本地文件上传服务,包含:
- 单文件上传
- 多文件上传
- 文件类型验证
- 文件大小限制
- 生成唯一文件名
javascript
// lib/storage/local.js
import fs from "fs";
import path from "path";
import { randomUUID } from "crypto";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// 配置
const UPLOAD_DIR = path.join(__dirname, "../../public/uploads");
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const ALLOWED_TYPES = [
  "image/jpeg",
  "image/png",
  "image/gif",
  "image/webp",
  "application/pdf",
  "video/mp4",
];

// 确保上传目录存在
if (!fs.existsSync(UPLOAD_DIR)) {
  fs.mkdirSync(UPLOAD_DIR, { recursive: true });
}

export function validateFile(file) {
  const errors = [];

  // 检查文件大小
  if (file.size > MAX_FILE_SIZE) {
    errors.push(`文件大小超过限制(最大 ${MAX_FILE_SIZE / 1024 / 1024}MB)`);
  }

  // 检查文件类型
  if (!ALLOWED_TYPES.includes(file.mimetype)) {
    errors.push(`不支持的文件类型:${file.mimetype}`);
  }

  return {
    valid: errors.length === 0,
    errors,
  };
}

export async function uploadFile(file, subDir = "") {
  // 验证文件
  const validation = validateFile(file);
  if (!validation.valid) {
    throw new Error(validation.errors.join(", "));
  }

  // 生成唯一文件名
  const ext = path.extname(file.name);
  const filename = `${randomUUID()}${ext}`;

  // 目标目录
  const targetDir = subDir
    ? path.join(UPLOAD_DIR, subDir)
    : UPLOAD_DIR;

  if (!fs.existsSync(targetDir)) {
    fs.mkdirSync(targetDir, { recursive: true });
  }

  // 保存文件
  const targetPath = path.join(targetDir, filename);

  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(file.path);
    const writeStream = fs.createWriteStream(targetPath);

    readStream.on("error", reject);
    writeStream.on("error", reject);
    writeStream.on("finish", () => {
      resolve({
        filename,
        originalName: file.name,
        size: file.size,
        mimetype: file.mimetype,
        url: `/uploads/${subDir ? subDir + "/" : ""}${filename}`,
      });
    });

    readStream.pipe(writeStream);
  });
}

export async function uploadMultiple(files) {
  const results = await Promise.all(
    files.map((file) => uploadFile(file))
  );
  return results;
}

export async function deleteFile(filePath) {
  const fullPath = path.join(UPLOAD_DIR, filePath);

  return new Promise((resolve, reject) => {
    fs.unlink(fullPath, (err) => {
      if (err && err.code !== "ENOENT") {
        reject(err);
      } else {
        resolve(true);
      }
    });
  });
}

export function getFilePath(filename, subDir = "") {
  return path.join(UPLOAD_DIR, subDir, filename);
}

Express 文件上传路由

javascript
// routes/upload.js
import express from "express";
import multer from "multer";
import { uploadFile, uploadMultiple, deleteFile } from "../lib/storage/local.js";

const router = express.Router();

// 配置 multer
const upload = multer({
  dest: "/tmp/uploads",
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
  },
});

// 单文件上传
router.post("/upload", upload.single("file"), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: "请选择要上传的文件" });
    }

    const result = await uploadFile(req.file, req.body.subDir || "general");
    res.json({
      success: true,
      data: result,
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message,
    });
  }
});

// 多文件上传
router.post("/upload/multiple", upload.array("files", 10), async (req, res) => {
  try {
    if (!req.files || req.files.length === 0) {
      return res.status(400).json({ error: "请选择要上传的文件" });
    }

    const results = await uploadMultiple(req.files);
    res.json({
      success: true,
      data: results,
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message,
    });
  }
});

// 删除文件
router.delete("/:filename", async (req, res) => {
  try {
    const { filename } = req.params;
    await deleteFile(filename);
    res.json({
      success: true,
      message: "文件删除成功",
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message,
    });
  }
});

export default router;

云存储集成

阿里云 OSS

bash
帮我创建一个阿里云 OSS 文件存储模块。
javascript
// lib/storage/oss.js
import OSS from "ali-oss";
import { randomUUID } from "crypto";

// 创建 OSS 客户端
const client = new OSS({
  region: process.env.OSS_REGION,
  accessKeyId: process.env.OSS_ACCESS_KEY_ID,
  accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
  bucket: process.env.OSS_BUCKET,
  cname: process.env.OSS_CNAME, // 自定义域名
});

// 上传文件
export async function uploadToOSS(fileBuffer, filename, options = {}) {
  const ext = filename.split(".").pop();
  const uniqueName = `${randomUUID()}.${ext}`;
  const objectName = options.path
    ? `${options.path}/${uniqueName}`
    : uniqueName;

  try {
    const result = await client.put(objectName, fileBuffer, {
      meta: {
        uid: options.userId || "",
        type: options.mimetype || "application/octet-stream",
      },
    });

    return {
      filename: uniqueName,
      objectName,
      url: result.url,
      size: fileBuffer.length,
    };
  } catch (error) {
    console.error("OSS upload error:", error);
    throw new Error("文件上传失败");
  }
}

// 上传 Base64 图片
export async function uploadBase64ToOSS(base64Data, options = {}) {
  const matches = base64Data.match(/^data:image\/(\w+);base64,(.+)$/);

  if (!matches) {
    throw new Error("无效的 Base64 图片数据");
  }

  const ext = matches[1];
  const data = Buffer.from(matches[2], "base64");
  const filename = `${randomUUID()}.${ext}`;

  return uploadToOSS(data, filename, options);
}

// 删除文件
export async function deleteFromOSS(objectName) {
  try {
    await client.delete(objectName);
    return true;
  } catch (error) {
    console.error("OSS delete error:", error);
    return false;
  }
}

// 生成签名 URL(私有 bucket)
export async function getSignedUrl(objectName, expires = 3600) {
  try {
    const url = await client.signatureUrl(objectName, {
      expires,
    });
    return url;
  } catch (error) {
    console.error("OSS signature error:", error);
    throw new Error("生成签名 URL 失败");
  }
}

// 复制文件
export async function copyObject(source, target) {
  try {
    await client.copy(target, source);
    return true;
  } catch (error) {
    console.error("OSS copy error:", error);
    return false;
  }
}

// 列出文件
export async function listObjects(prefix = "", maxKeys = 100) {
  try {
    const result = await client.list({
      prefix,
      "max-keys": maxKeys,
    });

    return result.objects.map((obj) => ({
      name: obj.name,
      size: obj.size,
      lastModified: obj.lastModified,
      url: obj.url,
    }));
  } catch (error) {
    console.error("OSS list error:", error);
    return [];
  }
}

AWS S3

javascript
// lib/storage/s3.js
import AWS from "aws-sdk";
import { randomUUID } from "crypto";
import fs from "fs";

const s3 = new AWS.S3({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION,
});

// 上传文件
export async function uploadToS3(file, options = {}) {
  const ext = file.originalname.split(".").pop();
  const uniqueName = `${randomUUID()}.${ext}`;
  const key = options.path ? `${options.path}/${uniqueName}` : uniqueName;

  const params = {
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Body: fs.createReadStream(file.path),
    ContentType: file.mimetype,
    ACL: options.public ? "public-read" : "private",
    Metadata: options.meta || {},
  };

  try {
    const result = await s3.upload(params).promise();

    return {
      filename: uniqueName,
      key,
      url: result.Location,
      size: file.size,
    };
  } catch (error) {
    console.error("S3 upload error:", error);
    throw new Error("文件上传失败");
  }
}

// 删除文件
export async function deleteFromS3(key) {
  try {
    await s3
      .deleteObject({
        Bucket: process.env.S3_BUCKET,
        Key: key,
      })
      .promise();
    return true;
  } catch (error) {
    console.error("S3 delete error:", error);
    return false;
  }
}

// 生成签名 URL
export function getSignedUrl(key, expires = 3600) {
  return s3.getSignedUrl("getObject", {
    Bucket: process.env.S3_BUCKET,
    Key: key,
    Expires: expires,
  });
}

统一存储接口

存储适配器

javascript
// lib/storage/index.js
import * as localStorage from "./local.js";
import * as ossStorage from "./oss.js";
import * as s3Storage from "./s3.js";

// 根据配置选择存储方式
const STORAGE_TYPE = process.env.STORAGE_TYPE || "local";

let storage;

switch (STORAGE_TYPE) {
  case "oss":
    storage = ossStorage;
    break;
  case "s3":
    storage = s3Storage;
    break;
  default:
    storage = localStorage;
}

// 统一接口
export const storageService = {
  // 上传文件
  async upload(file, options = {}) {
    return storage.upload(file, options);
  },

  // 上传 Base64
  async uploadBase64(base64, options = {}) {
    if (STORAGE_TYPE === "oss") {
      return ossStorage.uploadBase64ToOSS(base64, options);
    }
    throw new Error("该存储类型不支持 Base64 上传");
  },

  // 删除文件
  async delete(filename) {
    return storage.delete(filename);
  },

  // 获取文件 URL 或签名
  async getUrl(filename, options = {}) {
    if (STORAGE_TYPE === "local") {
      return `/uploads/${filename}`;
    }

    if (STORAGE_TYPE === "oss" || STORAGE_TYPE === "s3") {
      return storage.getSignedUrl
        ? storage.getSignedUrl(filename, options.expires)
        : storage.getUrl(filename);
    }

    return filename;
  },

  // 批量删除
  async deleteMultiple(filenames) {
    const results = await Promise.all(
      filenames.map((filename) => this.delete(filename))
    );
    return results.every((r) => r);
  },
};

export default storageService;

图片处理

图片缩放和优化

javascript
// lib/storage/image.js
import sharp from "sharp";
import { uploadToOSS } from "./oss.js";
import { uploadToS3 } from "./s3.js";

const STORAGE_TYPE = process.env.STORAGE_TYPE || "local";

export async function processAndUpload(imageBuffer, filename, options = {}) {
  const {
    width,
    height,
    quality = 80,
    format = "jpeg",
    path = "images",
  } = options;

  let processedImage = sharp(imageBuffer);

  // 调整尺寸
  if (width || height) {
    processedImage = processedImage.resize(width, height, {
      fit: "inside",
      withoutEnlargement: true,
    });
  }

  // 转换格式
  if (format === "webp") {
    processedImage = processedImage.webp({ quality });
  } else if (format === "png") {
    processedImage = processedImage.png({ quality });
  } else {
    processedImage = processedImagejpeg({ quality });
  }

  const processedBuffer = await processedImage.toBuffer();
  const ext = format === "webp" ? "webp" : "jpg";
  const newFilename = filename.replace(/\.\w+$/, `.${ext}`);

  // 上传
  if (STORAGE_TYPE === "oss") {
    return uploadToOSS(processedBuffer, newFilename, { path });
  } else if (STORAGE_TYPE === "s3") {
    return uploadToS3(
      { path: "/tmp/processed", originalname: newFilename, mimetype: `image/${format}` },
      { path }
    );
  }

  return {
    filename: newFilename,
    size: processedBuffer.length,
    url: `/uploads/${path}/${newFilename}`,
  };
}

// 生成缩略图
export async function generateThumbnail(imageBuffer, size = 200) {
  const thumbnail = await sharp(imageBuffer)
    .resize(size, size, {
      fit: "cover",
      position: "center",
    })
    .jpeg({ quality: 70 })
    .toBuffer();

  return thumbnail;
}

最佳实践

1. 文件命名规范

javascript
// 好的文件命名
const generateFileName = (originalName) => {
  const ext = path.extname(originalName);
  const uuid = randomUUID();
  const timestamp = Date.now();
  return `${timestamp}-${uuid}${ext}`;
};

// 输出示例: 1712233445566-a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg

2. 安全检查

javascript
// lib/storage/security.js
export function validateFileName(filename) {
  // 禁止特殊字符
  const forbiddenChars = /[<>:"|?*\x00-\x1f]/;
  if (forbiddenChars.test(filename)) {
    return { valid: false, error: "文件名包含非法字符" };
  }

  // 防止路径穿越
  if (filename.includes("..") || filename.includes("/") || filename.includes("\\")) {
    return { valid: false, error: "文件名路径穿越" };
  }

  // 限制文件名长度
  if (filename.length > 255) {
    return { valid: false, error: "文件名过长" };
  }

  return { valid: true };
}

export function sanitizeFileName(filename) {
  return filename.replace(/[<>:"|?*\x00-\x1f]/g, "_");
}

3. 上传进度追踪

javascript
// routes/upload-progress.js
export function uploadProgressHandler(req, res) {
  const progress = {};

  req.on("file", (fieldname, file) => {
    file.on("progress", (data) => {
      progress[fieldname] = {
        bytesUploaded: data.bytesUploaded,
        bytesExpected: data.bytesExpected,
        percentage: Math.round((data.bytesUploaded / data.bytesExpected) * 100),
      };

      // 可以通过 WebSocket 发送到客户端
      io.emit("upload:progress", progress);
    });
  });
}

存储选择建议

个人项目用本地存储,中小型项目用 OSS/S3,需要 CDN 加速时使用云存储配合 CDN。

总结

使用 Claude Code 实现文件存储功能:

  • 本地存储适合简单场景,配置简单
  • 云存储(OSS/S3)提供高可用和扩展性
  • 统一的存储接口便于后期切换
  • 图片处理可在上传时完成
  • 注意文件安全验证和命名规范