上传Object
在BOS中,用户操作的基本数据单元是Object。Bucket中的Object数量不限,但单个Object最大允许存储5TB的数据。
Object包含Key、Meta和Data。其中:
- Key是Object的名字;
- Meta是用户对该Object的描述,由一系列Name-Value对组成;
- Data是Object的数据。
BOS JavaScript SDK提供了丰富的文件上传接口,可以通过以下方式上传文件:
- 简单上传
- 追加上传
- 分块上传
- 断点续传上传
Object的命名规范如下:
- 使用UTF-8编码。
- 长度必须在1-1023字节之间。
- 首字母不能为'/',不能包含'@'字符,'@'用于图片处理接口。
简单上传
在简单上传的场景中,JS SDK 支持以数据流方式、以字符串方式、以指定文件形式(仅支持Node.js环境)、以blob对象形式(仅支持浏览器环境)执行Object上传。 分别对应 putObject 、 putObjectFromString 、putObjectFromFile 、putObjectFromBlob 方法。
-
基本流程
- 创建BosClient。
- 调用putObject()相关方法
-
示例代码
function done(response) { // 上传完成 } function fail(fail) { // 上传失败 } // 以buffer形式上传 var buffer = new Buffer('hello world'); client.putObject(bucket, object, buffer) .then(done) .catch(fail); // 以字符串形式上传 client.putObjectFromString(bucket, object, 'hello world') .then(done) .catch(fail); // 以文件形式上传,仅支持Node.js环境 client.putObjectFromFile(bucket, object, <path-to-file>) .then(done) .catch(fail); // 以blob对象形式上传,仅支持浏览器环境 client.putObjectFromBlob(bucket, object, <blob对象>) .then(done) .catch(fail);
说明:Object以文件的形式上传到BOS中,putObject函数支持不超过5GB的Object上传。在putObject请求处理成功后,BOS会在Header中返回Object的ETag作为文件标识。
追加上传
上文介绍的简单上传方式,创建的Object都是Normal类型,用户不可再进行追加写,这在日志、视频监控、视频直播等数据复写较频繁的场景中使用不方便。
正因如此,百度智能云BOS特别支持了AppendObject,即以追加写的方式上传文件。通过AppendObject操作创建的Object类型为Appendable Object,可以对该Object追加数据。AppendObject大小限制为0~5G。
let bucketName = "yourbucket";
let appendKey = "appendObjectKey";
// 首次上传时,offset设置为null
client.appendObjectFromString(bucketName,appendKey,"firstContent",null)
.then(function(response){
// 从响应头中获取offset偏移量
var offset = + response.http_headers['x-bce-next-append-offset'];
// 第二次追加上传时,指定上面获取到的offset传入
client.appendObjectFromString(bucketName, appendKey, "appendContent", offset);
});
分块上传
除了通过putObject()方法上传文件到BOS以外,BOS还提供了另外一种上传模式:分块上传(Multipart Upload)。用户可以在如下的应用场景内(但不仅限于此),使用分块上传模式,如:
- 需要支持断点上传。
- 上传超过5GB大小的文件。
- 网络条件较差,和BOS的服务器之间的连接经常断开。
- 需要流式地上传文件。
- 上传文件之前,无法确定上传文件的大小。
分块上传比直接上传稍微复杂一点,分块上传需要分为三个阶段:
- 开始上传(initiateMultipartUpload)
- 上传分块(uploadPartFromBlob)
- 上传完成(completeMultipartUpload)
浏览器端代码示例
对文件进行分块
let options = {
'Content-Type': 'application/json', // 添加http header
'Cache-Control': 'public, max-age=31536000', // 指定缓存指令
'Content-Disposition': 'attachment; filename="example.jpg"', // 指示回复的内容该以何种形式展示
'x-bce-meta-foo1': 'bar1', // 添加自定义meta信息
'x-bce-meta-foo2': 'bar2', // 添加自定义meta信息
'x-bce-meta-foo3': 'bar3' // 添加自定义meta信息
};
let PART_SIZE = 5 * 1024 * 1024; // 指定分块大小
function getTasks(file, uploadId, bucketName, key) {
let leftSize = file.size;
let offset = 0;
let partNumber = 1;
let tasks = [];
while (leftSize > 0) {
let partSize = Math.min(leftSize, PART_SIZE);
tasks.push({
file: file,
uploadId: uploadId,
bucketName: bucketName,
key: key,
partNumber: partNumber,
partSize: partSize,
start: offset,
stop: offset + partSize - 1
});
leftSize -= partSize;
offset += partSize;
partNumber += 1;
}
return tasks;
}
处理每个分块的上传逻辑
function uploadPartFile(state, client) {
return function(task, callback) {
let blob = task.file.slice(task.start, task.stop + 1);
client.uploadPartFromBlob(task.bucketName, task.key, task.uploadId, task.partNumber, task.partSize, blob)
.then(function(res) {
++state.loaded;
callback(null, res);
})
.catch(function(err) {
callback(err);
});
};
}
初始化uploadID,开始上传分块,并完成上传
let uploadId;
client.initiateMultipartUpload(bucket, key, options)
.then(function(response) {
uploadId = response.body.uploadId; // 开始上传,获取服务器生成的uploadId
let deferred = sdk.Q.defer();
let tasks = getTasks(blob, uploadId, bucket, key);
let state = {
lengthComputable: true,
loaded: 0,
total: tasks.length
};
// 为了管理分块上传,使用了async(https://github.com/caolan/async)库来进行异步处理
let THREADS = 2; // 同时上传的分块数量
async.mapLimit(tasks, THREADS, uploadPartFile(state, client), function(err, results) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(results);
}
});
return deferred.promise;
})
.then(function(allResponse) {
let partList = [];
allResponse.forEach(function(response, index) {
// 生成分块清单
partList.push({
partNumber: index + 1,
eTag: response.http_headers.etag
});
});
return client.completeMultipartUpload(bucket, key, uploadId, partList); // 完成上传
})
.then(function (res) {
// 上传完成
})
.catch(function (err) {
// 上传失败,添加您的代码
console.error(err);
});
Node.js端代码示例
对文件进行分块,并初始化UploadID,上传分块
let options = {
'Content-Type': 'application/json', // 添加http header
'Cache-Control': 'public, max-age=31536000', // 指定缓存指令
'Content-Disposition': 'attachment; filename="example.jpg"', // 指示回复的内容该以何种形式展示
'x-bce-meta-foo1': 'bar1', // 添加自定义meta信息
'x-bce-meta-foo2': 'bar2', // 添加自定义meta信息
'x-bce-meta-foo3': 'bar3' // 添加自定义meta信息
};
let PART_SIZE = 5 * 1024 * 1024; // 指定分块大小
let uploadId;
client.initiateMultipartUpload(bucket, key, options)
.then(function(response) {
uploadId = response.body.uploadId; // 开始上传,获取服务器生成的uploadId
let deferred = sdk.Q.defer();
let blob = {
// 使用fs文件库获取文件大小
size: fs.statSync(localFileName).size,
filename: localFileName
}
let tasks = getTasks(blob, uploadId, bucket, key);
let state = {
lengthComputable: true,
loaded: 0,
total: tasks.length
};
// 为了管理分块上传,使用了async(https://github.com/caolan/async)库来进行异步处理
let THREADS = 2; // 同时上传的分块数量
async.mapLimit(tasks, THREADS, uploadPartFile(state, client), function(err, results) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(results);
}
});
return deferred.promise;
})
.then(function(allResponse) {
let partList = [];
allResponse.forEach(function(response, index) {
// 生成分块清单
partList.push({
partNumber: index + 1,
eTag: response.http_headers.etag
});
});
return client.completeMultipartUpload(bucket, key, uploadId, partList); // 完成上传
})
.then(function (res) {
// 上传完成
})
.catch(function (err) {
// 上传失败,添加您的代码
console.error(err);
});
function getTasks(file, uploadId, bucketName, key) {
let leftSize = file.size;
let offset = 0;
let partNumber = 1;
let tasks = [];
while (leftSize > 0) {
let partSize = Math.min(leftSize, PART_SIZE);
tasks.push({
file: file.filename,
uploadId: uploadId,
bucketName: bucketName,
key: key,
partNumber: partNumber,
partSize: partSize,
start: offset,
stop: offset + partSize - 1
});
leftSize -= partSize;
offset += partSize;
partNumber += 1;
}
return tasks;
}
function uploadPartFile(state, client) {
return function(task, callback) {
console.log("task: ", task)
return client.uploadPartFromFile(task.bucketName, task.key, task.uploadId, task.partNumber, task.partSize, task.file , task.start)
.then(function(res) {
++state.loaded;
console.log("ok")
callback(null, res);
})
.catch(function(err) {
console.log("bad")
callback(err);
});
};
}
取消分块上传事件
用户可以使用abortMultipartUpload方法取消分块上传。
client.abortMultipartUpload(<BucketName>, <Objectkey>, <UploadID>);
获取未完成的分块上传事件
用户可以使用listMultipartUploads方法获取Bucket内未完成的分块上传事件。
client.listMultipartUploads(<bucketName>)
.then(function (response) {
// 遍历所有上传事件
for (var i = 0; i < response.body.multipartUploads.length; i++) {
console.log(response.body.multipartUploads[i].uploadId);
}
});
获取所有已上传的块信息
用户可以使用listParts方法获取某个上传事件中所有已上传的块。
client.listParts(<bucketName>, <key>, <uploadId>)
.then(function (response) {
// 遍历所有上传事件
for (var i = 0; i < response.body.parts.length; i++) {
console.log(response.body.parts[i].partNumber);
}
});
断点续传上传
当用户向BOS上传大文件时,如果网络不稳定或者遇到程序崩等情况,则整个上传就失败了,失败前已经上传的部分也作废,用户不得不重头再来。这样做不仅浪费资源,在网络不稳定的情况下,往往重试多次还是无法完成上传。基于上述场景,BOS提供了断点续传上传的能力,主要利用了分块上传的能力,将待上传的文件拆分成多个分块(Part),然后分别上传这些分块,当所有分块全部上传完成后,BOS将请求者上传的所有分块组合成完整的Object。
putSuperObject
1.0.1-beta.2
及以上版本支持
JavaScript SDK提供了putSuperObject
方法,对分块上传相关的API做了高级封装,支持分上传任务暂停、恢复、取消、分片并发数设置,失败重试等功能。
请求头
除公共头域外,无其他特殊请求头
初始化参数
参数名称 | 说明 | 类型 | 必填 | 默认值 | 示例值 |
---|---|---|---|---|---|
bucketName | 存储桶名称 | string |
是 | - | "bucket001" |
objectName | 上传后对象名称 | string |
是 | - | "file001" |
data | 上传数据, 类型为string时表示文件路径,还支持Buffer和Blob对象 | string | Buffer | Blob |
是 | - | - |
StorageClass | 文件存储类型 | "STANDARD" | "STANDARD_IA" | "COLD" | "ARCHIVE" | "MAZ_STANDARD" | "MAZ_STANDARD_IA" |
否 | "STANDARD" |
"STANDARD" |
chunkSize | 上传文件分片默认体积, 单位为bytes | number |
否 | 5 * 1024 ** 2(5MB) | 1048576 |
partConcurrency | 分片并发数量 | number | 否 | 5 | 5 |
ContentLength | 文件大小,单位为bytes,非必填,内部会基于自动计算 | string |
否 | - | 1048576 |
ContentType | 文件媒体类型,非必填,内部会基于objectName字段自动生成 | string |
否 | - | "application/x-gtar" |
uploadId | 分片上传任务ID,非必填,当需要从断点恢复任务时,需要传入该参数 | string |
否 | - | "a44cc9bab11cbd156984767aad637851" |
onProgress | 上传进度回调函数 | ProgressCallback |
否 | - | - |
onStateChange | 任务状态变化回调函数 | StateChangeCallback |
否 | - | - |
ProgressCallback
type ProgressCallback = (
/* 当前上传速度 */
speed: string,
/* 上传进度,保留4位小数 */
progress: number,
/* 上传进度-百分比 */
percent: string,
/* 已上传字节数 */
uploadedBytes: number,
/* 文件总字节数 */
totalBytes
) => void
StateChangeCallback
type StateChangeCallback = (
/* 状态 */
state: string,
/* 如果上传失败会传递失败的message,上传成功会返回上传后的地址 */
options: {
message: string;
data: Record<string, any> | null
}
) => void;
状态
"inited"
:任务初始化完成"running"
:任务队列运行中"paused"
:任务队列暂停"completed"
:任务完成,上传结束"cancelled"
:任务取消,上传结束"failed"
:任务异常,上传结束
Node.js端代码示例
const sdk = require('@baiducloud/sdk');
const client = new sdk.BosClient({
endpoint: 'http://bj.bcebos.com',
credentials: {
ak: "<Your Access Key>",
sk: "<Your Secret Key>"
}
});
// 存储桶名称
const bucketName = "<Your Bucket Name>";
// 上传后文件名称
const objectName = 'demo.tgz';
// 本地文件路径
const data = '/Mock/path/to/local/file/demo.tgz';
// 初始化上传任务
const SuperUploadTask = client.putSuperObject({
// 桶名称
bucketName,
// 上传后对象名称
objectName,
// 上传数据, 类型为string时表示文件路径
data,
// 分片并发数
partConcurrency: 2,
// 上传进度回调函数
onProgress: (options) => {
const {speed, progress, percent, uploadedBytes, totalBytes} = options;
console.log(options);
},
// 状态变化回调函数
onStateChange: (state, data) => {
if (state === 'completed') {
console.log('上传成功');
} else if (state === 'failed') {
console.error('上传失败,失败原因:' + data.message);
} else if (state === 'cancelled') {
console.log('上传任务取消');
} else if (state === 'inited') {
console.log('上传任务初始化完成');
} else if (state === 'running') {
console.log('上传任务开始运行...');
} else if (state === 'paused') {
console.log('上传任务已暂停');
}
}
});
// 启动上传任务
const tasks = SuperUploadTask.start();
console.log('切分任务: ', tasks);
// 暂停上传任务
setTimeout(() => {
SuperUploadTask.pause();
}, 5000);
// 恢复上传任务
setTimeout(() => {
SuperUploadTask.resume();
}, 15000);
// 取消上传任务
setTimeout(async () => {
const result = SuperUploadTask.cancel();
console.log(result ? '任务取消成功' : '任务取消失败');
}, 25000);
浏览器端代码示例
<html>
<head>
<meta charset="utf-8" />
<title>SuperUpload测试</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.2.2/sdk.min.css" integrity="sha512-9yikVhRqNeq1rypIzAFKR8CA2uG8V5gYppoDKK4xx+7eoLXJsEm+f9QN0++xqHvrOxxvb93uiipgYk9uEW7RlA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/amis/6.2.2/cxd.min.css" integrity="sha512-9yikVhRqNeq1rypIzAFKR8CA2uG8V5gYppoDKK4xx+7eoLXJsEm+f9QN0++xqHvrOxxvb93uiipgYk9uEW7RlA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-notify@1.0.4/dist/simple-notify.css" />
</head>
<body>
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/simple-notify@1.0.4/dist/simple-notify.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/amis/6.2.2/sdk.min.js" integrity="sha512-BpMIHWCtAUDARuH/qnGH6eBxoOv4l0i4Y9/9f6vnGVDNnPqbWCZoPgk3/KvY6NuOb6cAPFmqVN1R2sXz7Z3VcQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://bce.bdstatic.com/lib/@baiducloud/sdk/1.0.1-beta.2/baidubce-sdk.bundle.min.js" ></script>
<script>
(async function () {
// 实例化BOS SDK
const client = new window.baidubce.sdk.BosClient({
endpoint: 'http://bj.bcebos.com',
credentials: {
ak: "<Your Access Key>",
sk: "<Your Secret Key>"
}
});
// 实例化BOS SDK
const bucketName = '<Your Bucket Name>';
// 上传任务实例
let SuperUpload;
// 进度信息
let p1 = 0;
// 消息弹窗
function pushNotify({status, title, text}) {
new Notify({
status: status || 'success',
title: title,
text: text,
distance: 200,
position: 'x-center'
});
}
// 获取任务实例
function getInstance() {
if (SuperUpload) {
pushNotify({title: '获取实例信息', text: '实例信息获取成功'});
} else {
pushNotify({status: 'error', title: '获取实例信息', text: '实例信息获取失败'});
}
}
async function start() {
const files = $('#file')?.[0]?.files;
if (!files || files.length === 0) {
pushNotify({status: 'error', title: '上传', text: '请先选择文件'});
return;
}
const file = files[0];
let reader = new FileReader();
let rs = reader.readAsArrayBuffer(file);
let blob = null;
// 将文件读取为Blob对象
reader.onload = async e => {
if (typeof e.target.result === 'object') {
blob = new Blob([e.target.result]);
} else {
blob = e.target.result;
}
SuperUpload = client.putSuperObject({
bucketName,
objectName: file.name,
ContentLength: file.size,
ContentType: file.type,
data: blob,
partConcurrency: 2,
onProgress: options => {
const {speed, progress, uploadedBytes, totalBytes} = options;
console.log(options);
amisScoped.updateProps({
data: {p1: progress * 100}
});
},
onStateChange: (state, data) => {
if (state === 'completed') {
pushNotify({title: '上传', text: '上传成功'});
console.log(data);
} else if (state === 'failed') {
pushNotify({status: 'error', title: '上传失败', text: data.message});
} else if (state === 'cancelled') {
pushNotify({title: '上传', text: '上传任务取消'});
} else if (state === 'inited') {
pushNotify({title: '上传', text: '上传任务初始化完成'});
} else if (state === 'running') {
pushNotify({title: '上传', text: '上传任务开始运行...'});
} else if (state === 'paused') {
pushNotify({title: '上传', text: '上传任务已暂停'});
}
}
});
const tasks = await SuperUpload.start();
console.log('切片列表: ', tasks);
};
}
function pause() {
if (SuperUpload) {
SuperUpload.pause();
} else {
pushNotify({status: 'error', title: '暂停', text: '实例不存在'});
}
}
function resume() {
if (SuperUpload) {
SuperUpload.resume();
} else {
pushNotify({status: 'error', title: '恢复', text: '实例不存在'});
}
}
async function cancel() {
if (SuperUpload) {
const result = await SuperUpload.cancel();
if (result) {
SuperUpload = undefined;
p1 = 0;
amisScoped.updateProps({
data: {p1: 0}
});
}
} else {
pushNotify({status: 'error', title: '取消任务', text: '实例不存在'});
}
}
const amis = amisRequire('amis/embed');
const amisJSON = {
type: 'page',
body: [
{
type: 'alert',
title: 'CORS配置',
body: '如果接口报CORS错误,请开启对应存储桶的<a href="https://cloud.baidu.com/doc/BOS/s/Dk6kqw1g8", target="__blank">CORS配置</a>',
level: 'warning',
showIcon: true,
className: 'mb-3'
},
{
type: 'custom',
name: 'file',
onMount: (dom, value, onChange, props) => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('id', 'file');
dom.appendChild(input);
}
},
{
type: 'progress',
value: '${p1}',
style: {
marginTop: '20px',
width: '500px'
}
},
{
type: 'flex',
justify: 'flex-start',
style: {
marginTop: '20px'
},
items: [
{
type: 'button',
level: 'primary',
label: '获取实例',
style: {
marginRight: '10px'
},
onClick: getInstance
},
{
type: 'button',
level: 'primary',
label: '上传',
style: {
marginRight: '10px'
},
onClick: start
},
{
type: 'button',
level: 'primary',
label: '暂停',
style: {
marginRight: '10px'
},
onClick: pause
},
{
type: 'button',
level: 'primary',
label: '恢复',
style: {
marginRight: '10px'
},
onClick: resume
},
{
type: 'button',
level: 'danger',
label: '取消',
onClick: cancel
}
]
}
]
};
const amisScoped = amis.embed('#root', amisJSON, {data: {p1}});
})()
</script>
</body>
</html>
设定Object的Http Header和自定义Meta数据
SDK本质上是调用后台的HTTP接口,因此BOS服务允许用户自定义Http Header。同时也允许用户对要上传的Object添加自定义Meta信息。以putObjectFromFile()函数为例,可以用以下代码来处理:
-
示例代码
let options = { 'Content-Type': 'application/json', // 添加http header 'Cache-Control': 'public, max-age=31536000', // 指定缓存指令 'Content-Disposition': 'attachment; filename="example.jpg"', // 指示回复的内容该以何种形式展示 'x-bce-meta-foo1': 'bar1', // 添加自定义meta信息 'x-bce-meta-foo2': 'bar2', // 添加自定义meta信息 'x-bce-meta-foo3': 'bar3' // 添加自定义meta信息 } client.putObjectFromFile(bucket, object, <path-to-file>, options) .then(done) .catch(fail);
注意:自定义Meta信息的key需要以
x-bce-meta-
开头。
获取上传进度
JavaScript SDK支持在上传过程中实时提供上传进度信息,可通过监听process事件获取相关信息,所有上传相关接口均支持该功能。
-
示例代码:以putObjectFromBlob接口为例
// 以blob对象形式上传,仅支持浏览器环境 client.putObjectFromBlob(bucket, object, <blob对象>) .then(done) .catch(fail); client.on('progress', function() { // do something })
同步回调
JavaScript SDK支持BOS服务端同步回调接口,通过请求头中添加x-bce-process
或者在url的query中添加x-bce-process
,指定回调的服务器地址和相关参数配置,就可以在上传完成后,让BOS服务端主动调用回调接口来达到通知客户目的。
目前仅支持普通上传(PutObject)和 完成分片上传(CompleteMultipartUpload)
- 方法一:
使用
callback
参数,SDK会帮助你处理参数并添加到请求头中
try {
const res = await client.putObjectFromString('bucketName', 'fileName', 'demo-string', {
callback: {
urls: ["https://www.test.com/callback"],
vars: {name: 'baidu'},
encrypt: 'config',
key: 'callback1'
}
});
/* callback result */
console.log(res.body.callback.result);
} catch (e) {
/* callback error code */
console.error(res.body.callback.code);
/* callback error message */
console.error(res.body.callback.message);
}
- 方法二:
自行处理参数,将
"x-bce-process"
参数和值添加到请求头中
try {
const res = await client.putObjectFromString('bucketName', 'fileName', 'demo-string', {
'x-bce-process': 'callback/callback,u_WyJodHRwczovL3d3dy50ZXN0LmNvbS9jYWxsYmFjayJd,m_sync,v_eyJuYW1lIjoiYmFpZHUifQ'
});
/* callback result */
console.log(res.body.callback.result);
} catch (e) {
/* callback error code */
console.error(res.body.callback.code);
/* callback error message */
console.error(res.body.callback.message);
}