文件上传是常见需求,一般不会直接把文件保存在服务器的某个目录下,因为服务器的存储容量是有限的,这样不好扩展。
会用 OSS (Object Storage Service)对象存储服务来存文件,它是支持分布式扩展的,不用担心存储容量问题,而且也好管理。
比如阿里云的 OSS 服务。
但是有一些业务场景下,数据需要保密,要求私有部署,也就是要在自己的机房里部署一套 OSS 服务。
这时候怎么办呢?
这种需求一般会用 minio 来做。
它可以实现和阿里云 OSS 一样的功能。
首先,用一下阿里云的 OSS 服务。
OSS 里的文件是放在一个个 Bucket(桶)里的:
创建个 Bucket:
然后进入文件列表,就可以上传文件了:
因为创建的 Bucket 设置了公共读,所以可以直接访问:
此外,阿里云 OSS 还可以通过 SDK 来上传文件。
创建个项目:
mkdir minio-test
cd minio-test
npm init -y进入项目,安装 ali-oss:
npm install ali-oss创建 index.js
const OSS = require('ali-oss')
const client = new OSS({
region: 'oss-cn-beijing',
bucket: 'guang-666',
accessKeyId: '',
accessKeySecret: '',
});
async function put () {
try {
const result = await client.put('smile.png', './smile.png');
console.log(result);
} catch (e) {
console.log(e);
}
}
put();填入 region、bucket 和 accessKeyId、accessKeySecret
这里的 region 可以从概览里看到:
accessKey 是在这里看:
具体创建 accessKey 的流程看之前 OSS 那节
然后跑一下:
上传成功之后就可以通过 OSS 服务访问这个图片了:
也可以通过 sdk 下载图片:
执行后可以看到,图片被下载下来了:
这就是阿里云 OSS 的用法。
那用 minio 自己搭呢?
首先,需要安装 docker 桌面端:
打开后可以看到本地的所有镜像和容器:
搜索下 minio(这步需要科学上网):
填入一些信息:
name 是容器名。
port 是映射本地 9000 和 9001 端口到容器内的端口。
volume 是挂载本地目录到容器内的目录
这里挂载了一个本地一个目录到容器内的数据目录 /bitnami/minio/data,这样容器里的各种数据都保存在本地了。
还要指定两个环境变量,MINIO_ROOT_USER 和 MINIO_ROOT_PASSWORD,是用来登录的。
点击 run,跑起来之后可以看到数据目录被标记为 mounted,端口也映射成功了:
输入刚才环境变量填的用户名密码:
进入管理界面:
这个 bucket 就是管理桶的地方,而 object browser 就是管理文件列表的地方。
和阿里云 OSS 用法一样。
创建个 bucket:
然后在这个 bucket 下上传一个文件:
点击 share 就可以看到这个文件的 url:
现在倒是能在浏览器访问,只不过需要带后面的一长串东西:
不带的话会提示拒绝访问:
因为现在文件访问权限不是公开的。
设置下:
添加一个 / 的匿名的访问规则。
然后就可以直接访问了:
再来试试 sdk 的方式:
npm install minio安装 minio 包。
然后创建 index2.js
var Minio = require('minio')
var minioClient = new Minio.Client({
endPoint: 'localhost',
port: 9000,
useSSL: false,
accessKey: '',
secretKey: '',
})
function put() {
minioClient.fPutObject('aaa', 'hello.png', './smile.png', function (err, etag) {
if (err) return console.log(err)
console.log('上传成功');
});
}
put();创建用到的 accessKey: 跑一下:
可以看到,文件上传成功了:
同样,也可以下载文件:
const fs = require('fs');
function get() {
minioClient.getObject('aaa', 'hello.png', (err, stream) => {
if (err) return console.log(err)
stream.pipe(fs.createWriteStream('./xxx.png'));
});
}
get();用起来和阿里云 OSS 几乎一毛一样。
更多的 api 用法可以看 minio 文档。
最后,还记得跑 docker 容器的时候指定了挂载目录么:
这样,数据就会保存在本地的那个目录下:
那为什么 OSS 服务都这么相似呢?
因为它们都是遵循 AWS 的 Simple Storage Service(S3)规范的,简称 S3 规范。
所以不管哪家的 OSS,用起来都是差不多的。
案例代码上传了。
文件上传一般都是用 OSS 服务来存储,比如阿里云的 OSS。
但是 OSS 是收费的,而且有些敏感数据不能传到云上,需要私有部署,这种就可以自己搭一个 OSS 服务。
用 docker 跑了一个 minio 的容器,然后分别在管理界面和用 npm 包的方式做了文件上传和下载。
用法和阿里云 OSS 差不多,因为他们都是亚马逊 S3 规范的实现。
你公司内部有没有自己用 minio 搭 OSS 服务呢?
前面学了 Nest 如何上传文件到服务器,但实际上基本不会直接在应用服务器存文件。
因为很容易到达存储上限,而且不好管理。
一般都会用 OSS 服务,比如阿里云的 OSS。
或者自己搭的 OSS 服务,比如用 minio。
用了 OSS 服务之后,可以通过服务器中转的方式上传文件:
也就是前端把文件上传应用服务器,服务器上传阿里云或者 minio。
但这样没必要,传两次文件,浪费流量。
一般都是前端直传 OSS 服务,然后把文件 url 给应用服务器:
但这样直接把 accessKey 暴露给前端也不安全。
学阿里云 OSS 的时候讲过通过临时凭证的方式直传 OSS。
也就是应用服务器返回一个临时的凭证,前端用这个临时凭证传 OSS,不需要把 accessKey 暴露给前端。
用 minio 自然也可以。
这节就来讲下前端如何直传 minio 的 OSS 服务。
搜索下 minio:
填入一些信息:
name 是容器名。
port 是映射本地 9000 和 9001 端口到容器内的端口。
volume 是挂载本地目录到容器内的目录
这里挂载了一个本地一个目录到容器内的数据目录 /bitnami/minio/data,这样容器里的各种数据都保存在本地了。
还要指定两个环境变量,MINIO_ROOT_USER 和 MINIO_ROOT_PASSWORD,是用来登录的。
点击 run,跑起来之后可以看到数据目录被标记为 mounted,端口也映射成功了:
创建个 bucket:
设置下可以公开访问:
然后上传个文件:
浏览器直接访问文件路径的 URL:
http://localhost:9000/aaa/ground.png
可以看到,现在 OSS 服务的上传和查看图片就都成功了。
上节也写过在 node 里上传文件到 minio:
var Minio = require('minio')
var minioClient = new Minio.Client({
endPoint: 'localhost',
port: 9000,
useSSL: false,
accessKey: '',
secretKey: '',
})
function put() {
minioClient.fPutObject('aaa', 'hello.png', './smile.png', function (err, etag) {
if (err) return console.log(err)
console.log('上传成功');
});
}
put()这节来做前端直传 minio。
因为需要服务端生成临时凭证,创建个 nest 服务:
nest new minio-fe-upload安装 minio 包:
npm install --save minio然后创建个模块:
nest g module minioimport { Global, Module } from '@nestjs/common';
import * as Minio from 'minio';
export const MINIO_CLIENT = 'MINIO_CLIENT';
@Global()
@Module({
providers: [
{
provide: MINIO_CLIENT,
async useFactory() {
const client = new Minio.Client({
endPoint: 'localhost',
port: 9000,
useSSL: false,
accessKey: '',
secretKey: ''
})
return client;
}
}
],
exports: [MINIO_CLIENT]
})
export class MinioModule {}把 minio client 封装成 provider,放到 exports 里,并设置模块为 @Global。
用到 accessKey 和 secretKey 在这里创建:
在 AppController 里注入下测试下:
import { Controller, Get, Inject } from '@nestjs/common';
import { AppService } from './app.service';
import { MINIO_CLIENT } from './minio/minio.module';
import * as Minio from 'minio';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Inject(MINIO_CLIENT)
private minioClient: Minio.Client;
@Get('test')
async test() {
try {
await this.minioClient.fPutObject('aaa', 'hello.json', './package.json');
return 'http://localhost:9000/aaa/hello.json';
} catch(e) {
console.log(e);
return '上传失败';
}
}
@Get()
getHello(): string {
return this.appService.getHello();
}
}注入 Minio Client,在 test 接口里上传文件。
把服务跑起来:
npm run start:dev试一下:
上传成功。
然后要在前端做直传,
指定 public 为静态文件目录:
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets('public');
await app.listen(3000);
}
bootstrap();写下前端代码 public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="file" id="selector" multiple>
<button onclick="upload()">Upload</button>
<div id="status">No uploads</div>
<script type="text/javascript">
function upload() {
var files = document.querySelector("#selector").files;
for (var i = 0; i < files.length; i++) {
var file = files[i];
retrieveNewURL(file, (file, url) => {
uploadFile(file, url);
});
}
}
function retrieveNewURL(file, cb) {
fetch(`/presignedUrl?name=${file.name}`).then((response) => {
response.text().then((url) => {
cb(file, url);
});
}).catch((e) => {
console.error(e);
});
}
function uploadFile(file, url) {
if (document.querySelector('#status').innerText === 'No uploads') {
document.querySelector('#status').innerHTML = '';
}
fetch(url, {
method: 'PUT',
body: file
}).then(() => {
document.querySelector('#status').innerHTML += `<br>Uploaded ${file.name}.`;
}).catch((e) => {
console.error(e);
});
}
</script>
</body>
</html>这部分是文档里的。
就是一个 type 为 file 的 input,可以多选。
点击上传的时候遍历文件,对每个文件路径调用服务端的 presignedUrl 接口进行 url 签名:
之后用返回的 url 就可以直传服务端了:
用 fetch 传的,换成 axios 也一样。
然后在服务端增加这个签名接口:
@Get('presignedUrl')
async presignedUrl(@Query('name') name: string) {
return this.minioClient.presignedPutObject('aaa', name, 3600);
}这里的第一个参数是 bucketName,第二个参数是 objectName,第三个参数是过期时间,指定 3600秒,也就是一小时
bucketName 是 aaa,而 objectName 需要文件上传的时候拿到 file.name 作为参数传过来。
测试下:
上传成功!
看下 url 签名之后的样子:
其实就是在 url 里带上了鉴权信息。
这样,前端不需要 accessKey 也可以直传文件到 minio 了。
案例代码上传了。
前面实现过阿里云 OSS 的前端直传文件,只要在服务端做预签名,前端就可以不用 accessKey 实现文件上传。
这节实现了 minio 的前端文件直传,也是通过服务端做预签名,然后前端直接传 minio 就行。
一般不会直接上传文件到应用服务器,而是传阿里云 OSS 或者传 minio。
文件上传是常见需求,只要指定 content-type 为 multipart/form-data,内容就会以这种格式被传递到服务端:
服务端再按照 multipart/form-data 的格式提取数据,就能拿到其中的文件。
但当文件很大的时候,事情就变得不一样了。
假设传一个 100M 的文件需要 3 分钟,那传一个 1G 的文件就需要 30 分钟。
这样是能完成功能,但是产品的体验会很不好。
所以大文件上传的场景,需要做专门的优化。
把 1G 的大文件分割成 10 个 100M 的小文件,然后这些文件并行上传,不就快了?
然后等 10 个小文件都传完之后,再发一个请求把这 10 个小文件合并成原来的大文件。
这就是大文件分片上传的方案。
那如何拆分和合并呢?
浏览器里 Blob 有 slice 方法,可以截取某个范围的数据,而 File 就是一种 Blob:
所以可以在 input 里选择了 file 之后,通过 slice 对 File 分片。
那合并呢?
fs 的 createWriteStream 方法支持指定 start,也就是从什么位置开始写入。
这样把每个分片按照不同位置写入文件里,不就完成合并了么。
思路理清了,接下来实现一下。
创建个 Nest 项目:
npm install -g @nestjs/cli
nest new large-file-sharding-upload在 AppController 添加一个路由:
@Post('upload')
@UseInterceptors(FilesInterceptor('files', 20, {
dest: 'uploads'
}))
uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
console.log('body', body);
console.log('files', files);
}这是一个 post 接口,会读取请求体里的 files 文件字段传入该方法。
这里还需要安装用到的 multer 包的类型:
npm install -D @types/multer然后在网页里试一下:
首先在 main.ts 里开启跨域支持:
然后添加一个 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file" multiple/>
<script>
const fileInput = document.querySelector('#fileInput');
fileInput.onchange = async function () {
const data = new FormData();
data.set('name','光');
data.set('age', 20);
[...fileInput.files].forEach(item => {
data.append('files', item)
})
const res = await axios.post('http://localhost:3000/upload', data);
console.log(res);
}
</script>
</body>
</html>input 指定 multiple,可以选择多个文件。
选择文件之后,通过 post 请求 upload 接口,携带 FormData。FormData 里保存着 files 和其它字段。
起个静态服务:
npx http-server .浏览器访问下:
选择几个文件:
这时候,Nest 服务端就接收到了上传的文件和其他字段:
当然,并不是想上传多个文件,而是一个大文件的多个分片。
所以是这样写:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file"/>
<script>
const fileInput = document.querySelector('#fileInput');
const chunkSize = 20 * 1024;
fileInput.onchange = async function () {
const file = fileInput.files[0];
console.log(file);
const chunks = [];
let startPos = 0;
while(startPos < file.size) {
chunks.push(file.slice(startPos, startPos + chunkSize));
startPos += chunkSize;
}
chunks.map((chunk, index) => {
const data = new FormData();
data.set('name', file.name + '-' + index)
data.append('files', chunk);
axios.post('http://localhost:3000/upload', data);
})
}
</script>
</body>
</html>对拿到的文件进行分片,然后单独上传每个分片,分片名字为文件名 + index。
这里测试用的图片是 80k:
所以每 20k 一个分片,一共是 4 个分片。
测试下:
服务端接收到了这 4 个分片:
然后把它们移动到单独的目录:
@Post('upload')
@UseInterceptors(FilesInterceptor('files', 20, {
dest: 'uploads'
}))
uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body: { name: string }) {
console.log('body', body);
console.log('files', files);
const fileName = body.name.match(/(.+)\-\d+$/)[1];
const chunkDir = 'uploads/chunks_'+ fileName;
if(!fs.existsSync(chunkDir)){
fs.mkdirSync(chunkDir);
}
fs.cpSync(files[0].path, chunkDir + '/' + body.name);
fs.rmSync(files[0].path);
}用正则匹配出文件名:
在 uploads 下创建 chunks_文件名 的目录,把文件复制过去,然后删掉原始文件。
测试下:
分片文件移动成功了。
不过直接以 chunks_文件名 做为目录名,太容易冲突了。
可以在上传文件的时候给文件名加一个随机的字符串。
这样就不会冲突了:
接下来,就是在全部分片上传完之后,发送合并分片的请求。
添加一个 merge 的接口:
@Get('merge')
merge(@Query('name') name: string) {
const chunkDir = 'uploads/chunks_'+ name;
const files = fs.readdirSync(chunkDir);
let startPos = 0;
files.map(file => {
const filePath = chunkDir + '/' + file;
const stream = fs.createReadStream(filePath);
stream.pipe(fs.createWriteStream('uploads/' + name, {
start: startPos
}))
startPos += fs.statSync(filePath).size;
})
}接收文件名,然后查找对应的 chunks 目录,把下面的文件读取出来,按照不同的 start 位置写入到同一个文件里。
浏览器访问下这个接口:
可以看到,合并成功了:
再测试一个:
也没啥问题。
然后在合并完成之后把 chunks 目录删掉。
@Get('merge')
merge(@Query('name') name: string) {
const chunkDir = 'uploads/chunks_'+ name;
const files = fs.readdirSync(chunkDir);
let count = 0;
let startPos = 0;
files.map(file => {
const filePath = chunkDir + '/' + file;
const stream = fs.createReadStream(filePath);
stream.pipe(fs.createWriteStream('uploads/' + name, {
start: startPos
})).on('finish', () => {
count ++;
if(count === files.length) {
fs.rm(chunkDir, {
recursive: true
}, () =>{});
}
})
startPos += fs.statSync(filePath).size;
});
}然后在前端代码里,当分片全部上传完之后,调用 merge 接口:
const tasks = [];
chunks.map((chunk, index) => {
const data = new FormData();
data.set('name', randomStr + '_' + file.name + '-' + index)
data.append('files', chunk);
tasks.push(axios.post('http://localhost:3000/upload', data));
})
await Promise.all(tasks);
axios.get('http://localhost:3000/merge?name=' + randomStr + '_' + file.name);连起来测试下:
因为文件比较小,开启 network 的 slow 3g 网速来测。
可以看到,分片上传和最后的合并都没问题。
当然,你还可以加一个进度条,这个用 axios 很容易实现:
至此,大文件分片上传就完成了。
阿里云的大文件分片上传也是这样实现的:
当文件比较大的时候,文件上传会很慢,这时候一般会通过分片的方式来优化。
原理就是浏览器里通过 slice 来把文件分成多个分片,并发上传。
服务端把这些分片文件保存在一个目录下。
当所有分片传输完成时,发送一个合并请求,服务端通过 fs.createWriteStream 指定 start 位置,来把这些分片文件写入到同一个文件里,完成合并。
这样,就实现了大文件分片上传。
文件上传是常见需求,一般不会把文件直接上传到应用服务器,因为单台服务器存储空间是有限的,不好扩展。
会用单独的 OSS (Object Storage Service)对象存储服务来上传下载文件。
比如一般会买阿里云的 OSS 服务。
本地文件存储是目录-文件的组织方式:
而 OSS 服务的存储结构是这样的:
一个桶里放一些文件。
阿里云 OSS 的控制台也提到了对象存储没有目录层级结构:
但下面明明是支持目录的呀:
这其实只是模拟实现的。
Object 会存储 id、文件内容、元数据三部分信息:
阿里云 OSS 只是用元信息部分模拟实现了目录。
就像打了个 tag 一样,并不是说文件存储在这个 tag 下,只是你可以用这个 tag 来检索文件。
除了对象存储 OSS,阿里云也提供了文件存储和块存储的方式:
块存储就是把整块磁盘给你用,你需要自己格式化,存储容量有限。
文件存储就是有目录层次结构,你可以上传下载文件,存储容量有限。
对象存储就是 key-value 存储,分布式的方式实现的,存储容量无限。
这些简单了解就行,绝大多数情况下,都是用 OSS 对象存储。
买个阿里云的 OSS 服务来试试看:
我买了 40G 的 OSS 国内通用资源包,花了 5 块钱。
然后创建个 Bucket(桶):
在北京创建了一个 Bucket,文件就会存储在那里的服务器上。
设置公共读,也就是这些文件大家都可以直接访问。
不然私有的方式,你访问每个文件都要带上一些身份信息:
有的同学说,不是静态文件要在全国各地都能访问到么?存在北京的服务器会不会访问速度慢?
这是 CDN 的活:
接入 CDN 后,访问该域名会走到云服务的 DNS,然后返回一台最近的缓存服务器的地址,这台服务器会从源站拿文件来缓存,之后就不再访问源站。
这里的源站就可以是 OSS 服务。
创建 Bucket 之后,上传个文件试试:
上传完之后在文件列表就可以看到这个文件了:
点开可以看到文件详情,用这个 URL 就可以访问:
当然,生产环境下不会直接用 OSS 的 URL 访问,而是会开启 CDN,用网站域名访问,最终回源到 OSS 服务:
在控制台里上传很简单,那如果想在代码里上传呢?
官方文档里有示例代码,试试看:
mkdir oss-test
cd oss-test
npm init -y安装用到的包:
npm install ali-oss写下代码:
const OSS = require('ali-oss')
const client = new OSS({
region: 'oss-cn-beijing',
bucket: 'guang-333',
accessKeyId: '',
accessKeySecret: '',
});
async function put () {
try {
const result = await client.put('cat.png', './mao.png');
console.log(result);
} catch (e) {
console.log(e);
}
}
put();region 在概览里可以看到:
这里的 accessKeyId 和 accessKeySecret 是什么呢?
本来身份认证都是通过用户名密码:
但这样不够安全,所以创建了 accessKey 用来代表身份,用它来做身份认证,就算泄漏了,也不影响别的:
创建个 accessKey:
创建完成后,拿到 accessKeyId 和 accessKeySecret 后运行代码:
这里的 mao.png 是这样的:
在控制台可以看到上传成功了:
这就是 OSS 用 api 上传文件的用法。
只是刚刚用的 accessKey 不够安全。
打开 accessKey 管理页面的时候就提示了:
让不要直接用 accessKey,而是创建一个子用户再创建 accessKey。
那就创建个子用户:
然后用这个 accessKey 的 id 和 secret 就好了:
但你直接换上它还不行:
会提示你 403,没有权限。
需要你授权下:
新增一个授权:
把 OSS 的管理和读取权限给这个子用户:
然后再试下:
这时候就上传成功了。
回过头来看下,不得不说阿里云在安全这一块设计的就很巧妙。
如果直接用用户名密码验证呢:
那万一泄漏了不就完蛋了么?
但是如果创建个 accessKey 用它来做身份认证:
就算泄漏了我也可以禁用啊:
再进一步,直接用这个 accessKey 它是有所有权限的。
先创建个 RAM 子用户,再分配给他某些权限,这样就算泄漏了,是不是能做的事情就更少了?
当然就更安全。
所以说,阿里云这套 accessKey 和 RAM 子用户的身份认证方式,还是很不错的。
再说回 OSS,一般的文件直接上传就行,涉及到大文件就要分片上传了。
分片上传实现原理是前端常见面试题了,大家都能答上来,上节刚实现过。
就是把文件用 slice 方法分成一个个小的片,然后全部上传完之后请求一个接口合并分片。
阿里云的大文件分片上传也是这样实现的:
具体怎么用直接看文档就好了,这里就不试了:
有了 OSS 服务之后,上传文件还需要经过应用服务器么?
可以经过也可以不经过。
如果经过应用服务器,那就要客户端上传文件之后,在服务里接受文件,上传 OSS:
这样当然是可以的,还能保护 accessKey 不被人窃取。
只是会浪费应用服务器的流量。
那如果不经过呢?
在客户端用 accessKey 把文件传到 OSS,之后把 URL 传给应用服务器就好了。
这样减少了应用服务器的流量消耗,但是增加了 accessKey 暴露的风险。
各有各的坏处。
那有没有啥两全其美的办法呢?
有。
阿里云的文档里也提到了这个问题。
它给出的解决方案就是生成一个临时的签名来用。
代码是这样的:
const OSS = require('ali-oss')
async function main() {
const config = {
region: 'oss-cn-beijing',
bucket: 'guang-333',
accessKeyId: '你的AccessKeyId',
accessKeySecret: '你的AccessKeySecret',
}
const client = new OSS(config);
const date = new Date();
date.setDate(date.getDate() + 1);
const res = client.calculatePostSignature({
expiration: date.toISOString(),
conditions: [
["content-length-range", 0, 1048576000], //设置上传文件的大小限制。
]
});
console.log(res);
const location = await client.getBucketLocation();
const host = `http://${config.bucket}.${location.location}.aliyuncs.com`;
console.log(host);
}
main();
上传 OSS 的地址,用的临时 signature 和 policy 都有了:
这些代码不用记,文档里都有:
这样就能在网页里用这些来上传文件到 OSS 了:
创建个 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/axios@1.6.5/dist/axios.min.js"></script>
</head>
<body>
<input id="fileInput" type="file"/>
<script>
const fileInput = document.getElementById('fileInput');
async function getOSSInfo() {
await '请求应用服务器拿到临时凭证';
return {
OSSAccessKeyId: '你的AccessKeyId',
Signature: '服务端返回的签名',
policy: '服务端返回的 policy',
host: 'http://guang-333.oss-cn-beijing.aliyuncs.com'
}
}
fileInput.onchange = async () => {
const file = fileInput.files[0];
const ossInfo = await getOSSInfo();
const formdata = new FormData()
formdata.append('key', file.name);
formdata.append('OSSAccessKeyId', ossInfo.OSSAccessKeyId)
formdata.append('policy', ossInfo.policy)
formdata.append('signature', ossInfo.Signature)
formdata.append('success_action_status', '200')
formdata.append('file', file)
const res = await axios.post(ossInfo.host, formdata);
if(res.status === 200) {
const img = document.createElement('img');
img.src = ossInfo.host + '/' + file.name
document.body.append(img);
alert('上传成功');
}
}
</script>
</body>
</html>这里 getOSSInfo 应该是请求服务端的接口,拿到刚才控制台输出的那些东西。
这里就简化下,直接写死在代码里了。
引入 axios,用这些信息来上传文件。
跑个静态服务器:
npx http-server .这时候你上传文件的时候会提示跨域错误:
在控制台开启下跨域:
然后再试下:
上传成功了!
控制台文件列表也可以看到这个文件:
这就是完美的 OSS 上传方案。
服务端用 RAM 子用户的 accessKey 来生成临时签名,然后返回给客户端,客户端用这个来直传文件到 OSS。
因为临时的签名过期时间很短,设置的是一天,所以暴露的风险也不大。
这样服务端就根本没有接受文件的压力,只要等客户端上传完之后,带上 URL 就好了。
案例代码上传了
上传文件一般不会直接存在服务器目录下,这样不好扩展,一般会用阿里云的 OSS,它会自己做弹性扩展,所以存储空间是无限的。
OSS 对象存储是在一个 bucket 桶下,存放多个文件。
它是用 key-value 存储的,没有目录的概念,阿里云 OSS 的目录只是用元信息来模拟实现的。
在测试了在控制台的文件上传,也测试过了 node 里用 ali-oss 包来上传、在网页里直传 OSS 这三种上传方式。
不管在哪里上传,都需要 accessKeyId 和 accessKeySecret。
这个是阿里云的安全策略,因为直接用用户名密码,一旦泄漏就很麻烦,而 accessKey 泄漏了也可以禁用。而且建议用 RAM 子用户的方式生成 accessKey,这样可以最小化权限,进一步减少泄漏的风险。
客户端直传 OSS 的方式不需要消耗服务器的资源,但是会有泄漏 accessKey 的风险,所以一般都是用服务端生成临时的签名等信息,然后用这些信息来上传。
这种方案就是最完美的 OSS 上传方案了。
掌握了这些,就完全足够应对工作中的 OSS 使用了。