不说话,装高手。
Maintain silence and pretend to be an experta
本文主要是记录在搭建博客过程中,随着数据规模不断增长,存储和管理变得越来越重要。所以当时考虑使用图床或者 OSS 服务,于是买了一年的私人图床,因为价格便宜经常掉线,后面考虑更换 OSS 对象存储服务。但是没钱,所以我考虑自己在服务区上部署一套 minio 对象存储服务。下面是对 minio 的介绍以及这次部署的流程。
根据官网的表述简单整理,MinIO 是一个开源的、轻量级、易于部署的对象存储服务器。它采用分布式架构,可以在多个节点上进行数据存储和访问,实现数据的冗余备份和高可用性。同时,它还具备强大的扩展性,可以根据需要进行水平扩展,以满足不断增长的存储需求。它可以用于构建私有云存储、备份存储、数据湖等各种场景。
下载最新的稳定的 MinIO 二进制文件并将其安装到系统。
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
sudo mv minio /usr/local/bin/
// 新建 monio 数据文件夹
mkdir -p /admin/minio/data /admin/minio/config
minio server /admin/minio/data --console-address :9999
启动成功后得到如下 log,证明 MinIO 启动成功。
在浏览器中输入公网 ip + 端口 9999 能看到如下界面。
MinIO 已经部署完成,接下来使用 systemctl 来管理 MinIO。
使用 systemctl 管理 MinIO 有以下好处:
// 创建 minio.service 文件
touch /etc/systemd/system/minio.service
// 配置内容
vi /etc/systemd/system/minio.service
[Unit]
Description=MinIO Server // MinIO 服务的名称
Documentation=https://min.io/docs/minio/linux/index.html // 这个无所谓可以随便写,我写了 minio 文档的地址
After=network.target // 在网络连接完成后启动 MinIO 服务
[Service]
ExecStart=/root/minio server /admin/minio/data --console-address :9999 // 指定启动 MinIO 服务的命令行 /root/minio ==> minio 的安装位置 /admin/minio/data ==> minio 服务储存数据的目录 --console-address :9999 ==> 设置 minio 控制台的地址和端口
Restart=always // 如果服务意外停止,自动重新启动
Environment="MINIO_ROOT_USER=xxxxxx" "MINIO_ROOT_PASSWORD=xxxxxx" // 设置环境变量,用于指定 MinIO 的根用户和密码
[Install]
WantedBy=multi-user.target // 指定在多用户模式下启动 MinIO 服务
systemctl start minio
systemctl status minio
得到如下结果怎么 minio 服务启动完成
使用上面配置的账户密码进入 minio 管理界面,在下方 user 界面新建一个用户
新增完成后点进去,在这边创建密钥
在新建密钥界面就可以看到两个 key,一定要保存下来,这玩意只会出现一次
完成密钥创建后再去 buckets 界面创建储存桶
以上步骤完成后就可以在 nestjs 中使用 minio 了
npm install minio -S
全局配置文件加入 minio 配置 app.config.ts
export const minioConfig = {
endPoint: 'xxx.xxx.xxx.xxx', // 服务器公网 ip
port: 9000, // minio 挂载的端口,没指定的话一般是 9000
useSSL: false, // 是否开启 SSL
accessKey: 'xxxxxxxxxxxxxxx', // 访问密钥
secretKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', // 密钥
};
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import * as Minio from 'minio';
import { v4 } from 'uuid';
import * as dayjs from 'dayjs';
@Injectable()
export class MinIOService {
private readonly minioClient: Minio.Client;
constructor(private readonly httpService: HttpService) {
this.minioClient = new Minio.Client(minioConfig);
}
// 多图片上传
async uploadImageMultiple(file: Array<Express.Multer.File>) {
let promiseList = [];
const length = file.length; // 文件数量
promiseList = file.map((item) => {
return this.uploadImageWithMinio(item.originalname, item.buffer);
});
return Promise.all(promiseList).then((res) => {
const status = res.map((item) => item.status); // 上传状态
const trueCount = status.filter((item) => item === true).length; // 成功统计
const falseCount = status.filter((item) => item === false).length; // 失败统计
const links = [];
const error = [];
res.forEach((item) => {
if (item.status) {
links.push(item.data.links.url);
} else {
error.push(item);
}
});
if (error.length > 0) {
console.log('上传图片 Error', error);
}
const returnData = {
msg: `共上传${length}张图片,成功${trueCount},失败${falseCount}`,
data: links,
};
return returnData;
});
}
// 使用 minio 上传图片
async uploadImageWithMinio(objectName: string, data: Buffer) {
try {
const fileExtension = objectName.match(/\.([^.]+)$/)[1]; // 获取文件后缀
const newfilename = `${v4()}-${dayjs().format('YYYYMMDDHHmmss')}.${fileExtension}`; // 使用 uuid + 当前时间重写文件名
await this.minioClient.putObject('blogpic', newfilename, data); // 调用 minio 提供的方法上传对象
return {
status: true,
data: {
links: {
url: `https://dawdlepig.cn/minio/blogpic/${newfilename}`, // 返回文件地址,这里我使用 nginx 做了代理,后面再说,一般文件地址是 ip + port+ bucketName + fileName
},
},
};
} catch (error) {
console.log('oss图片上传失败', error);
return {
status: false,
error
};
}
}
}
import { Controller, HttpException, HttpStatus, Post, UploadedFiles, UseInterceptors } from '@nestjs/common'
import { AnyFilesInterceptor } from '@nestjs/platform-express';
import { RequestResult } from 'src/common/interface/common.interface';
import { MinIOService } from './minio.service'
@Controller('minio')
export class MinIOController {
constructor(private readonly minioService: MinIOService) {}
@Post('uploadImageOSS')
@UseInterceptors(AnyFilesInterceptor())
async uploadImageOSS(
@UploadedFiles() files: Array<Express.Multer.File>,
): Promise<RequestResult> {
if (!files || files.length === 0) {
throw new HttpException('file不能为空!', HttpStatus.OK);
}
const imageType = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
files.forEach((file) => {
if (!imageType.includes(file.mimetype)) {
throw new HttpException('文件类型错误', HttpStatus.OK);
}
});
try {
const res = await this.systemService.uploadImageMultiple(files, 'oss');
return {
data: res.data,
msg: res.msg,
};
} catch (error) {
console.error(error);
throw new HttpException(
{ error, message: '图片上传失败' },
HttpStatus.OK,
);
}
}
}
一般咱们发布网站都是有域名的,很少裸奔,所以我们的图片地址也需要使用 nginx 代理一下,下面是我的配置
location /blogpic/ {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;
proxy_pass http://localhost:9000;
}
NoSuchkey ==> 检查自己的文件名是否正确
AccessDenied ==> 检查 bucket 权限,是否设置成 Private 了,可以在 Buckets 界面更改,设置成 Public 或者自定义权限