支援Fast DFS、伺服器、OSS等上傳方式
介紹
在實際的業務中,可以根據客戶的需求設定不同的檔案上傳需求,支援普通伺服器上傳+分散式上傳(Fast DFS)+雲服務上傳OSS(OSS)
軟體架構
為了方便演示使用,本專案使用的是前後端不分離的架構
前端:Jquery.uploadFile
後端:SpringBoot
前期準備:FastDFS、OSS(華為)、伺服器
實現邏輯
透過 application 配置對上傳檔案進行一個自定義配置,從而部署在不同客戶環境可以自定義選擇方式。
優點:
- 一鍵切換;
- 支援當前主流方式;
缺點:
- 遷移資料難度增加:因為表示FileID在物件儲存和伺服器上傳都是生成的UUID,而FastDFS是返回存取ID,當需要遷移的時候,透過指令碼可以快速將FastDFS的資料遷移上雲,因為儲存ID可以共用。但是物件儲存和伺服器上傳的UUID無法被FastDFS使用,增加遷移成本
核心程式碼
package com.example.file.util;
import com.example.file.common.ResultBean;
import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.proto.storage.DownloadByteArray;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.obs.services.ObsClient;
import com.obs.services.exception.ObsException;
import com.obs.services.model.DeleteObjectRequest;
import com.obs.services.model.GetObjectRequest;
import com.obs.services.model.ObsObject;
import com.obs.services.model.PutObjectResult;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.Objects;
import java.util.UUID;
@Slf4j
public class FileUtil {
/**
*
* @param file 檔案
* @param uploadFlag 標識
* @param uploadPath 上傳路徑
* @param endPoint 域名
* @param ak ak
* @param sk sk
* @param bucketName 桶名字
* @return fileId 用於下載
*/
public static ResultBean uploadFile(MultipartFile file, String uploadFlag, String uploadPath,
FastFileStorageClient fastFileStorageClient,
String endPoint, String ak, String sk,
String bucketName) {
if (StringUtils.isBlank(uploadFlag)){
ResultBean.error("uploadFlag is null");
}
switch (uploadFlag){
case "fastDFS":
return uploadFileByFastDFS(file,fastFileStorageClient);
case "huaweiOOS":
return uploadFileByHuaweiObject(file, endPoint, ak, ak, bucketName);
case "server":
default:
return uploadFileByOrigin(file,uploadPath);
}}
/**
* 上傳檔案fastDFS
* @param file 檔名
* @return
*/
private static ResultBean uploadFileByFastDFS(MultipartFile file,FastFileStorageClient fastFileStorageClient){
Long size=file.getSize();
String fileName=file.getOriginalFilename();
String extName=fileName.substring(fileName.lastIndexOf(".")+1);
InputStream inputStream=null;
try {
inputStream=file.getInputStream();
//1-上傳的檔案流 2-檔案的大小 3-檔案的字尾 4-可以不管他
StorePath storePath=fastFileStorageClient.uploadFile(inputStream,size,extName,null);
log.info("[uploadFileByFastDFS][FullPath]"+storePath.getFullPath());
return ResultBean.success(storePath.getPath());
}catch (Exception e){
log.info("[ERROR][uploadFileByFastDFS]"+e.getMessage());
return ResultBean.error(e.getMessage());
}finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 物件儲存上傳
* @param file 檔名
* @return
*/
private static ResultBean uploadFileByHuaweiObject(MultipartFile file, String huaweiEndPoint,String huaweiobsAk,String huaweiobsSk,
String bucketName){
String fileName = file.getOriginalFilename();
fileName = renameToUUID(fileName);
InputStream inputStream=null;
try {
inputStream=file.getInputStream();
// 建立ObsClient例項
ObsClient obsClient = new ObsClient(huaweiobsAk, huaweiobsSk, huaweiEndPoint);
PutObjectResult result = obsClient.putObject(bucketName, fileName, inputStream);
obsClient.close();
return ResultBean.success(fileName);
}catch (ObsException e){
log.info("[ERROR][uploadFileByHuaweiObject]"+e.getErrorMessage());
return ResultBean.error(e.getErrorMessage());
}catch (Exception e){
log.info("[ERROR][uploadFileByHuaweiObject]"+e.getMessage());
return ResultBean.error(e.getMessage());
}finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 上傳檔案原本方法
* @param file 檔名
* @return
*/
private static ResultBean uploadFileByOrigin(MultipartFile file,String uploadPath){
String fileName = file.getOriginalFilename();
fileName = renameToUUID(fileName);
File targetFile = new File(uploadPath);
if (!targetFile.exists()) {
targetFile.mkdirs();
}
FileOutputStream out = null;
try {
out = new FileOutputStream(uploadPath + fileName);
out.write(file.getBytes());
} catch (IOException e) {
log.info("[ERROR][uploadFileByOrigin]"+e.getMessage());
return ResultBean.error(e.getMessage());
}finally {
try {
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return ResultBean.success(fileName);
}
/**
* 下載
* @return
*/
public static byte[] downloadFile(String fileId,String uploadFlag,String uploadPath
,FastFileStorageClient fastFileStorageClient, String group,
String endPoint,String ak,String sk,
String bucketName) {
byte[] result=null;
switch (uploadFlag){
case "fastDFS":
result =downloadFileByFastDFS(fileId,fastFileStorageClient,group);
break;
case "huaweiOOS":
result =downloadFileByHuaweiObject(fileId, endPoint, ak, sk, bucketName);
break;
case "server":
default:
String path2 = uploadPath + fileId;
path2 = path2.replace("//", "/");
result=downloadFileByOrigin(path2);
break;
}
return result;
}
/**
* 下載檔案fastDFS
* @param fileId 檔名
* @return
*/
private static byte[] downloadFileByFastDFS(String fileId,FastFileStorageClient fastFileStorageClient,
String group){
DownloadByteArray callback=new DownloadByteArray();
byte[] group1s=null;
try {
group1s = fastFileStorageClient.downloadFile(group, fileId, callback);
}catch (Exception e){
log.info("[ERROR][downloadFileByFastDFS]"+e.getMessage());
}
return group1s;
}
/**
* 下載檔案物件儲存
* @param fileId 檔名
* @return
*/
private static byte[] downloadFileByHuaweiObject(String fileId, String huaweiEndPoint,String huaweiobsAk,String huaweiobsSk,
String bucketName){
byte[] bytes =null;
try {
// 建立ObsClient例項
ObsClient obsClient = new ObsClient(huaweiobsAk, huaweiobsSk, huaweiEndPoint);
// 構造GetObjectRequest請求
GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, fileId);
// 執行下載操作
ObsObject obsObject = obsClient.getObject(getObjectRequest);
bytes = inputStreamToByteArray(obsObject.getObjectContent());
// 關閉OBS客戶端
obsClient.close();
return bytes;
}catch (ObsException e){
log.info("[ERROR][downloadFileByHuaweiObject]"+e.getErrorMessage());
}catch (Exception e) {
log.info("[ERROR][downloadFileByHuaweiObject]"+e.getMessage());
}
return bytes;
}
/**
*
* @param input
* @return
* @throws IOException
*/
private static byte[] inputStreamToByteArray(InputStream input) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
}
return output.toByteArray();
}
/**
* 下載檔案
* @param fileId 檔名
* @return
*/
private static byte[] downloadFileByOrigin(String fileId){
File file =new File(fileId);
InputStream inputStream=null;
byte[] buff = new byte[1024];
byte[] result=null;
BufferedInputStream bis = null;
ByteArrayOutputStream os = null;
try {
os=new ByteArrayOutputStream();
bis = new BufferedInputStream(new FileInputStream(file));
int i = bis.read(buff);
while (i != -1) {
os.write(buff, 0, buff.length);
i = bis.read(buff);
}
result=os.toByteArray();
os.flush();
} catch (Exception e) {
log.info("[ERROR][downloadFile]"+e.getMessage());
}finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}
/**
* 刪除檔案
* @param fileId 檔案ID
* @return 刪除失敗返回-1,否則返回0
*/
public static boolean deleteFile(String fileId,String fastDFSFlag,FastFileStorageClient fastFileStorageClient,
String group,String uploadPath) {
boolean result=false;
if (StringUtils.isNotBlank(fastDFSFlag)&&
fastDFSFlag.trim().equalsIgnoreCase("true")){
result =deleteFileByFastDFS(fileId,fastFileStorageClient,group);
}else {
String path2 = uploadPath + fileId;
path2 = path2.replace("//", "/");
result=deleteByOrigin(path2);
}
return result;
}
private static boolean deleteByOrigin(String fileName) {
File file = new File(fileName);
// 如果檔案路徑所對應的檔案存在,並且是一個檔案,則直接刪除
if (file.exists() && file.isFile()) {
if (file.delete()) {
return true;
} else {
return false;
}
} else {
return false;
}
}
private static boolean deleteFileByFastDFS(String fileId,FastFileStorageClient fastFileStorageClient,
String group) {
try {
String groupFieId=group+"/"+fileId;
StorePath storePath = StorePath.praseFromUrl(groupFieId);
fastFileStorageClient.deleteFile(storePath.getGroup(), storePath.getPath());
} catch (Exception e) {
log.info("[ERROR][deleteFileByFastDFS]"+e.getMessage());
return false;
}
return true;
}
/**
* 生成fileId
* @param fileName
* @return
*/
private static String renameToUUID(String fileName) {
return UUID.randomUUID() + "." + fileName.substring(fileName.lastIndexOf(".") + 1);
}
/**
* 刪除檔案物件儲存
* @param fileId 檔名
* @return
*/
private static boolean deleteFileByHuaweiObject(String fileId, String huaweiEndPoint,String huaweiobsAk,String huaweiobsSk,
String bucketName){
try {
// 建立ObsClient例項
ObsClient obsClient = new ObsClient(huaweiobsAk, huaweiobsSk, huaweiEndPoint);
// 構造GetObjectRequest請求
DeleteObjectRequest getObjectRequest = new DeleteObjectRequest(bucketName, fileId);
// 執行刪除操作
obsClient.deleteObject(getObjectRequest);
// 關閉OBS客戶端
obsClient.close();
}catch (ObsException e){
log.info("[ERROR][deleteFileByHuaweiObject]"+e.getErrorMessage());
}catch (Exception e) {
log.info("[ERROR][downloadFileByHuaweiObject]"+e.getMessage());
}
return true;
}
/**
* 檔案資料輸出(image)
* @param fileId
* @param bytes
* @param res
*/
public static void fileDataOut(String fileId, byte[] bytes, HttpServletResponse res){
String[] prefixArray = fileId.split("\\.");
String prefix=prefixArray[1];
File file =new File(fileId);
res.reset();
res.setCharacterEncoding("utf-8");
// res.setHeader("content-type", "");
res.addHeader("Content-Length", "" + bytes.length);
if(prefix.equals("svg"))
prefix ="svg+xml";
res.setContentType("image/"+prefix);
res.setHeader("Accept-Ranges","bytes");
OutputStream os = null;
try {
// os = res.getOutputStream();
// os.write(bytes);
// os.flush();
res.getOutputStream().write(bytes);
} catch (IOException e) {
System.out.println("not find img..");
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
參考資料
假設個人實戰使用可以採用docker快速安裝,但是未安裝過FastDFS建議普通安裝部署,瞭解一下tracker和storage的使用,以及部署搭建叢集。
FastDFS普通安裝部署:https://www.cnblogs.com/chenliugou/p/15322389.html
FasdDFS docker安裝部署:https://blog.csdn.net/weixin_44621343/article/details/117825755?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_baidulandingword~default-0-117825755-blog-127896984.235v43pc_blog_bottom_relevance_base6&spm=1001.2101.3001.4242.1&utm_relevant_index=3
OpenFeign和FastDFS的類衝突:https://www.cnblogs.com/chenliugou/p/18113183
Gitee地址:https://gitee.com/chen-liugou/file/new/master?readme=true