OSS
「OSS」的英文全称是Object Storage Service,翻译成中文就是「对象存储服务」,官方一点解释就是对象存储是一种使用HTTP API存储和检索非结构化数据和元数据对象的工具。
白话文解释就是将系统所要用的文件上传到云硬盘上,该云硬盘提供了文件下载、上传等一列服务,这样的服务以及技术可以统称为OSS,业内提供OSS服务的厂商很多,知名常用且成规模的七牛云等。一般我们做网站还是服务都希望将文件放在一个指定的服务器,并且更好的管理,并且动静分离更好的提升软件性能我之间写过如何springboot整合七牛云,感兴趣的可以了解一下,但是由于七牛云需要自己购买他的服务器,对于学生做项目不友好,所以今天介绍的是开源可以自己搭建的oss服务
Minio
介绍:
MinIO 是在 GNU Affero 通用公共许可证 v3.0 下发布的高性能对象存储。 它是与 Amazon S3 云存储服务兼容的 API。 使用 MinIO 为机器学习、分析和应用程序数据工作负载构建高性能基础架构。Minio官网
搭建(DOCKER)
docker的好处不用多说了,最主要的是服务的安装与卸载很方便,这里也提供docker的学习笔记,那么现在开始搭建
拉取镜像
使用之间可以先查看下各个版本
docker pull minio/minio
一般选择star最多的版本
docker pull minion
在虚拟机或者云服务创建一个文件夹,并且包含一个数据,一个配置文件目录做挂载
运行
docker run -p 9000:9000 -p 9090:9090 \
--name minio \
-d --restart=always \
-e "MINIO_ACCESS_KEY=minioadmin" \
-e "MINIO_SECRET_KEY=tskradmin" \
-v /home/docker/minio/data:/data \
-v /home/docker/minio/config:/root/.minio \
minio/minio:latest server /data
说明:
-p 9000:9000 -p 9090:9090:将容器的9000端口映射到主机的9000端口,以及将容器的9090端口映射到主机的9090端口。
–name minio:给容器命名为"minio",以便后续管理和操作。
-d --restart=always:在后台模式下运行容器,并设置容器失败或主机重启后自动重启。
-e “MINIO_ACCESS_KEY=minioadmin” -e “MINIO_SECRET_KEY=tskradmin”:设置MinIO的访问密钥和秘钥。请确保你使用的是安全的访问密钥和秘钥。
-v /home/docker/minio/data:/data:挂载主机上的 /home/docker/minio/data 目录到容器内的 /data 目录,用于持久化存储MinIO数据。
-v /home/docker/minio/config:/root/.minio:挂载主机上的 /home/docker/minio/config 目录到容器内的 /root/.minio 目录,用于存储MinIO的配置文件。
minio/minio:latest server /data:指定使用的MinIO镜像及其命令。server /data 表示以服务器模式运行MinIO,并指定数据存储目录为 /data。
注意
目前24年我再次使用的时候 发现新版minio启动的时候需要固定9000端口 不然默认采用动态端口无法访问
docker run -p 9000:9000 -p 9090:9090 \
--name minio \
-d --restart=always \
-e "MINIO_ACCESS_KEY=minioadmin" \
-e "MINIO_SECRET_KEY=tskradmin" \
-v /home/docker/minio/data:/data \
-v /home/docker/minio/config:/root/.minio \
minio/minio:latest server /data \
--console-address ":9090" --address ":9000"
固定端口: --console-address “:9090” --address “:9000”
账号密码是配置文件中指定的
- MINIO_ACCESS_KEY=账户
- MINIO_SECRET_KEY=密码
对象储存服务中,数据是按照bucket 桶来的,所以先新建一个桶
自此服务端部署完成
java客户端拉取依赖
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
我的是8.5.1
java客户端处死话:
MinioClient minioClient = MinioClient.builder()
.endpoint("http://192.168.249.132:9001")
.credentials("minioadmin", "minioadmin")
.build();
核心api
上传文件
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket("bucket-name") // 桶的名称
.object("object-name") // 对象的名称(文件名)
.stream(inputStream, inputStream.available(), -1) // 文件输入流
.contentType("application/octet-stream") // 文件的 MIME 类型
.build();
minioClient.putObject(putObjectArgs);
- bucket:文件存储桶的名称。
- object:对象(文件名)的名称。
- stream:文件输入流。
- size:对象的字节大小。如果未知,设置为 -1。
- contentType:文件的 MIME 类型。
java测试上传文件
@Test
@DisplayName("测试minio上传下载")
public void testMn() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
client = MinioClient.builder().credentials("minioadmin", "minioadmin")
.endpoint("http://localhost:9001").build();
FileInputStream inputStream;
PutObjectArgs putObjectArgs;
// 读取文件转换为输入流
inputStream = new FileInputStream("D:\\list.html");
// 上传一个对象 需要使用聚合对象包装在里面()
putObjectArgs = PutObjectArgs.builder()
.object("list.html")//文件名
.contentType("text/html")//文件类型
.bucket("leadnews")//区域名
.stream(inputStream, inputStream.available(), -1).build();//转换为流进行传输
client.putObject(putObjectArgs);
}
这样就可以实现上传了,现在进行封装,
便于使用
建立配置属性类便于动态注入数据
@Data
@ConfigurationProperties(prefix = "minio") // 文件上传 配置前缀file.oss
public class MinIOConfigProperties implements Serializable {
private String accessKey;
private String secretKey;
private String bucket;
private String endpoint;
private String readPath;
}
配置文件
minio:
accessKey: minioadmin
secretKey: minioadmin
bucket: leadnews
endpoint: http://192.168.249.132:9001
readPath: http://192.168.249.132:9001
注入配置类实列化
@Data
@Configuration
@EnableConfigurationProperties({MinIOConfigProperties.class})
//当引入FileStorageService接口时
@ConditionalOnClass(FileStorageService.class)
public class MinIOConfig {
@Autowired
private MinIOConfigProperties minIOConfigProperties;
@Bean
public MinioClient buildMinioClient() {
return MinioClient
.builder()
.credentials(minIOConfigProperties.getAccessKey(), minIOConfigProperties.getSecretKey())
.endpoint(minIOConfigProperties.getEndpoint())
.build();
}
}
下载api
InputStream inputStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket("bucket-name")
.object("object-name")
.build()
);
参数:
- bucket:检索对象的桶的名称。
- object:要检索的对象的名称。
删除接口
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket("bucket-name")
.object("object-name")
.build()
);
- bucket:删除对象的桶的名称。
- object:要删除的对象的名称。
各个接口的单元测试
@Slf4j
@SpringBootTest(classes = MdmAdminApplication.class)
public class MinioStorageTest {
private MinioClient minioClient;
private static final String ENDPOINT = "http://localhost:9000";
private static final String ACCESS_KEY = "minioadmin";
private static final String SECRET_KEY = "tskradmin";
private static final String BUCKET_NAME = "apks";
private static final String CONTENT_TYPE = "application/vnd.android.package-archive";
private static final String SEPARATOR = "/";
@BeforeEach
public void setUp() {
minioClient = MinioClient.builder()
.endpoint(ENDPOINT)
.credentials(ACCESS_KEY, SECRET_KEY)
.build();
}
@Test
@DisplayName("测试minio客户端 上传apk文件")
public void testMinioClient() {
// String FILE_PATH = "E:\\APKS\\da_1701653930625.apk";
String FILE_PATH = "C:\\Users\\侯\\Desktop\\dev_work.zip";
assertDoesNotThrow(() -> {
try (FileInputStream inputStream = new FileInputStream(FILE_PATH)) {
File file = new File(FILE_PATH);
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object("test"+ "/" +file.getName()) //如果需要加目录就是手动拼接
// .object(file.getName())
.contentType(CONTENT_TYPE)
.bucket(BUCKET_NAME)
.stream(inputStream, inputStream.available(), -1)
.build();
minioClient.putObject(putObjectArgs);
System.out.println("文件上传成功访问路径...."+ENDPOINT +"/"+BUCKET_NAME+"/"+"test"+ "/" +file.getName());
}
});
}
@Test
@DisplayName("测试minio客户端 上传图片文件")
public void testMinioImgClient() {
assertDoesNotThrow(() -> {
//上传后的名门
String FILE_PATH = "D:\\3.png";
try (FileInputStream inputStream = new FileInputStream("D:\\2.png")) {
File file = new File(FILE_PATH);
String fileName = file.getName();
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
String contentType;
String directory;
switch (fileExtension) {
case "png":
contentType = "image/png";
directory = "img";
break;
case "jpg":
case "jpeg":
contentType = "image/jpeg";
directory = "img";
break;
case "gif":
contentType = "image/gif";
directory = "img";
break;
default:
throw new IllegalArgumentException("Unsupported file type: " + fileExtension);
}
String objectName = directory + "/" + fileName;
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(objectName)
.contentType(contentType)
.bucket(BUCKET_NAME)
.stream(inputStream, inputStream.available(), -1)
.build();
minioClient.putObject(putObjectArgs);
System.out.println("文件上传成功访问路径.... " + ENDPOINT + "/" + BUCKET_NAME + "/" + objectName);
}
});
}
@Test
@DisplayName("测试minio客户端 下载文件")
public void testMinioClientDownload() {
String pathUrl = "http://localhost:9000/apks/img/2.png";
String key = pathUrl.replace(ENDPOINT + SEPARATOR, "");
int index = key.indexOf(SEPARATOR);
String bucket = key.substring(0, index);
String filePath = key.substring(index + 1);
//下载到本地的时候不同系统 路径分隔符不同 这里使用File.separator
String localFilePath = "E:"+File.separator+"ALI"+File.separator + new File(filePath).getName(); // 指定本地保存路径
try (InputStream inputStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucket)
.object(filePath)
.build());
FileOutputStream outputStream = new FileOutputStream(localFilePath)) {
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buf)) > 0) {
outputStream.write(buf, 0, bytesRead);
}
System.out.println("文件下载成功, 保存路径: " + localFilePath);
} catch (Exception e) {
log.error("minio 下载文件出错。URL: {}", pathUrl, e);
throw new RuntimeException("从MinIO下载文件失败", e);
}
}
@Test
@DisplayName("测试minio客户端 删除文件")
public void testMinioClientRemove() {
String fileName = "3.png";
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(BUCKET_NAME)
.object("img" + SEPARATOR + fileName)
.build());
System.out.println("文件删除成功: " + fileName);
} catch (Exception e) {
log.error("minio 删除文件出错。URL: {}", fileName, e);
throw new RuntimeException("从MinIO删除文件失败", e);
}
}
}
注意只有给你的对应的桶权限才可以再游览器访问文件
是一个json文件 可以配置那些buckt公开可以访问
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"*"
]
},
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::apks/*"
]
}
]
}
我这里指定的是apk,并且这些上传后的地址就是 minio服务地址 + 文件名地址(如果想要目录就上传时候拼接"/")
有一个好处
对于apk,压缩包等文件
是的,对于浏览器无法直接打开的文件类型,如 zip、tar、apk、iso 等,浏览器通常会自动触发下载行为。这是因为这些文件类型的 Content-Type 指示浏览器它们是二进制文件,无法在浏览器中直接展示。上传对象服务器后的地址,在游览器中直接打开就会触发行为
如果是手写下载接口还需要再响应体中写流
@GetMapping("/download/apk/{dir}/{filename}")
public void downloadApk(HttpServletResponse response, @PathVariable("dir") String dir, @PathVariable("filename") String filename) throws IOException {
//夸文件的分割符号
File apkFile = new File(storageService.properties.getLocal().getPath() + File.separator + dir + File.separator + filename);
// String apkUrl =storageService.properties.getLocal().getUrl()+dir+"/"+filename;
// File apkFile = new File("E:\\APKS\\da_1701653930625.apk");
if (!apkFile.exists()) {
response.sendError(404, "安装包不存在");
}
response.setContentType("application/vnd.android.package-archive");
response.setHeader("Content-Disposition", "attachment; filename=app.apk");
FileUtil.writeToStream(apkFile, response.getOutputStream());
}
各个api封装为接口
文件处理接口
public interface FileStorageService {
/**
* 上传图片文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
public String uploadImgFile(String prefix, String filename,InputStream inputStream);
/**
* 上传html文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
public String uploadHtmlFile(String prefix, String filename,InputStream inputStream);
/**
* 删除文件
* @param pathUrl 文件全路径
*/
public void delete(String pathUrl);
/**
* 下载文件
* @param pathUrl 文件全路径
* @return
*
*/
public byte[] downLoadFile(String pathUrl);
}
imp
@Slf4j
@EnableConfigurationProperties(MinIOConfigProperties.class)
@Import(MinIOConfig.class)
public class MinIOFileStorageService implements FileStorageService {
@Autowired
private MinioClient minioClient;
@Autowired
private MinIOConfigProperties minIOConfigProperties;
private final static String separator = "/";
/**
* @param dirPath
* @param filename yyyy/mm/dd/file.jpg
* @return
*/
public String builderFilePath(String dirPath,String filename) {
StringBuilder stringBuilder = new StringBuilder(50);
if(!StringUtils.isEmpty(dirPath)){
stringBuilder.append(dirPath).append(separator);
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
String todayStr = sdf.format(new Date());
stringBuilder.append(todayStr).append(separator);
stringBuilder.append(filename);
return stringBuilder.toString();
}
/**
* 上传图片文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
@Override
public String uploadImgFile(String prefix, String filename,InputStream inputStream) {
String filePath = builderFilePath(prefix, filename);
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(filePath)
.contentType("image/jpg")
.bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1)
.build();
minioClient.putObject(putObjectArgs);
StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath());
urlPath.append(separator+minIOConfigProperties.getBucket());
urlPath.append(separator);
urlPath.append(filePath);
return urlPath.toString();
}catch (Exception ex){
log.error("minio put file error.",ex);
throw new RuntimeException("上传文件失败");
}
}
/**
* 上传html文件
* @param prefix 文件前缀
* @param filename 文件名
* @param inputStream 文件流
* @return 文件全路径
*/
@Override
public String uploadHtmlFile(String prefix, String filename,InputStream inputStream) {
String filePath = builderFilePath(prefix, filename);
try {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.object(filePath)
.contentType("text/html")
.bucket(minIOConfigProperties.getBucket()).stream(inputStream,inputStream.available(),-1)
.build();
minioClient.putObject(putObjectArgs);
StringBuilder urlPath = new StringBuilder(minIOConfigProperties.getReadPath());
urlPath.append(separator+minIOConfigProperties.getBucket());
urlPath.append(separator);
urlPath.append(filePath);
return urlPath.toString();
}catch (Exception ex){
log.error("minio put file error.",ex);
ex.printStackTrace();
throw new RuntimeException("上传文件失败");
}
}
/**
* 删除文件
* @param pathUrl 文件全路径
*/
@Override
public void delete(String pathUrl) {
String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/","");
int index = key.indexOf(separator);
String bucket = key.substring(0,index);
String filePath = key.substring(index+1);
// 删除Objects
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder().bucket(bucket).object(filePath).build();
try {
minioClient.removeObject(removeObjectArgs);
} catch (Exception e) {
log.error("minio remove file error. pathUrl:{}",pathUrl);
e.printStackTrace();
}
}
/**
* 下载文件
* @param pathUrl 文件全路径
* @return 文件流
*
*/
@Override
public byte[] downLoadFile(String pathUrl) {
String key = pathUrl.replace(minIOConfigProperties.getEndpoint()+"/","");
int index = key.indexOf(separator);
String bucket = key.substring(0,index);
String filePath = key.substring(index+1);
InputStream inputStream = null;
try {
inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(minIOConfigProperties.getBucket()).object(filePath).build());
} catch (Exception e) {
log.error("minio down file error. pathUrl:{}",pathUrl);
e.printStackTrace();
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buff = new byte[100];
int rc = 0;
while (true) {
try {
if (!((rc = inputStream.read(buff, 0, 100)) > 0)) break;
} catch (IOException e) {
e.printStackTrace();
}
byteArrayOutputStream.write(buff, 0, rc);
}
return byteArrayOutputStream.toByteArray();
}
}
封装为模块
如果是springcloud分布式业务,文件处理可以封装为模块给其他模块使用
新建模块
<modules>
<module>heima-leadnews-basic</module> <modules>
<module>heima-file-starter</module>
</modules>
</modules>
在项目新建模块,依赖如上配置yaml文件不写,因为是做成模块给其他模块使用的
在该模块的resource目录新建meta-inf元数据配置文件
内容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.heima.file.service.impl.MinIOFileStorageService
这个配置的作用是为了将一个模块(或库)的功能引入到应用程序中,以便在应用程序的其他部分中可以使用这个模块提供的功能。这个模块可能包含一些服务、组件或类,你可以使用@Autowired等注解来注入它们,以便在应用程序中使用。
通常情况下,如果你没有一个显式的启动类(Spring Boot应用的入口类),或者你不想将这个模块的功能包含在应用程序的启动类中,你可以使用这种方式,通过spring.factories配置来自动启用这个模块。
这个模块的配置会在应用程序启动时被Spring Boot自动加载和初始化,从而使你可以在应用程序中使用它提供的功能,而不需要手动实例化或配置相关的Bean。这种方式可以使应用程序的模块化更好,遵循了依赖注入和解耦的原则。
在需要使用的模块导入该模块即可
进行优化
可以存储的对象有很多种 所以可以设置一个校验方法根据文件后缀判断上传类型和oss对应目录
private static final String CONTENT_TYPE = "contentType";
private static final String REAL_PATH = "realPath";
public HashMap<String, String> validateFileType(String fileName) {
HashMap<String, String> hashMap = new HashMap<>();
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
String contentType;
String directory;
switch (fileExtension) {
case "png":
contentType = "image/png";
directory = "img";
break;
case "jpg":
case "jpeg":
contentType = "image/jpeg";
directory = "img";
break;
case "gif":
contentType = "image/gif";
directory = "img";
break;
case "apk":
contentType = "application/vnd.android.package-archive";
directory = "apk";
break;
case "zip":
contentType = "application/zip";
directory = "archive";
break;
case "tar":
contentType = "application/x-tar";
directory = "archive";
break;
case "pdf":
contentType = "application/pdf";
directory = "documents";
break;
case "doc":
case "docx":
contentType = "application/msword";
directory = "documents";
break;
case "xls":
case "xlsx":
contentType = "application/vnd.ms-excel";
directory = "documents";
break;
case "ppt":
case "pptx":
contentType = "application/vnd.ms-powerpoint";
directory = "documents";
break;
case "txt":
contentType = "text/plain";
directory = "documents";
break;
case "mp3":
contentType = "audio/mpeg";
directory = "audio";
break;
case "wav":
contentType = "audio/wav";
directory = "audio";
break;
case "mp4":
contentType = "video/mp4";
directory = "video";
break;
case "avi":
contentType = "video/x-msvideo";
directory = "video";
break;
case "mkv":
contentType = "video/x-matroska";
directory = "video";
break;
default:
throw new IllegalArgumentException("Unsupported file type: " + fileExtension);
}
hashMap.put(CONTENT_TYPE, contentType);
hashMap.put(REAL_PATH, directory);
return hashMap;
}
但是这样上传还是不够通用, 因为是根据文件名来的,而DDD架构中服务层只关心服务 所以不能接收仅仅局限于Springboot框架的MultipartFile file类型
public String upload(MultipartFile file, String path) {
try {
//如果BucketName不存在,则创建
log.info("Uploading:{}",properties.getMinio().getBucketName());
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(properties.getMinio().getBucketName()).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(properties.getMinio().getBucketName()).build());
}
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
Optional<MediaType> mediaType = MediaTypeFactory.getMediaType(path);
if (mediaType.isPresent()) {
contentType = mediaType.get().toString();
}
log.info("contentType:{}",contentType);
InputStream inputStream = file.getInputStream();
HashMap<String, String> map = validateFileType(file.getName());
minioClient.putObject(
PutObjectArgs.builder()
.bucket(properties.getMinio().getBucketName())
.contentType(contentType)
.object(path)
.stream(inputStream, inputStream.available(), -1)
.build()
);
} catch (Exception e) {
try {
throw new ServerException("上传文件失败:",500, e.getMessage());
} catch (ServerException ex) {
throw new RuntimeException(ex);
}
}
return properties.getMinio().getEndPoint() + "/" + properties.getMinio().getBucketName() + "/" + path;
}
private static final String CONTENT_TYPE = "contentType";
private static final String REAL_PATH = "realPath";
/**
* 返回上传的目录路径路径
* 以及对应的contentType
* @param fileName
* @return
*/
public HashMap<String, String> validateFileType(String fileName) {
HashMap<String, String> hashMap = new HashMap<>();
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
String contentType;
String directory;
switch (fileExtension) {
case "png":
contentType = "image/png";
directory = "img";
break;
case "jpg":
case "jpeg":
contentType = "image/jpeg";
directory = "img";
break;
case "gif":
contentType = "image/gif";
directory = "img";
break;
case "apk":
contentType = "application/vnd.android.package-archive";
directory = "apk";
break;
case "zip":
contentType = "application/zip";
directory = "archive";
break;
case "tar":
contentType = "application/x-tar";
directory = "archive";
break;
case "pdf":
contentType = "application/pdf";
directory = "documents";
break;
case "doc":
case "docx":
contentType = "application/msword";
directory = "documents";
break;
case "xls":
case "xlsx":
contentType = "application/vnd.ms-excel";
directory = "documents";
break;
case "ppt":
case "pptx":
contentType = "application/vnd.ms-powerpoint";
directory = "documents";
break;
case "txt":
contentType = "text/plain";
directory = "documents";
break;
case "mp3":
contentType = "audio/mpeg";
directory = "audio";
break;
case "wav":
contentType = "audio/wav";
directory = "audio";
break;
case "mp4":
contentType = "video/mp4";
directory = "video";
break;
case "avi":
contentType = "video/x-msvideo";
directory = "video";
break;
case "mkv":
contentType = "video/x-matroska";
directory = "video";
break;
default:
throw new IllegalArgumentException("Unsupported file type: " + fileExtension);
}
hashMap.put(CONTENT_TYPE, contentType);
hashMap.put(REAL_PATH, directory);
return hashMap;
}
如果系统有多种Storage 保存,所以可以采取更简单的方法
Storage 接口
public abstract class StorageService {
public StorageProperties properties;
/**
* 根据文件名,生成带时间戳的新文件名
*
* @param fileName 文件名
* @return 返回带时间戳的文件名
*/
public String getNewFileName(String fileName) {
// 主文件名,不包含扩展名
String prefix = FileNameUtil.getPrefix(fileName);
// 文件扩展名
String suffix = FileNameUtil.getSuffix(fileName);
// 把当天HH:mm:ss,转换成秒
long time = DateUtil.timeToSecond(DateUtil.formatTime(new Date()));
// 新文件名
return prefix + "_" + time + "." + suffix;
}
/**
* 生成路径,不包含文件名
*
* @return 返回生成的路径
*/
public String getPath() {
// 文件路径
String path = DateUtil.format(new Date(), "yyyyMMdd");
// 如果有前缀,则也带上
if (StringUtils.hasText(properties.getConfig().getPrefix())) {
path = properties.getConfig().getPrefix() + "/" + path;
System.out.println("prex"+properties.getConfig().getPrefix() );
}
return path;
}
/**
* 根据文件名,生成路径
*
* @param fileName 文件名
* @return 生成文件路径
*/
public String getPath(String fileName) {
return getPath() + "/" + getNewFileName(fileName);
}
/**
* 文件上传
*
* @param data 文件字节数组
* @param path 文件路径,包含文件名
* @return 返回http地址
*/
public abstract String upload(byte[] data, String path);
/**
* 文件上传
*
* @param inputStream 字节流
* @param path 文件路径,包含文件名
* @return 返回http地址
*/
public abstract String upload(InputStream inputStream, String path);
}
minio实现 这样直接上传流 更加通用 但是如果是安装包或者重要信息 建议还是类型判断这样,因为文件包可能会损坏
public class MinioStorageService extends StorageService {
private final MinioClient minioClient;
public MinioStorageService(StorageProperties properties) {
this.properties = properties;
minioClient = MinioClient.builder().endpoint(properties.getMinio().getEndPoint())
.credentials(properties.getMinio().getAccessKey(), properties.getMinio().getSecretKey()).build();
}
@Override
public String upload(byte[] data, String path) {
return upload(new ByteArrayInputStream(data), path);
}
@Override
public String upload(InputStream inputStream, String path) {
try {
//如果BucketName不存在,则创建
log.info("Uploading:{}",properties.getMinio().getBucketName());
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(properties.getMinio().getBucketName()).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(properties.getMinio().getBucketName()).build());
}
//直接是二进制流
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
Optional<MediaType> mediaType = MediaTypeFactory.getMediaType(path);
if (mediaType.isPresent()) {
contentType = mediaType.get().toString();
}
minioClient.putObject(
PutObjectArgs.builder()
.bucket(properties.getMinio().getBucketName())
.contentType(contentType)
.object(path)
.stream(inputStream, inputStream.available(), -1)
.build()
);
} catch (Exception e) {
try {
throw new ServerException("上传文件失败:",500, e.getMessage());
} catch (ServerException ex) {
throw new RuntimeException(ex);
}
}
//服务地址/桶/文件名
return properties.getMinio().getEndPoint() + "/" + properties.getMinio().getBucketName() + "/" + path;
}
}
本地实现
public class LocalStorageService extends StorageService {
public LocalStorageService(StorageProperties properties) {
this.properties = properties;
}
@Override
public String upload(byte[] data, String path) {
return upload(new ByteArrayInputStream(data), path);
}
@SneakyThrows
@Override
public String upload(InputStream inputStream, String path) {
try {
// log.info("配置文件路径:{}",properties.getLocal().getPath() );//D://MDM
// log.info("文件路径{}", path);//真实路径20240807/da_1701653930625_60037.apk
//上传到本地
File file = new File(properties.getLocal().getPath() + File.separator + path);
// 没有目录,则自动创建目录
File parent = file.getParentFile();
if (parent != null && !parent.mkdirs() && !parent.isDirectory()) {
throw new IOException("目录 '" + parent + "' 创建失败");
}
FileCopyUtils.copy(inputStream, Files.newOutputStream(file.toPath()));
} catch (Exception e) {
throw new ServerException("上传文件失败:", e);
}
// log.info("properties.getConfig().getDomain{}",properties.getConfig().getDomain());//域名(http://localhost:8080/")
// log.info("properties.getLocal().getUrl{}",properties.getLocal().getUrl()); 接口路径
return properties.getConfig().getDomain() + "/" + properties.getLocal().getUrl() + "/" + path;
}
}
七牛云
public class QiniuStorageService extends StorageService {
private final UploadManager uploadManager;
public QiniuStorageService(StorageProperties properties) {
this.properties = properties;
uploadManager = new UploadManager(new Configuration(Region.autoRegion()));
}
@Override
public String upload(byte[] data, String path) {
try {
String token = Auth.create(properties.getQiniu().getAccessKey(), properties.getQiniu().getSecretKey()).
uploadToken(properties.getQiniu().getBucketName());
Response res = uploadManager.put(data, path, token);
if (!res.isOK()) {
throw new ServerException(res.toString());
}
return properties.getConfig().getDomain() + "/" + path;
} catch (Exception e) {
throw new ServerException("上传文件失败:", e);
}
}
@Override
public String upload(InputStream inputStream, String path) {
try {
byte[] data = IOUtils.toByteArray(inputStream);
return this.upload(data, path);
} catch (IOException e) {
throw new ServerException("上传文件失败:", e);
}
}
}
等等这样 就可以通用的进行上传了,再controller层进行解析拼接上传后的目录
public class MinioStorageService extends StorageService {
private final MinioClient minioClient;
public MinioStorageService(StorageProperties properties) {
this.properties = properties;
minioClient = MinioClient.builder().endpoint(properties.getMinio().getEndPoint())
.credentials(properties.getMinio().getAccessKey(), properties.getMinio().getSecretKey()).build();
}
@Override
public String upload(byte[] data, String path) {
return upload(new ByteArrayInputStream(data), path);
}
@Override
public String upload(InputStream inputStream, String path) {
try {
//如果BucketName不存在,则创建
log.info("Uploading:{}",properties.getMinio().getBucketName());
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(properties.getMinio().getBucketName()).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(properties.getMinio().getBucketName()).build());
}
String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
Optional<MediaType> mediaType = MediaTypeFactory.getMediaType(path);
if (mediaType.isPresent()) {
contentType = mediaType.get().toString();
}
path=validateFileType(path);
minioClient.putObject(
PutObjectArgs.builder()
.bucket(properties.getMinio().getBucketName())
.contentType(contentType)
.object(path)
.stream(inputStream, inputStream.available(), -1)
.build()
);
} catch (Exception e) {
try {
throw new ServerException("上传文件失败:",500, e.getMessage());
} catch (ServerException ex) {
throw new RuntimeException(ex);
}
}
return properties.getMinio().getEndPoint() + "/" + properties.getMinio().getBucketName() + "/" + path;
}
/**
* 定制目录结构
* @param fileName
* @return
*/
public String validateFileType(String fileName) {
String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
String directory;
switch (fileExtension) {
case "png":
case "gif":
case "jpg":
case "jpeg":
directory = "img";
break;
case "apk":
directory = "apk";
break;
case "zip":
case "tar":
directory = "archive";
break;
case "pdf":
case "ppt":
case "pptx":
case "txt":
case "xls":
case "xlsx":
case "doc":
case "docx":
directory = "documents";
break;
case "mp3":
case "wav":
directory = "audio";
break;
case "mp4":
case "mkv":
case "avi":
directory = "video";
break;
default:
throw new IllegalArgumentException("Unsupported file type: " + fileExtension);
}
return directory+"/"+fileName;
}
}
这样就可以有目录结构,也可以再次添加时间结构
// 主文件名,不包含扩展名
String prefix = FileNameUtil.getPrefix(fileName);
// 文件扩展名
String suffix = FileNameUtil.getSuffix(fileName);
// 把当天HH:mm:ss,转换成秒
long time = DateUtil.timeToSecond(DateUtil.formatTime(new Date()));
// 新文件名
return prefix + "_" + time + "." + suffix;