跳转到主要内容

云存储

直接将文件上传到云存储提供商,无需通过服务器代理。

Amazon S3

安装

npm install @samithahansaka/dropup

基本用法

import { useDropup } from '@samithahansaka/dropup';
import { createS3Uploader } from '@samithahansaka/dropup/cloud/s3';

function S3Uploader() {
const { files, actions, getDropProps, getInputProps } = useDropup({
upload: createS3Uploader({
getPresignedUrl: async (file) => {
// 调用后端获取预签名 URL
const response = await fetch('/api/s3/presign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
return response.json();
},
}),

onUploadComplete: (file) => {
console.log('已上传到 S3:', file.uploadedUrl);
},
});

return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>拖放文件上传到 S3</p>
</div>
);
}

后端:生成预签名 URL

// Node.js / Express 示例
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

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

app.post('/api/s3/presign', async (req, res) => {
const { filename, contentType } = req.body;
const key = `uploads/${Date.now()}-${filename}`;

const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
ContentType: contentType,
});

const url = await getSignedUrl(s3, command, { expiresIn: 3600 });

res.json({
url,
fields: {}, // 对于简单的 PUT,不需要额外字段
});
});

S3 使用 POST(多部分表单)

对于 S3 POST 策略:

createS3Uploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/s3/presign-post', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});

const { url, fields } = await response.json();

return {
url, // S3 存储桶 URL
fields, // 包含在表单中的策略字段
};
},
});

Google Cloud Storage

import { createGCSUploader } from '@samithahansaka/dropup/cloud/gcs';

function GCSUploader() {
const { files, getDropProps, getInputProps } = useDropup({
upload: createGCSUploader({
getSignedUrl: async (file) => {
const response = await fetch('/api/gcs/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
return response.json();
},
}),
});

return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>上传到 Google Cloud Storage</p>
</div>
);
}

后端:GCS 签名 URL

// Node.js 示例
import { Storage } from '@google-cloud/storage';

const storage = new Storage();
const bucket = storage.bucket(process.env.GCS_BUCKET);

app.post('/api/gcs/sign', async (req, res) => {
const { filename, contentType } = req.body;
const blob = bucket.file(`uploads/${Date.now()}-${filename}`);

const [url] = await blob.getSignedUrl({
version: 'v4',
action: 'write',
expires: Date.now() + 15 * 60 * 1000, // 15 分钟
contentType,
});

res.json({ url });
});

Azure Blob Storage

import { createAzureUploader } from '@samithahansaka/dropup/cloud/azure';

function AzureUploader() {
const { files, getDropProps, getInputProps } = useDropup({
upload: createAzureUploader({
getSasUrl: async (file) => {
const response = await fetch('/api/azure/sas', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
return response.json();
},
}),
});

return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>上传到 Azure Blob Storage</p>
</div>
);
}

后端:Azure SAS URL

// Node.js 示例
import {
BlobServiceClient,
generateBlobSASQueryParameters,
BlobSASPermissions,
} from '@azure/storage-blob';

const blobServiceClient = BlobServiceClient.fromConnectionString(
process.env.AZURE_STORAGE_CONNECTION_STRING
);

app.post('/api/azure/sas', async (req, res) => {
const { filename, contentType } = req.body;
const containerClient = blobServiceClient.getContainerClient('uploads');
const blobName = `${Date.now()}-${filename}`;
const blobClient = containerClient.getBlockBlobClient(blobName);

const sasToken = generateBlobSASQueryParameters(
{
containerName: 'uploads',
blobName,
permissions: BlobSASPermissions.parse('cw'), // 创建、写入
expiresOn: new Date(Date.now() + 15 * 60 * 1000),
},
blobServiceClient.credential
).toString();

res.json({
url: `${blobClient.url}?${sasToken}`,
headers: {
'x-ms-blob-type': 'BlockBlob',
'Content-Type': contentType,
},
});
});

Cloudflare R2

R2 兼容 S3,因此使用 S3 上传器:

import { createS3Uploader } from '@samithahansaka/dropup/cloud/s3';

function R2Uploader() {
const { files, getDropProps, getInputProps } = useDropup({
upload: createS3Uploader({
getPresignedUrl: async (file) => {
const response = await fetch('/api/r2/presign', {
method: 'POST',
body: JSON.stringify({ filename: file.name }),
});
return response.json();
},
}),
});

return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>上传到 Cloudflare R2</p>
</div>
);
}

后端:R2 预签名 URL

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
region: 'auto',
endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
},
});

app.post('/api/r2/presign', async (req, res) => {
const { filename } = req.body;

const command = new PutObjectCommand({
Bucket: process.env.R2_BUCKET,
Key: `uploads/${Date.now()}-${filename}`,
});

const url = await getSignedUrl(s3, command, { expiresIn: 3600 });
res.json({ url });
});

DigitalOcean Spaces

同样兼容 S3:

// 与 S3 相同,只需更新后端端点配置
const s3 = new S3Client({
region: 'nyc3',
endpoint: 'https://nyc3.digitaloceanspaces.com',
credentials: {
accessKeyId: process.env.DO_SPACES_KEY,
secretAccessKey: process.env.DO_SPACES_SECRET,
},
});

自定义云服务提供商

为任何云服务创建自己的上传器:

import { useDropup, type CustomUploader } from '@samithahansaka/dropup';

const customCloudUploader: CustomUploader = async (file, options) => {
// 1. 从后端获取上传 URL
const { uploadUrl, fileUrl } = await fetch('/api/custom-cloud/init', {
method: 'POST',
body: JSON.stringify({ filename: file.name, size: file.size }),
}).then(r => r.json());

// 2. 上传文件
const xhr = new XMLHttpRequest();

return new Promise((resolve, reject) => {
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
options.onProgress((e.loaded / e.total) * 100);
}
};

xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve({ url: fileUrl });
} else {
reject(new Error('上传失败'));
}
};

xhr.onerror = () => reject(new Error('网络错误'));

// 处理取消
options.signal.addEventListener('abort', () => xhr.abort());

xhr.open('PUT', uploadUrl);
xhr.send(file.file);
});
};

function CustomCloudUploader() {
const { files, getDropProps, getInputProps } = useDropup({
upload: customCloudUploader,
});

return (
<div {...getDropProps()}>
<input {...getInputProps()} />
<p>上传到自定义云</p>
</div>
);
}

安全最佳实践

  1. 永远不要在客户端暴露凭证 - 始终在后端生成签名 URL
  2. 使用较短的过期时间 - 通常 5-15 分钟就足够了
  3. 在后端验证文件类型 - 不要仅依赖客户端验证
  4. 在云存储上设置适当的 CORS 策略
  5. 在预签名 URL 策略中限制文件大小
  6. 使用单独的存储桶 用于用户上传与应用程序资源