前言:公司要做一个兼容本地存储和minio存储并可配置的静态资源保存,资源包括网站的图片和文档等
minio以前是用的6.0的jar包 但是这次从网上下载的minio的服务端是最新版,服务端启动之后有两个两个端口号, api的9000 和 console的动态端口号(具体的安装教程网上一大把,这里要说的是你如果用的是APi调用的接口,比如说java调用, 就需要使用9000这个端口) 所以 6.0版本的有点不太适用,所以改成8.0的jar包,没有仔细去研究版本升级之后的差异点,但是感觉8.0版本 1是规范了api的参数 ,针对不同的api参数使用了内部类的模式可以链式调用,变成各种args.build().bucket().build,还有一些可以为null的参数,现在也必须有值,比如putObject中的ObjectSize 。
话不多说 ,上代码吧
配置文件
1 本地存储
@Service @ConditionalOnProperty(value = {"file.store.type"},havingValue = "local") public class MdFileStoreServiceImpl implements ObjectStoreService { private static final Logger logger = LoggerFactory.getLogger(MdFileStoreServiceImpl.class); @Value("${file.absolutepath}") private String absolutePath; @Value("${file.imageUrlPrefix}") private String imageUrlPrefix; @Resource private MdFileStoreInfoMapper mdFileStoreInfoMapper; @Override public String uploadFile(String bucket, String fileName, InputStream in, Long size, String mime, Boolean autoMatch) throws ServiceException { return null; } @Override public void downloadFile(String bucket, String remoteFileName, String localFileName) throws ServiceException { } @Override public String getDownloadLink(String bucket, String fileName, Integer expires) throws ServiceException { return null; } @Override public void batchDeleteFile(String bucket, String prefix) throws ServiceException { } @Override public String getPublicLink(String bucket, String fileName) throws ServiceException { return null; } @Override public MdFileStoreInfo saveResourceFile(final MdFileStoreInfo mdFileStoreInfo, final InputStream in, final String productNo) throws ServiceException { FileOutputStream fos = null; final InputStream fin = null; String folder = productNo + DateUtil.date2StrByPattern(new Date(), DateUtil.DATE_FORMAT_STR); MdFileStoreInfo mdExist = this.mdFileStoreInfoMapper.selectByFolder(folder); while (mdExist != null) { // 如果该文件夹已存在,则更换,直到不存在为止 folder = productNo + DateUtil.date2StrByPattern(new Date(), DateUtil.DATE_FORMAT_STR); mdExist = this.mdFileStoreInfoMapper.selectByFolder(folder); } mdFileStoreInfo.setFileFolder(folder); try { // 保存到本地 final String tempDir = FileUtil.getTempDir(); final String zipFileName = tempDir + mdFileStoreInfo.getFileOrgName(); fos = new FileOutputStream(zipFileName); int index; final byte[] bytes = new byte[1024]; while ((index = in.read(bytes)) != -1) { fos.write(bytes, 0, index); } fos.flush(); // 解压zip文件到指定的文件夹 final String destDir = this.absolutePath + File.separator + folder + File.separator; mdFileStoreInfo.setFileAddress(destDir); logger.info("zipFileName:" + zipFileName); logger.info("destDir:" + destDir); ZipUtil.unZip(zipFileName, destDir); // 检查格式 final File unzipDir = new File(destDir); final String[] files = unzipDir.list(); if (files.length == 0) { ZipUtil.deleteFiles(unzipDir); throw new ServiceException("压缩包内容为空"); } int count = 0; for (final String fn : files) { if ("index.md".equalsIgnoreCase(fn)) { count++; } } if (count == 0) { ZipUtil.deleteFiles(unzipDir); throw new ServiceException("压缩包内未包含index.md文件"); } else if (count > 1) { ZipUtil.deleteFiles(unzipDir); throw new ServiceException("压缩包内包含多个index.md文件"); } /* //该种方式使用jsch的ssh2 但是访问被拒绝 内网内两个服务所在的机器无法通过ssh命令进行跳转 行不通 ScpConnectionEntity scpConnectionEntity = new ScpConnectionEntity(); scpConnectionEntity.setIp("192.168.0.237"); scpConnectionEntity.setPort(51888); //scpConnectionEntity.setPassword(""); scpConnectionEntity.setUserName("root"); fileSynchExecutorSupport.addTask(new FileSynchTask(unzipDir,scpConnectionEntity)); */ this.mdFileStoreInfoMapper.insert(mdFileStoreInfo); return mdFileStoreInfo; } catch (final ServiceException e) { throw e; } catch (final Exception e) { logger.error("文件上传失败,mdFileStoreInfo=", mdFileStoreInfo, e); throw new ServiceException("文件上传失败"); } finally { if (fos != null) { try { fos.close(); } catch (final IOException e) { logger.error("fos流关闭失败", e); } } if (fin != null) { try { fin.close(); } catch (final IOException e) { logger.error("fin流关闭失败", e); } } } } @Override public String saveImageFile(final MultipartFile file, final String fileFolder) throws ServiceException { final String imageUrl = this.imageUrlPrefix + File.separator + fileFolder + File.separator; final String imageFilename = DateUtil.date2StrByPattern(new Date(), DateUtil.DATE_FORMAT_STR) + "." + FileUtil.cutSuffix(file.getOriginalFilename()); final String imagePath = this.absolutePath + File.separator + fileFolder + File.separator + imageFilename; try { final File f = new File(imagePath); if (!f.getParentFile().exists()) { f.getParentFile().mkdir(); } file.transferTo(f); return imageUrl + f.getName(); } catch (final IOException e) { logger.error("图片上传失败,fileFolder=", fileFolder, e); throw new ServiceException("图片上传失败!"); } } @Override public void deleteImageFile(String imgUrl) throws ServiceException { logger.info("要删除的图片路径:" + imgUrl); String resourcesFilePath =""; if(imgUrl.contains("/static/")){ resourcesFilePath = imgUrl.replace("/static/",this.absolutePath + File.separator); }else if(imgUrl.startsWith("/")){ resourcesFilePath = this.absolutePath + imgUrl; }else{ resourcesFilePath = this.absolutePath + File.separator + imgUrl; } final File f = new File(resourcesFilePath); if(f.exists()){ ZipUtil.deleteFiles(f); } logger.info("文件路径:" + resourcesFilePath + "的图片已删除"); } @Override public void deleteBucket(String bucketName) throws ServiceException { } }
2 minio存储
@Service @ConditionalOnProperty(value = {"file.store.type"},havingValue = "minio") public class MinioObjectStoreServiceImpl implements ObjectStoreService { private static final Logger logger = LoggerFactory.getLogger(MinioObjectStoreServiceImpl.class); @Value("${file.absolutepath}") private String absolutePath; @Value("${file.imageUrlPrefix}") private String imageUrlPrefix; private String url; private String accessKey; private String secretKey; @Resource private MdFileStoreInfoMapper mdFileStoreInfoMapper; Set<String> set = new HashSet<>(); Map<String, String> map = new HashMap<>(); { //otf, css, ico, svg, ttf, png, js, json, html, woff2, eot, woff map.put("otf","application/x-font-otf"); map.put("css","text/css"); map.put("ico","image/x-icon"); map.put("svg","text/xml"); map.put("ttf","application/octet-stream"); map.put("png","image/png"); map.put("jpe","image/jpeg"); map.put("jpeg","image/jpeg"); map.put("jpg","image/jpeg"); map.put("js","application/x-javascript"); map.put("html","text/html"); map.put("woff2","application/x-font-woff"); map.put("eot","application/octet-stream"); map.put("woff","application/x-font-woff"); map.put("zip","application/zip"); map.put("md","text/x-markdown"); } private MinioClient minioClient; public MinioObjectStoreServiceImpl(@Value("${minio.url}")String url,@Value("${minio.accesskey}")String accessKey,@Value("${minio.secretKey}")String secretKey ){ //初始化minio this.url=url; this.accessKey=accessKey; this.secretKey=secretKey; init(); } private void init(){ logger.info("初始化Minio......"); try { minioClient = MinioClient.builder().endpoint(url) .credentials(accessKey, secretKey) .build(); } catch (Exception e) { logger.error("初始化Minio失败:", e); throw new RuntimeException("Minio客户端初始化失败"); } } @Override public String uploadFile(String bucket, String fileName, InputStream in, Long size, String mime, Boolean autoMatch) throws ServiceException { try { if(StringUtil.isBlank(mime) && autoMatch){ mime = map.get(FileUtil.cutSuffix(fileName)); } logger.info("文件类型为:{}",mime); checkBucket(bucket,true); minioClient.putObject(PutObjectArgs.builder().bucket(bucket).contentType(mime).stream(in,size,PutObjectArgs.MIN_MULTIPART_SIZE).object(fileName).build() ); if(in != null){ try { in.close(); } catch (IOException e) { logger.error("Minio关闭流异常,file=", fileName, e); } } return "/"+bucket+"/"+fileName; } catch (Exception e) { logger.error("上传文件失败,fileName={}", fileName, e); throw new ServiceException("上传文件失败"); } } @Override public void downloadFile(String bucket, String remoteFileName, String localFileName) throws ServiceException { FileOutputStream fos = null; InputStream in = null; try { fos = new FileOutputStream(localFileName); in = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(remoteFileName).build()); int index; byte[] bytes = new byte[1024]; //一个一个字节的读取并写入 while((index = in.read(bytes)) != -1) { fos.write(bytes, 0, index); } fos.flush(); } catch (Exception e) { logger.error("Minio下载文件失败,remoteFileName=", remoteFileName, e); throw new ServiceException("Minio下载文件失败"); } finally { if(fos != null){ try { fos.close(); } catch (IOException e) { logger.error("os流关闭失败", e); } } if(in != null){ try { in.close(); } catch (IOException e) { logger.error("in流关闭失败", e); } } } } @Override public String getDownloadLink(String bucket, String fileName, Integer expires) throws ServiceException{ try { return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().bucket(bucket).expiry(expires).object(fileName).build() ); } catch (Exception e) { logger.error("Minio获取文件下载链接失败,fileName=", fileName, e); throw new ServiceException("Minio获取文件下载链接失败"); } } public String getPolicyStr(String bucketName){ /*利用getBucketPolicy 在控制台设置创建一个bucket并设置访问策略以后获取到的 {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:GetBucketLocation","s3:ListBucket","s3:ListBucketMultipartUploads"],"Resource":["arn:aws:s3:::portalstaticbucket"]},{"Effect":"Allow","Principal":{"AWS":["*"]},"Action":["s3:AbortMultipartUpload","s3:DeleteObject","s3:GetObject","s3:ListMultipartUploadParts","s3:PutObject"],"Resource":["arn:aws:s3:::portalstaticbucket/*"]}]}*/ StringBuilder builder = new StringBuilder(); builder.append("{"); builder.append("\"Version\":\"2012-10-17\","); builder.append("\"Statement\":["); builder.append("{"); builder.append("\"Effect\":\"Allow\","); builder.append("\"Principal\":{\"AWS\":[\"*\"]},"); builder.append("\"Action\":["); builder.append("\"s3:GetBucketLocation\","); builder.append("\"s3:ListBucket\","); builder.append("\"s3:ListBucketMultipartUploads\""); builder.append("],"); builder.append("\"Resource\":[\"arn:aws:s3:::"); builder.append(bucketName); builder.append("\"]},"); builder.append("{"); builder.append("\"Effect\":\"Allow\","); builder.append("\"Principal\":{\"AWS\":[\"*\"]},"); builder.append("\"Action\":["); builder.append("\"s3:AbortMultipartUpload\","); builder.append("\"s3:DeleteObject\","); builder.append("\"s3:GetObject\","); builder.append("\"s3:ListMultipartUploadParts\","); builder.append("\"s3:PutObject\""); builder.append("],"); builder.append("\"Resource\":[\"arn:aws:s3:::"); builder.append(bucketName); builder.append("/*\"]"); builder.append("}"); builder.append("]"); builder.append("}"); //builder.append("{\"Version\": \"2012-10-17\", \"Statement\": [{ \"Effect\": \"Allow\", \"Action\": [ \"s3:*\"], \"Resource\": [ \"arn:aws:s3:::*\"] }] }"); return builder.toString(); } public boolean checkBucket(String bucketName,Boolean create) throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException { logger.info("桶名称:{}",bucketName); boolean exist = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); if(!exist && create){ //创建bucket minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); //设置访问策略 minioClient.setBucketPolicy(SetBucketPolicyArgs.builder().bucket(bucketName).config(getPolicyStr(bucketName)).build()); exist = true; } return exist; } public void batchUpload(String bucket, String path) throws ServiceException { try { checkBucket(bucket,true); File file = new File(path); System.out.println(file.getName()); File[] files = file.listFiles(); for(File item:files){ uploadFile(bucket, item, ""); } logger.info("Suffix set:", set); } catch (Exception e) { logger.error("Minio批量上传文件失败,path={}", path, e); throw new ServiceException("Minio批量上传文件失败"); } } private void uploadFile(String bucket, File file, String parentPath) throws Exception{ if(file.isDirectory()){ File[] files = file.listFiles(); for (File item:files) { System.out.println(item.getName()); uploadFile(bucket, item, parentPath+file.getName()+"/"); } }else{ InputStream in = new FileInputStream(file); logger.info("文件名称:{}",file.getName()); logger.info("content-type名称:{}",map.get(FileUtil.cutSuffix(file.getName()))); String contentType = map.get(FileUtil.cutSuffix(file.getName())) != null ? map.get(FileUtil.cutSuffix(file.getName())) : "application/octet-stream"; minioClient.putObject(PutObjectArgs.builder().bucket(bucket).contentType(contentType).stream(in,file.length(),PutObjectArgs.MIN_MULTIPART_SIZE).object(parentPath+file.getName()).build() ); if(in != null){ try { in.close(); } catch (IOException e) { logger.error("Minio关闭流异常,file=", file.getName(), e); } } } } @Override public void batchDeleteFile(String bucket, String prefix) throws ServiceException{ try { Iterable<Result<Item>> re = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucket).prefix(prefix).build() ); Iterator<Result<Item>> reIterator = re.iterator(); List<DeleteObject> list = new ArrayList<>(); logger.info("批量删除开始"); while(reIterator.hasNext()){ String obname = reIterator.next().get().objectName(); if(obname.endsWith("/") || obname.endsWith(".")){ batchDeleteFile(bucket,obname); } logger.info("对象名称;{}",obname); DeleteObject deleteObject = new DeleteObject(obname); list.add(deleteObject); } Iterable<Result<DeleteError>> deleleResult = minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucket).objects(list).build() ); if(deleleResult != null){ Iterator<Result<DeleteError>> deleleResultIterator = deleleResult.iterator(); while(deleleResultIterator.hasNext()){ String obName = deleleResultIterator.next().get().objectName(); System.out.println(obName); logger.error("Minio批量删除出现错误,obname={}", obName); } } } catch (Exception e) { logger.error("Minio批量删除失败:{}", bucket, prefix, e); throw new RuntimeException("Minio批量删除失败"); } } @Override public String getPublicLink(String bucket, String fileName) throws ServiceException { return this.url+"/"+bucket+"/"+fileName; } @Override public MdFileStoreInfo saveResourceFile(MdFileStoreInfo mdFileStoreInfo, InputStream in, String productNo) throws ServiceException { FileOutputStream fos = null; final InputStream fin = null; String folder = productNo + DateUtil.date2StrByPattern(new Date(), DateUtil.DATE_FORMAT_STR); MdFileStoreInfo mdExist = this.mdFileStoreInfoMapper.selectByFolder(folder); while (mdExist != null) { // 如果该文件夹已存在,则更换,直到不存在为止 folder = productNo + DateUtil.date2StrByPattern(new Date(), DateUtil.DATE_FORMAT_STR); mdExist = this.mdFileStoreInfoMapper.selectByFolder(folder); } mdFileStoreInfo.setFileFolder(folder); try { // 保存到本地 final String tempDir = FileUtil.getTempDir(); final String zipFileName = tempDir + mdFileStoreInfo.getFileOrgName(); fos = new FileOutputStream(zipFileName); int index; final byte[] bytes = new byte[1024]; while ((index = in.read(bytes)) != -1) { fos.write(bytes, 0, index); } fos.flush(); // 解压zip文件到指定的文件夹 final String destDir = this.absolutePath + File.separator + folder + File.separator; mdFileStoreInfo.setFileAddress(destDir); logger.info("zipFileName:" + zipFileName); logger.info("destDir:" + destDir); ZipUtil.unZip(zipFileName, destDir); // 检查格式 final File unzipDir = new File(destDir); final String[] files = unzipDir.list(); if (files.length == 0) { ZipUtil.deleteFiles(unzipDir); throw new ServiceException("压缩包内容为空"); } int count = 0; for (final String fn : files) { if ("index.md".equalsIgnoreCase(fn)) { count++; } } if (count == 0) { ZipUtil.deleteFiles(unzipDir); throw new ServiceException("压缩包内未包含index.md文件"); } else if (count > 1) { ZipUtil.deleteFiles(unzipDir); throw new ServiceException("压缩包内包含多个index.md文件"); } /* //该种方式使用jsch的ssh2 但是访问被拒绝 内网内两个服务所在的机器无法通过ssh命令进行跳转 行不通 ScpConnectionEntity scpConnectionEntity = new ScpConnectionEntity(); scpConnectionEntity.setIp("192.168.0.237"); scpConnectionEntity.setPort(51888); //scpConnectionEntity.setPassword(""); scpConnectionEntity.setUserName("root"); fileSynchExecutorSupport.addTask(new FileSynchTask(unzipDir,scpConnectionEntity)); */ //minio批量上传 batchUpload(folder,destDir); this.mdFileStoreInfoMapper.insert(mdFileStoreInfo); return mdFileStoreInfo; } catch (final ServiceException e) { throw e; } catch (final Exception e) { logger.error("文件上传失败,mdFileStoreInfo=", mdFileStoreInfo, e); throw new ServiceException("文件上传失败"); } finally { if (fos != null) { try { fos.close(); } catch (final IOException e) { logger.error("fos流关闭失败", e); } } if (fin != null) { try { fin.close(); } catch (final IOException e) { logger.error("fin流关闭失败", e); } } } } @Override public String saveImageFile(MultipartFile file, String fileFolder) throws ServiceException { try { String fileName = file.getOriginalFilename(); Long fileSize = file.getSize(); logger.info("文件名称:{}",fileName); InputStream in = file.getInputStream(); checkBucket(fileFolder,true); String url = uploadFile(fileFolder, fileName, in, fileSize, null, true); return url; } catch (Exception e) { logger.error("图片上传失败,fileFolder={}", fileFolder, e); throw new ServiceException("图片上传失败!"); } } @Override public void deleteImageFile(String imgUrl) throws ServiceException { logger.info("要删除的图片路径:" + imgUrl); String objectName; String bucketName; if(imgUrl.startsWith("/")){ objectName = imgUrl.substring(imgUrl.indexOf("/", 1) + 1); bucketName = imgUrl.substring(1,imgUrl.indexOf("/", 1)); }else{ objectName = imgUrl.substring(imgUrl.indexOf("/") + 1); bucketName = imgUrl.substring(0,imgUrl.indexOf("/")); } try { minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build()); logger.info("文件路径:" + imgUrl + "的图片已删除"); } catch (Exception e) { logger.error("图片删除失败,imgUrl=", imgUrl, e); throw new ServiceException("图片删除失败!"); } } @Override public void deleteBucket(String bucketName) throws ServiceException { try { boolean b = checkBucket(bucketName, false); if(b){ batchDeleteFile(bucketName,null); minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); } } catch (Exception e) { logger.error("bucket删除失败,bucketName={}", bucketName, e); throw new ServiceException("bucket删除失败!"); } } }