8 分布式文件服务(系统)
1、背景
搭建分布式文件系统的初衷只是为了解决文件(附件)的存储问题。所以选型主要考虑的因素有:
•通用的文件系统•开源•轻量级•开发友好(Java,Go)•性能卓越•API操作,并能提供简单的管理视图
2、为什么需要分布式文件服务器
主要原因有以下几点
•通过API的方式管理文件,文件管理变成开发问题•降低WEB服务器的压力,提高文件访问的效率和稳定性•统一、公用•独立服务易扩展•统一安全认证和保护
3、分布式文件存储要求
•数据安全 需要实现数据冗余,避免数据的单点故障•可线性扩展 当数据增长到TB、甚至PB以上时,存储方案需要支持可线性扩展,暂时没有此需求•存储高可用 某个存储服务宕掉时,不影响整体存储方案的可用•性能 性能达到应用要求
4、分布式文件系统对比
对比说明/文件系统 | TFS | FastDFS | MogileFS | MooseFS | GlusterFS | Ceph |
开发语言 | C++ | C | Perl | C | C | C++ |
开源协议 | GPL V2 | GPL V3 | GPL | GPL V3 | GPL V3 | LGPL |
数据存储方式 | 块 | 文件/Trunk | 文件 | 块 | 文件/块 | 对象/文件/块 |
集群节点通信协议 | 私有协议(TCP) | 私有协议(TCP) | HTTP | 私有协议(TCP) | 私有协议(TCP)/ RDAM(远程直接访问内存) | 私有协议(TCP) |
专用元数据存储点 | 占用NS | 无 | 占用DB | 占用MFS | 无 | 占用MDS |
在线扩容 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
冗余备份 | 支持 | 支持 | - | 支持 | 支持 | 支持 |
单点故障 | 存在 | 不存在 | 存在 | 存在 | 不存在 | 存在 |
跨集群同步 | 支持 | 部分支持 | - | - | 支持 | 不适用 |
易用性 | 安装复杂,官方文档少 | 安装简单,社区相对活跃 | - | 安装简单,官方文档多 | 安装简单,官方文档专业化 | 安装简单,官方文档专业化 |
适用场景 | 跨集群的小文件 | 单集群的中小文件 | - | 单集群的大中文件 | 跨集群云存储 | 单集群的大中小文件 |
指标 | 适合类型 | 文件分布 | 系统性能 | 复杂度 | FUSE | POSIX | 备份机制 | 通讯协议接口 | 社区支持 | 去重 | 开发语言 |
FastDFS | 4KB~500MB | 小文件合并存储不分片处理 | 很高 | 简单 | 不支持 | 不支持 | 组内冗余备份 | ApiHTTP | 国内用户群 | C语言 | |
TFS | 所有文件 | 小文件合并,以block组织分片 | 复杂 | 不支持 | 不支持 | Block存储多份,主辅灾备 | APIhttp | 少 | C++ | ||
MFS | 大于64K | 分片存储 | Master占内存多 | 支持 | 支持 | 多点备份动态冗余 | 使用fuse挂在 | 较多 | Perl | ||
HDFS | 大文件 | 大文件分片分块存储 | 简单 | 支持 | 支持 | 多副本 | 原生api | 较多 | java | ||
Ceph | 对象文件块 | OSD一主多从 | 复杂 | 支持 | 支持 | 多副本 | 原生api | 较少 | C++ | ||
MogileFS | 海量小图片 | 高 | 复杂 | 可以支持 | 不支持 | 动态冗余 | 原生api | 文档少 | Perl | ||
ClusterFS | 大文件 | 简单 | 支持 | 支持 | 镜像 | 多 | C |
特性 | ceph | minio | swift | hbase/hdfs | GlusterFS | fastdfs |
开发语言 | C | go | python | java | 副本 | 副本 |
数据冗余 | 副本,纠删码 | Reed-Solomon code | 副本 | 副本 | 副本 | 副本 |
一致性 | 强一致性 | 强一致 | 最终一致 | 最终一致 | ? | ? |
动态扩展 | HASH | 不支持动态加节点 | 一致性hash | ? | ? | ? |
性能 | ? | ? | ? | ? | ? | ? |
中心节点 | 对象存储无中心,cephFS有元数据服务中心点 | 无中心 | 无中心 | nameNode单点 | ? | ? |
存储方式 | 块、文件、对象 | 对象存储(分块) | 块存储 | 块存储 | ? | ? |
活跃度 | 高,中文社区不算活跃 | 高,没有中文社区 | 高 | 高 | 中 | 中 |
成熟度 | 高 | 中 | 高 | 高 | ? | ? |
操作系统 | linux-3.10.0+[1] | linux,windows | ? | 任何支持java的OS | ? | ? |
文件系统 | EXT4,XFS | EXT4,XFS | ? | ? | ? | ? |
客户端 | c、python,S3 | java,s3 | java,RESTful | java,RESTful | ? | ? |
断点续传 | 兼容S3,分段上传,断点下载 | 兼容S3,分段上传,断点下载 | 不支持 | 不支持 | ? | ? |
学习成本 | 高 | 中 | ? | 中 | ? | ? |
前景 | 10 | 8 | 9 | 9 | 7 | 5 |
开源协议 | LGPL version 2.1 | Apache v2.0 | Apache V2.0 | ? | ? | ? |
管理工具 | ceph-admin,ceph-mgr,zabbix插件[2],web管理工具[3] | 命令行工具 mc | ? | ? | ? | ? |
存储系统 | Ceph | GlusterFS | Sheepdog | Lustre | Swift | Cinder | TFS | HDFS | MooseFS | FastDFS | MogileFS |
开发语言 | C++ | C | C | C | Python | Python | C++ | Java | C | C | Perl |
开源协议 | LGPL | GPL V3 | GPLv2 | GPL | Apache | Apache | GPL V2 | Apache | GPL V3 | GPL V3 | GPL |
数据存储方式 | 对象/文件/块 | 文件/块 | 块 | 对象 | 对象 | 块 | 文件 | 文件 | 块 | 文件/块 | 文件 |
集群节点通信协议 | 私有协议(TCP) | 私有协议(TCP)/ RDAM(远程直接访问内存) | totem协议 | 私有协议(TCP)/ RDAM(远程直接访问内存) | TCP | 未知 | TCP | TCP | TCP | TCP | HTTP |
专用元数据存储点 | 占用MDS | 无 | 无 | 双MDS | 无 | 未知 | 占用NS | 占用MDS | 占用MFS | 无 | 占用DB |
在线扩容 | 支持 | 支持 | 支持 | 支持 | 支持 | 未知 | 支持 | 支持 | 支持 | 支持 | 支持 |
冗余备份 | 支持 | 支持 | 支持 | 无 | 支持 | 未知 | 支持 | 支持 | 支持 | 支持 | 不支持 |
单点故障 | 存在 | 不存在 | 不存在 | 存在 | 不存在 | 未知 | 存在 | 存在 | 存在 | 不存在 | 存在 |
跨集群同步 | 不支持 | 支持 | 未知 | 未知 | 未知 | 未知 | 支持 | 不支持 | 不支持 | 部分支持 | 不支持 |
易用性 | 安装简单,官方文档专业化 | 安装简单,官方文档专业化 | 未知 | 复杂。而且Lustre严重依赖内核,需要重新编译内核 | 未知 | 目前来说框架不算成熟存在一些问题 | 安装复杂,官方文档少 | 安装简单,官方文档专业化 | 安装简单,官方文档多 | 安装简单,社区相对活跃 | 未知 |
适用场景 | 单集群的大中小文件 | 跨集群云存储 | 弹性块存储虚拟机 | 大文件读写 | openstack对象存储 | openstack块存储 | 跨集群的小文件 | Mapreduce使用的文件存储 | 单集群的大中文件 | 单集群的中小文件 | 未知 |
FUSE挂载 | 支持 | 支持 | 支持 | 支持 | 支持 | 未知 | 未知 | 支持 | 支持 | 不支持 | 不支持 |
访问接口 | POSIX | POSIX | 未知 | POSIX/MPI | POSIX | 未知 | 不支持POSIX | 不支持POSIX | POSIX | 不支持POSIX | 不支持POSIX |
综上:在ceph、minio、fastdfs之间选择。
ceph
ceph优点
1.成熟稳定2.功能强大3.支持数千节点4.支持动态增加节点,自动平衡数据分布5.可配置性强,可针对不同场景进行调优6.支持对象存储(OSD)集群,通过CRUSH算法,完成文件动态定位, 处理效率更高7.支持通过FUSE方式挂载,降低客户端的开发成本,通用性高8.支持分布式的MDS/MON,无单点故障9.强大的容错处理和自愈能力10.支持在线扩容和冗余备份,增强系统的可靠性11.高性能、高可用、高可扩展性、特性丰富
ceph缺点
学习成本高,安装运维复杂
ceph应用场景
可应付各种场景
minio
minio优点
1.学习成本低,安装运维简单,开箱即用2.目前minio论坛[4]推广给力,有问必答3.有java客户端、js客户端、常用语言都有
minio缺点
1.社区不够成熟,业界参考资料较少2.不支持动态增加节点,minio创始人的设计理念就是动态增加节点太复杂,后续会采用其它方案来支持扩容。
fastDFS
fastdfs优点
1.系统无需支持POSIX(可移植操作系统),降低了系统的复杂度,处理效率更高2.支持在线扩容机制,增强系统的可扩展性3.实现了软RAID,增强系统的并发处理能力及数据容错恢复能力4.支持主从文件,支持自定义扩展名5.主备Tracker服务,增强系统的可用性
fastdfs缺点
1.不支持断点续传,对大文件将是噩梦(FastDFS不适合大文件存储)2.不支持POSIX通用接口访问,通用性较低3.对跨公网的文件同步,存在较大延迟,需要应用做相应的容错策略4.同步机制不支持文件正确性校验,降低了系统的可用性5.通过API下载,存在单点的性能瓶颈
fastdfs应用场景
1.单集群部署的应用2.存储后基本不做改动3.小中型文件
结论:目前的需求是需要一个分布式文件存储服务器,考虑到现在的人员情况,选择minio作为内部的文件存储服务器。
5、minio使用
MinIO是基于go语言开发的一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。
MinIO是一个非常轻量的服务,可以很简单的和其他应用的结合,类似 NodeJS, Redis 或者 MySQL。
5.1引入依赖
<!-- minio support -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version> <!-- 6.0.8 -->
<exclusions>
<exclusion>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 解决版本冲突问题,如果不主动覆盖,默认引入3.8.1会报错 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.13.1</version><!--$NO-MVN-MAN-VER$-->
</dependency>
复制
5.2配置
@Configuration
@ConfigurationProperties(ignoreInvalidFields=true, ignoreUnknownFields=true, prefix="minio")
@Data
public class MinioConfig {
// 地址
private String endpoint;
private String accessKey;
private String secretKey;
}
复制
5.3客户端Template
/**
* minio 交互类
*
* @author zhangwy
*/
@Component
@Repository
public class MinioTemplate implements InitializingBean {
@Autowired
MinioConfig minioConfig;
@Autowired
private FileService fileService;
private MinioClient client;
@Override
public void afterPropertiesSet() throws Exception {
this.client = new MinioClient(minioConfig.getEndpoint(), minioConfig.getAccessKey(), minioConfig.getSecretKey());
}
/**
* 判断bucket是否存在
*
* @param bucketName
* @return boolean
* */
@SneakyThrows
public boolean bucketExists(String bucketName) {
return client.bucketExists(bucketName);
}
/**
* 创建bucket
*
* @param bucketName
*/
@SneakyThrows
public void createBucket(String bucketName) {
if (!bucketExists(bucketName)) {
client.makeBucket(bucketName);
}
}
/**
* 获取全部bucket
*
* @return List<Bucket>
*/
@SneakyThrows
public List<Bucket> getAllBuckets() {
return client.listBuckets();
}
/**
* 获取所有bucket的Object
*
* @param bucketName
* @return List<MinioItem>
* */
@SneakyThrows
public List<MinioItem> getAllObjects(String bucketName) {
List<MinioItem> objectList = new ArrayList<>();
if(this.bucketExists(bucketName)) {
Iterable<Result<Item>> objectsIterator = client.listObjects(bucketName);
while (objectsIterator.iterator().hasNext()) {
objectList.add(new MinioItem(objectsIterator.iterator().next().get()));
}
}
return objectList;
}
/**
* 根据文件前缀查询文件
*
* @param bucketName
* @param prefix
* @param recursive 是否递归查询
* @return List<MinioItem>
*/
@SneakyThrows
public List<MinioItem> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {
List<MinioItem> objectList = new ArrayList<>();
if(this.bucketExists(bucketName)) {
Iterable<Result<Item>> objectsIterator = client.listObjects(bucketName, prefix, recursive);
while (objectsIterator.iterator().hasNext()) {
objectList.add(new MinioItem(objectsIterator.iterator().next().get()));
}
}
return objectList;
}
/**
* 根据名称查找bucket
*
* @param bucketName
* @return Optional<Bucket>
*/
@SneakyThrows
public Optional<Bucket> getBucket(String bucketName) {
return client.listBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
}
/**
* 根据bucket名称移除bucket
*
* @param bucketName
*/
@SneakyThrows
public void removeBucket(String bucketName) {
if(bucketExists(bucketName)) {
client.removeBucket(bucketName);
}
}
/**
* 查询未完成的上传
*
* @param bucketName
* @return List<MinioUpload>
*/
@SneakyThrows
public List<MinioUpload> getIncompleteUploads(String bucketName) {
List<MinioUpload> uploadList = new ArrayList<>();
if(bucketExists(bucketName)) {
Iterable<Result<Upload>> uploadIterator = client.listIncompleteUploads(bucketName);
while (uploadIterator.iterator().hasNext()) {
uploadList.add(new MinioUpload(uploadIterator.iterator().next().get()));
}
}
return uploadList;
}
/**
* 删除未完成的上传
*
* @param bucketName
* @param objectName
*/
@SneakyThrows
public void removeIncompleteUploads(String bucketName, String objectName) {
client.removeIncompleteUpload(bucketName, objectName);
}
/**
* 上传文件
*
* @param bucketName 桶名
* @param objectName 对象名
* @param stream 文件流
* @throws Exception
*/
@SneakyThrows
public void putObject(String bucketName, String objectName, InputStream stream) throws IOException {
putObject(bucketName, objectName, stream, Long.valueOf(stream.available()), null, null, "application/octet-stream");
}
@SneakyThrows
public String putObject(String bucketName, MultipartFile mFile) {
//判断桶名是否存在,不存在则创建
createBucket(bucketName);
SimpleDateFormat sdfOne = new SimpleDateFormat("yyyyMMdd");
//生成日期+随机+原始文件名
String ymd = sdfOne.format(new Date());
String uuid = UUID.randomUUID().toString();
String name = mFile.getOriginalFilename();
String minIoName = ymd+"/"+uuid+"/"+name;
putObject(bucketName, minIoName , mFile.getInputStream(), mFile.getSize(), null, null, mFile.getContentType());
//保存在数据库里
File file = new File();
file.setBucket(bucketName);
file.setDir(ymd);
file.setUuid(uuid);
file.setName(name);
file.setType(name.substring(name.lastIndexOf(".")+1));
file.setSize(mFile.getSize());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String nowTime = sdf.format(new Date());
Date gmtCreate = null;
try {
gmtCreate = new java.sql.Date(sdf.parse(nowTime).getTime());
} catch (ParseException e1) {
e1.printStackTrace();
}
file.setGmtCreate(gmtCreate);
User user = UserContext.getUser();
int userId = 0;
if (user != null) {
userId = user.getId();
}
file.setCreator(userId);
fileService.save(file);
return file.getId().toString();
}
@SneakyThrows
public String getUrl(int id) {
File file = fileService.getOne(id);
String name = file.getDir()+"/"+file.getUuid()+"/"+file.getName();
String bucketName = file.getBucket();
String objectURL = getObjectURL(bucketName, name);
return objectURL;
}
@SneakyThrows
public List<Map<String, Object>> getUrls(String ids) {
List<Integer> idList= Util.string2IntList(ids);
List<Map<String, Object>> maps = new ArrayList<Map<String,Object>>();
for (int i = 0; i < idList.size(); i++) {
int id = idList.get(i);
File file = fileService.getOne(id);
String url = getUrl(id);
Map<String, Object> map = new HashMap<String, Object>();
map.put("id", file.getId());
map.put("fileName", file.getName());
map.put("url", url);
maps.add(map);
}
return maps;
}
@SneakyThrows
public String putObjects(String bucketName, MultipartFile[] files) {
//判断桶名是否存在
createBucket(bucketName);
List<String> list = new ArrayList<String>();
if (files.length != 0) {
for (int i = 0; i < files.length; i++) {
if (!files[i].isEmpty() ) {
String id = putObject(bucketName, files[i]);
list.add(id);
}
}
}
String ids = Util.list2String(list);
return ids;
}
@SneakyThrows
public void putObject(String bucketName,String objectName, MultipartFile file) {
putObject(bucketName, objectName, file.getInputStream(), file.getSize(), null, null, file.getContentType());
}
@SneakyThrows
public void putObject(String bucketName, String objectName, InputStream stream, String contentType) {
putObject(bucketName, objectName, stream, Long.valueOf(stream.available()), null, null, contentType);
}
@SneakyThrows
public void putObject(String bucketName, String objectName, InputStream stream, long size, Map<String,String> headerMap, ServerSideEncryption sse, String contentType) throws IOException {
if("".equals(StringUtil.null2String(contentType))) {
contentType = "application/octet-stream";
}
if(size == 0) {
size = Long.valueOf(stream.available());
}
client.putObject(bucketName, objectName, stream, size, headerMap, sse, contentType);
}
/**
* KMS加密
* Map<String,String> myContext = new HashMap<>();
* myContext.put("key1","value1");
* ServerSideEncryption sse = ServerSideEncryption.withManagedKeys("Key-Id", myContext);
* S3加密
* ServerSideEncryption sse = ServerSideEncryption.atRest();
* AES加密
* KeyGenerator keyGen = KeyGenerator.getInstance("AES");
* keyGen.init(256);
* ServerSideEncryption sse = ServerSideEncryption.withCustomerKey(keyGen.generateKey());
* */
@SneakyThrows
public void putObjectEncrypted(String bucketName, String objectName, InputStream stream, ServerSideEncryption sse) {
putObject(bucketName, objectName, stream,Long.valueOf(stream.available()), null, sse, "application/octet-stream");
}
/**
* 示例
* // Create metadata map
* Map<String, String> headerMap = new HashMap<>();
* // Add custom metadata
* headerMap.put("CustomMeta", "TEST");
* // Add custom content type
* headerMap.put("Content-Type", "application/octet-stream");
* // Add storage class
* headerMap.put("X-Amz-Storage-Class", "REDUCED_REDUNDANCY");
* */
@SneakyThrows
public void putObjectWithHeader(String bucketName, String objectName, InputStream stream, Map<String,String> headerMap) throws Exception {
putObject(bucketName, objectName, stream,Long.valueOf(stream.available()), headerMap, null, "application/octet-stream");
}
public boolean objectExists(String bucketName, String objectName) {
try {
client.statObject(bucketName, objectName);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 复制对象
*
* @param sourceBucketName
* @param sourceObjectName
* @param targetBucketName
* @param targetObjectName
* */
@SneakyThrows
public void copyObject(String sourceBucketName, String sourceObjectName, String targetBucketName, String targetObjectName) {
if(objectExists(sourceBucketName,sourceObjectName)) {
client.copyObject(sourceBucketName, sourceObjectName, targetBucketName, targetObjectName);
}
}
/**
* 获取文件信息
*
* @param bucketName
* @param objectName
* @return ObjectStat
*/
@SneakyThrows
public ObjectStat getObjectInfo(String bucketName, String objectName) {
if (objectExists(bucketName,objectName)) {
return client.statObject(bucketName, objectName);
} else {
return null;
}
}
/**
* 获取多文件信息
*
* @param fileIds
* @param bucketName
* @return ObjectStat
*/
@SneakyThrows
public List<MinioObject> getObjectInfos(String fileIds, String bucketName) {
List<Integer> ids = Util.string2IntList(fileIds);
List<MinioObject> listFile = new ArrayList<MinioObject>();
String objectName = "";
File file = new File();
for (int i = 0; i < ids.size(); i++) {
file = fileService.getOne(ids.get(i));
objectName = file.getDir()+"/"+file.getUuid()+"/"+file.getName();
ObjectStat objectInfo = getObjectInfo(bucketName, objectName);
MinioObject minioObject = new MinioObject(objectInfo);
listFile.add(minioObject);
}
return listFile;
}
/**
* 获取文件
*
* @param bucketName
* @param objectName
* @return InputStream
*/
@SneakyThrows
public InputStream getObject(String bucketName, String objectName) {
if (objectExists(bucketName,objectName)) {
return client.getObject(bucketName, objectName);
} else {
return null;
}
}
/**
* 下载文件保存到本地
*
* @param bucketName
* @param objectName
* @param fileName
*/
@SneakyThrows
public void getObject(String bucketName, String objectName, String fileName) {
if (objectExists(bucketName,objectName)) {
client.getObject(bucketName, objectName, fileName);
}
}
/**
* 删除单个文件
*
* @param bucketName
* @param objectName
*/
@SneakyThrows
public void removeObject(String bucketName, String objectName) {
if(objectExists(bucketName,objectName)) {
client.removeObject(bucketName, objectName);
}
}
/**
* 删除多个文件
*
* @param bucketName
* @param objectNames
*/
@SneakyThrows
public void removeObjects(String bucketName, List<String> objectNames) {
client.removeObjects(bucketName, objectNames);
}
/**
* 获取文件外链,默认过期时间为七天
*
* @param bucketName
* @param objectName
* @return url
*/
@SneakyThrows
public String getObjectURL(String bucketName, String objectName) {
return getObjectURL(bucketName, objectName, 7 * 24 * 3600);
}
/**
* 获取文件外链
*
* @author zhuhui
* @date 2019-07-09
* @param bucketName
* @param objectName
* @param expires
* @return
*/
@SneakyThrows
public String getObjectURL(String bucketName, String objectName,Integer expires) {
if(objectExists(bucketName,objectName)) {
return client.presignedGetObject(bucketName, objectName, expires);
} else {
return "";
}
}
/**
* 单文件上传返回ID
*
* @author zhuhui
* @date 2019-07-08
* @param file
* @param bucketName
* @return id
*/
public String fileUpload(MultipartFile file, String bucketName) {
return putObject(bucketName, file);
}
/**
* 通过spring的多文件上传
*
* @author zhuhui
* @date 2019-07-09
* @param multipartRequest
* @param bucketName
* @return ids
*/
public Map<String, String> fileUploads(DefaultMultipartHttpServletRequest multipartRequest, String bucketName) {
//判断桶名是否存在,不存在则创建
createBucket(bucketName);
String ids = "";
if (multipartRequest != null) {
List<String> list = new ArrayList<String>();
Iterator<String> iterator = multipartRequest.getFileNames();
while (iterator.hasNext()) {
////单文件上传 。
//MultipartFile file = multipartRequest.getFile(iterator.next());//一次传一个文件
//if (StringUtils.hasText(file.getOriginalFilename())) {
// file.transferTo(new File("E:/upload_" + file.getOriginalFilename()));
//}
// 多文件上传
List<MultipartFile> fileList = multipartRequest.getFiles(iterator.next());
for (MultipartFile file : fileList) {
if (StringUtils.hasText(file.getOriginalFilename())) {
String id = putObject(bucketName, file);
list.add(id);
}
}
}
ids = Util.list2String(list);
}
Map<String, String> map = new HashMap<String, String>();
map.put("ids", ids);
return map;
}
public void updateFile(int contactId, String ids) {
fileService.updateFile(contactId, ids);
}
public String fileUploadsByIo(String files, String bucketName) throws IOException {
String ids = "";
List<String> list = new ArrayList<String>();
JSONObject object = JSONObject.fromObject(files);
Map<String, Object> map = object;
for (String fileName : map.keySet()) {
String base64 = map.get(fileName).toString();
ByteArrayInputStream stream =new Base64Convert().base64ToIo(base64); //将字符串转换为byte数组
//判断桶名是否存在,不存在则创建
createBucket(bucketName);
SimpleDateFormat sdfOne = new SimpleDateFormat("yyyyMMdd");
//生成日期+随机+原始文件名
String ymd = sdfOne.format(new Date());
String uuid = UUID.randomUUID().toString();
String minIoName = ymd+"/"+uuid+"/"+fileName;
putObject(bucketName, minIoName, stream);
//保存在数据库里
File file = new File();
file.setBucket(bucketName);
file.setDir(ymd);
file.setUuid(uuid);
file.setName(fileName);
file.setType(fileName.substring(fileName.lastIndexOf(".")+1));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String nowTime = sdf.format(new Date());
Date gmtCreate = null;
try {
gmtCreate = new java.sql.Date(sdf.parse(nowTime).getTime());
} catch (ParseException e1) {
e1.printStackTrace();
}
file.setGmtCreate(gmtCreate);
User user = UserContext.getUser();
int userId = 0;
if (user != null) {
userId = user.getId();
}
file.setCreator(userId);
fileService.save(file);
list.add(file.getId().toString());
}
ids = Util.list2String(list);
return ids;
}
/**
* 多文件上传返回ID
*
* @author zhuhui
* @date 2019-07-05
* @param userName
* @param files
* @param bucketName
*/
/*
public String fileUploads(MultipartFile[] files, String bucketName) {
return putObjects(bucketName, files);
}
*/
}
复制
注意事项
桶的概念相当于文件夹,桶下面还可以有子文件夹,设置子文件夹的方式就是在传递objectName的时候按照文件路径进行传递
minIoName = ymd+"/"+uuid+"/"+fileName;
复制