自己写个简易版 PicGo
1、Why not PicGo?
- 不得不说,我被 PicGo 坑惨了,写了这么久的笔记,用了 PicGo 的一键上传,图片顺序全乱了。。。
- 我可真是老倒霉蛋了。。。我还是自己写个简易版的 PicGo 吧,用着放心一些~
2、Let’s do it
2.1、环境搭建
- 创建 Maven 工程,引入依赖:首先需要引入阿里云 OSS 客户端 SDK ,并引入 commons-lang 的依赖(阿里云 OSS 客户端需要依赖 commons-lang)
- Lombok 插件的依赖、Junit 单元测试的依赖
<dependencies><!-- OSS客户端SDK --><dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.5.0</version></dependency><!-- HttpUtil 工具类所需依赖 --><dependency><groupId>commons-lang</groupId><artifactId>commons-lang</artifactId><version>2.6</version></dependency><!-- Lombok 插件 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.16.10</version></dependency><!-- Junit --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.0</version></dependency></dependencies>
2.2、创建 OSS 工具类
- 准备工作:封装 ResultEntity 类,用于封装调用返回的信息result :SUCCESS :请求成功
- FAILED :请求失败
@Data@AllArgsConstructor@NoArgsConstructorpublic class ResultEntity<T> {public static final String SUCCESS = \"SUCCESS\";public static final String FAILED = \"FAILED\";// 用来封装当前请求处理的结果是成功还是失败private String result;// 请求处理失败时返回的错误消息private String message;// 要返回的数据private T data;/*** 请求处理成功且不需要返回数据时使用的工具方法** @return*/public static <Type> ResultEntity<Type> successWithoutData() {return new ResultEntity<Type>(SUCCESS, null, null);}/*** 请求处理成功且需要返回数据时使用的工具方法** @param data 要返回的数据* @return*/public static <Type> ResultEntity<Type> successWithData(Type data) {return new ResultEntity<Type>(SUCCESS, null, data);}/*** 请求处理失败后使用的工具方法** @param message 失败的错误消息* @return*/public static <Type> ResultEntity<Type> failed(String message) {return new ResultEntity<Type>(FAILED, message, null);}@Overridepublic String toString() {return \"ResultEntity [result=\" + result + \", message=\" + message + \", data=\" + data + \"]\";}}
- 封装 OSS 工具类uploadFileToOss() 方法:上传文件至 OSS 服务器
- isFileExitsOnOSS() 方法:查看文件是否已经存在于 OSS 服务器
/*** OSS 工具类*/public class OSSUtil {/*** 上传文件至 OSS 服务器* @param endpoint OSS endpoint* @param accessKeyId OSS accessKeyId* @param accessKeySecret OSS accessKeySecret* @param bucketDomain OSS bucketDomain* @param bucketName OSS bucketName* @param inputStream 待上传文件的输入流对象* @param folderName OSS 上的文件夹路径(你要把文件存在那个文件夹找中)* @param originalName 文件的原始名称* @return 参考 ResultEntity*/public static ResultEntity<String> uploadFileToOss(String endpoint,String accessKeyId,String accessKeySecret,String bucketDomain,String bucketName,InputStream inputStream,String folderName,String originalName) {// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);// folderName + originalName 获得文件在 OSS 上的存储路径String objectName = folderName + \"/\" + originalName;try {// 调用OSS客户端对象的方法上传文件并获取响应结果数据PutObjectResult putObjectResult = ossClient.putObject(bucketName, objectName, inputStream);// 从响应结果中获取具体响应消息ResponseMessage responseMessage = putObjectResult.getResponse();// 根据响应状态码判断请求是否成功if (responseMessage == null) {// 获得刚刚上传的文件的路径String ossFileAccessPath = bucketDomain + \"/\" + objectName;// 当前方法返回成功return ResultEntity.successWithData(ossFileAccessPath);} else {// 获取响应状态码int statusCode = responseMessage.getStatusCode();// 如果请求没有成功,获取错误消息String errorMessage = responseMessage.getErrorResponseAsString();// 当前方法返回失败return ResultEntity.failed(\"当前响应状态码=\" + statusCode + \" 错误消息=\" + errorMessage);}} catch (Exception e) {e.printStackTrace();// 当前方法返回失败return ResultEntity.failed(e.getMessage());} finally {if (ossClient != null) {// 关闭OSSClient。ossClient.shutdown();}}}/*** 查看文件是否已经存在于 OSS 服务器* @param endpoint OSS endpoint* @param accessKeyId OSS accessKeyId* @param accessKeySecret OSS accessKeySecret* @param bucketName OSS bucketName* @param folderName 文件夹路径* @param fileName 文件 file 对象* @return true:存在;false:不存在*/public static Boolean isFileExitsOnOSS(String endpoint,String accessKeyId,String accessKeySecret,String bucketName,String folderName,String fileName) {// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);// 拼接 objectNameString objectName = folderName + \"/\" + fileName;// 是否找到文件boolean isFound = false;try {// 判断文件是否存在。doesObjectExist还有一个参数isOnlyInOSS,如果为true则忽略302重定向或镜像;如果为false,则考虑302重定向或镜像。isFound = ossClient.doesObjectExist(bucketName, objectName);} catch (Exception e) {e.printStackTrace();// 抛异常则认为没找到isFound = false;} finally {// 关闭OSSClient。ossClient.shutdown();}// 返回查询结果return isFound;}}
2.3、OSS 配置类
- 封装 OSS 配置类,对应于编程连接阿里云 OSS 所需要的配置信息
@Data@AllArgsConstructor@NoArgsConstructorpublic class OSSConfig {private String endPoint;private String bucketName;private String accessKeyId;private String accessKeySecret;private String bucketDomain;public static OSSConfig getOSSConfig(){return new OSSConfig(\"<输入你的 endpoint>\",\"<输入你的 bucketName>\",\"<输入你的 accessKeyId>\",\"<输入你的 accessKeySecret>\",\"<输入你的 bucketDomain>\");}}
- 进行单元测试,单元测试均成功后,便可进行主逻辑的编写
public class UnitTest {@Testpublic void testUploadFileToOss() throws FileNotFoundException {// 获取配置信息OSSConfig ossConfig = OSSConfig.getOSSConfig();// 创建输入流InputStream is = new FileInputStream(\"C:\\\\Users\\\\Heygo\\\\Desktop\\\\image-20200710163305710.png\");// 执行 OSS 上传ResultEntity<String> resultEntity = OSSUtil.uploadFileToOss(ossConfig.getEndPoint(),ossConfig.getAccessKeyId(),ossConfig.getAccessKeySecret(),ossConfig.getBucketDomain(),ossConfig.getBucketName(),is,\"Users/Heygo/Desktop\",\"image-20200710163305710.png\");// 输出上传结果System.out.println(resultEntity.getResult());}@Testpublic void testIsFileExitsOnOSS() {// 获取配置信息OSSConfig ossConfig = OSSConfig.getOSSConfig();// 判断文件是否存在Boolean isExist = OSSUtil.isFileExitsOnOSS(ossConfig.getEndPoint(),ossConfig.getAccessKeyId(),ossConfig.getAccessKeySecret(),ossConfig.getBucketName(),\"Users/Heygo/Desktop/\",\"image-20200710163305710.png\");// 输出结果System.out.println(isExist);}}
2.4、Typora 图片上传至 OSS
大致思路:
- 读取 MD 文件的每一行,判断当前行是否为图片标签:如果是图片标签,则需进行进一步判断,判断该图片标签是否引用了网络图片途径:如果当前图片标签已经引用了网络 URL ,则不需做处理
- 如果当前图片标签引用了本地链接,则证明图片需要上传至 OSS服务器,先将图片上传至阿里云 OSS 服务器,并获得图片的网络 URL 地址,替换原来图片标签中的本地引用
public class TyporaPicSyncToOSS {public static void main(String[] args) {// MD 文件路径String destMdFilePath;// 从命令行读取 MD 文件位置,否则使用默认值if (args == null || args.length == 0) {destMdFilePath = \"C:\\\\Users\\\\Heygo\\\\Desktop\\\\Typora 瘦身.md\";} else {destMdFilePath = args[0];}// 上传 Typora 中的图片至 OSS 服务器doPicSyncToOSS(destMdFilePath);}/*** 上传 Typora 中的图片至 OSS 服务器** @param destMdFilePath destMdFilePath*/private static void doPicSyncToOSS(String destMdFilePath) {// MD 文件的 File 对象File destMdFile = new File(destMdFilePath);// 获取 MD 文件对应的 assets 文件夹// MD 文件所在目录String mdFileParentDir = destMdFile.getParent();// MD 文件名String mdFileName = destMdFile.getName();// 不带扩展名的 MD 文件名String mdFileNameWithoutExt = mdFileName.substring(0, mdFileName.lastIndexOf(\".\"));// 拼接得到 assets 文件夹的路径String assetsAbsolutePath = mdFileParentDir + \"\\\\\" + mdFileNameWithoutExt + \".assets\";// assets 目录的 File 对象File assetsFile = new File(assetsAbsolutePath);// 获取 assets 文件夹中的所有图片File[] allPicFiles = assetsFile.listFiles();// 将 Typora 中本地图片链接修改为 URL 链接String mdFileContent = changeLocalReferToUrlRefer(destMdFilePath, allPicFiles);// 执行保存(覆盖原文件)SaveMdContentToFile(destMdFilePath, mdFileContent);}/*** 将 Typora 中本地图片链接修改为 URL 链接** @param destMdFilePath MD 文件的路径* @param allPicFiles 所有的本地图片的 file 数组* @return 修改图片路径后的 MD 文件内容*/private static String changeLocalReferToUrlRefer(String destMdFilePath, File[] allPicFiles) {// 如果不是 MD 文件,滚蛋Boolean isMdFile = destMdFilePath.endsWith(\".md\");if (!isMdFile) {return \"\";}// 存储md文件内容StringBuilder sb = new StringBuilder();// 当前行内容String curLine;// 获取所有本地图片的名称List<String> allPicNames = new ArrayList<>();for (File curPicFile : allPicFiles) {// 获取图片名称String curPicName = curPicFile.getName();// 添加至集合中allPicNames.add(curPicName);}// 装饰者模式:FileReader 无法一行一行读取,所以使用 BufferedReader 装饰 FileReadertry (FileReader fr = new FileReader(destMdFilePath);BufferedReader br = new BufferedReader(fr);) {// 当前行有内容while ((curLine = br.readLine()) != null) {// 图片路径存储格式:![image-20200711220145723](https://heygo.oss-cn-shanghai.aliyuncs.com/Software/Typora/Typora_PicGo_CSDN.assets/image-20200711220145723.png)// 正则表达式/*^$:匹配一行的开头和结尾\\[.*\\]:![image-20200711220145723]. :匹配任意字符* :出现0次或多次\\(.+\\):(https://heygo.oss-cn-shanghai.aliyuncs.com/Software/Typora/Typora_PicGo_CSDN.assets/image-20200711220145723.png). :匹配任意字符+ :出现1次或多次*/String regex = \"!\\\\[.*\\\\]\\\\(.+\\\\)\";// 执行正则表达式Matcher matcher = Pattern.compile(regex).matcher(curLine);// 是否匹配到图片路径boolean isPicUrl = matcher.find();// 如果当前行是图片链接,干他if (isPicUrl) {// 检查图片是否已经是网络 URL 引用,如果已经是网络 URL 引用,则不需做任何操作Boolean isOSSUrl = curLine.contains(\"http://\") || curLine.contains(\"https://\");if (!isOSSUrl) {// 提取图片路径前面不变的部分Integer preStrEndIndex = curLine.indexOf(\"(\");String preStr = curLine.substring(0, preStrEndIndex + 1);// 获取图片名称Integer picNameStartIndex = curLine.lastIndexOf(\"/\");Integer curLineLength = curLine.length();String picName = curLine.substring(picNameStartIndex + 1, curLineLength - 1);// 拿到 URl 在 List 中的索引Integer picIndex = allPicNames.indexOf(picName);// 如果图片是真实存在于本地磁盘上的if (picIndex != -1) {// 拿到待上传的图片 File 对象File needUploadPicFile = allPicFiles[picIndex];// 检查 OSS 上是否已经有该图片的 URLString picOSSUrl = findPicOnOSS(needUploadPicFile);// 在 OSS 上找不到才执行上传if (picOSSUrl == \"\") {// 执行上传picOSSUrl = uploadPicToOSS(needUploadPicFile);}// 拼接得到 typora 中的图片链接curLine = preStr + picOSSUrl + \")\";// 打印输出日志System.out.println(\"修改图片连接:\" + curLine);}}}sb.append(curLine + \"\\r\\n\");}// 返回 MD 文件内容return sb.toString();} catch (IOException e) {e.printStackTrace();return \"\";}}/*** 上传文件至 OSS 服务器* @param curPicFile 当上传文件的 File 对象* @return 空串:上传失败;非空串:上传成功后,文件对应的 URL*/private static String uploadPicToOSS(File curPicFile) {// 目录名称,注意:不建议使用特殊符号和中文,会进行 URL 编码String folderName = \"images\";// 获取 OSS 配置信息OSSConfig ossConfig = OSSConfig.getOSSConfig();// 执行上传InputStream is = null;try {// 文件输入流is = new FileInputStream(curPicFile);// 文件名称String curFileName = curPicFile.getName();// 执行上传ResultEntity<String> resultEntity = OSSUtil.uploadFileToOss(ossConfig.getEndPoint(),ossConfig.getAccessKeyId(),ossConfig.getAccessKeySecret(),ossConfig.getBucketDomain(),ossConfig.getBucketName(),is,folderName,curFileName);if (ResultEntity.SUCCESS.equals(resultEntity.getResult())) {// 上传成功:URL 格式:http://heygo.oss-cn-shanghai.aliyuncs.com/images/2020-07-12_204547.pngString url = resultEntity.getData();return url;} else {// 上传失败,返回空串System.out.println(resultEntity.getMessage());return \"\";}} catch (FileNotFoundException e) {// 发生异常也返回空串e.printStackTrace();return \"\";}}/*** 判断 OSS 服务器上是否已经有该文件* @param curPicFile file 对象* @return 空串:不存在;非空串:存在,返回值为文件对应的 URL*/private static String findPicOnOSS(File curPicFile) {// 获取 OSS 配置信息OSSConfig ossConfig = OSSConfig.getOSSConfig();// 目录名称String folderName = \"images\";// 获取文件路径String fileName = curPicFile.getName();// 判断是否存在于 OSS 中Boolean isExist = OSSUtil.isFileExitsOnOSS(ossConfig.getEndPoint(),ossConfig.getAccessKeyId(),ossConfig.getAccessKeySecret(),ossConfig.getBucketName(),folderName,fileName);// 不存在返回空串if (!isExist) {return \"\";}// 拼接图片 URL 并返回String bucketDomain = ossConfig.getBucketDomain();String picUrl = bucketDomain + \"/images/\" + fileName;return picUrl;}/**** @param destMdFilePath* @param mdFileContent*/private static void SaveMdContentToFile(String destMdFilePath, String mdFileContent) {// 不保存空文件if (mdFileContent == null || mdFileContent == \"\") {return;}// 执行保存try (FileWriter fw = new FileWriter(destMdFilePath)) {fw.write(mdFileContent);} catch (IOException e) {e.printStackTrace();}}}
3、测试
3.1、测试结果
- 程序运行结果:图片上传成功
3.2、注意事项
- 如果图片第一次上传,会输出如下日志,经测试,为正常现象
- 程序在调用 ossClient.doesObjectExist(bucketName, objectName); 方法时如果找不到目标文件,就会输出如下日志,然后返回 false
- 如果找到目标文件,不会输出日志文件,直接返回 true
七月 15, 2020 5:35:40 下午 com.aliyun.oss logException信息: [Server]Unable to execute HTTP request: The specified key does not exist.[ErrorCode]: NoSuchKey[RequestId]: 5F0ECEFD999ED45736FFFA7C[HostId]: heygo.oss-cn-shanghai.aliyuncs.com[ResponseError]:<?xml version=\"1.0\" encoding=\"UTF-8\"?><Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><RequestId>5F0ECEFD999ED45736FFFA7C</RequestId><HostId>heygo.oss-cn-shanghai.aliyuncs.com</HostId><Key>images/image-20200715172311084.png</Key></Error>