文件存储概述
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.jpg2. 安全检查
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)提供高可用和扩展性
- 统一的存储接口便于后期切换
- 图片处理可在上传时完成
- 注意文件安全验证和命名规范