自己写个简易版 PicGo

以你之姓@ 2023-02-26 12:29 69阅读 0赞

自己写个简易版 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 单元测试的依赖



    com.aliyun.oss
    aliyun-sdk-oss
    3.5.0




    commons-lang
    commons-lang
    2.6




    org.projectlombok
    lombok
    1.16.10




    junit
    junit
    4.0

2.2、创建 OSS 工具类

  • 准备工作:封装 ResultEntity 类,用于封装调用返回的信息

    • result :

      • SUCCESS :请求成功
      • FAILED :请求失败
    • message :请求处理失败时,返回的错误消息
    • data :请求返回的数据

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class ResultEntity {

  1. public static final String SUCCESS = "SUCCESS";
  2. public static final String FAILED = "FAILED";
  3. // 用来封装当前请求处理的结果是成功还是失败
  4. private String result;
  5. // 请求处理失败时返回的错误消息
  6. private String message;
  7. // 要返回的数据
  8. private T data;
  9. /**
  10. * 请求处理成功且不需要返回数据时使用的工具方法
  11. *
  12. * @return
  13. */
  14. public static <Type> ResultEntity<Type> successWithoutData() {
  15. return new ResultEntity<Type>(SUCCESS, null, null);
  16. }
  17. /**
  18. * 请求处理成功且需要返回数据时使用的工具方法
  19. *
  20. * @param data 要返回的数据
  21. * @return
  22. */
  23. public static <Type> ResultEntity<Type> successWithData(Type data) {
  24. return new ResultEntity<Type>(SUCCESS, null, data);
  25. }
  26. /**
  27. * 请求处理失败后使用的工具方法
  28. *
  29. * @param message 失败的错误消息
  30. * @return
  31. */
  32. public static <Type> ResultEntity<Type> failed(String message) {
  33. return new ResultEntity<Type>(FAILED, message, null);
  34. }
  35. @Override
  36. public String toString() {
  37. return "ResultEntity [result=" + result + ", message=" + message + ", data=" + data + "]";
  38. }
  39. }
  • 封装 OSS 工具类

    • uploadFileToOss() 方法:上传文件至 OSS 服务器
    • isFileExitsOnOSS() 方法:查看文件是否已经存在于 OSS 服务器

    /**

    • OSS 工具类
      */
      public class OSSUtil {
  1. /**
  2. * 上传文件至 OSS 服务器
  3. * @param endpoint OSS endpoint
  4. * @param accessKeyId OSS accessKeyId
  5. * @param accessKeySecret OSS accessKeySecret
  6. * @param bucketDomain OSS bucketDomain
  7. * @param bucketName OSS bucketName
  8. * @param inputStream 待上传文件的输入流对象
  9. * @param folderName OSS 上的文件夹路径(你要把文件存在那个文件夹找中)
  10. * @param originalName 文件的原始名称
  11. * @return 参考 ResultEntity
  12. */
  13. public static ResultEntity<String> uploadFileToOss(
  14. String endpoint,
  15. String accessKeyId,
  16. String accessKeySecret,
  17. String bucketDomain,
  18. String bucketName,
  19. InputStream inputStream,
  20. String folderName,
  21. String originalName) {
  22. // 创建OSSClient实例。
  23. OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
  24. // folderName + originalName 获得文件在 OSS 上的存储路径
  25. String objectName = folderName + "/" + originalName;
  26. try {
  27. // 调用OSS客户端对象的方法上传文件并获取响应结果数据
  28. PutObjectResult putObjectResult = ossClient.putObject(bucketName, objectName, inputStream);
  29. // 从响应结果中获取具体响应消息
  30. ResponseMessage responseMessage = putObjectResult.getResponse();
  31. // 根据响应状态码判断请求是否成功
  32. if (responseMessage == null) {
  33. // 获得刚刚上传的文件的路径
  34. String ossFileAccessPath = bucketDomain + "/" + objectName;
  35. // 当前方法返回成功
  36. return ResultEntity.successWithData(ossFileAccessPath);
  37. } else {
  38. // 获取响应状态码
  39. int statusCode = responseMessage.getStatusCode();
  40. // 如果请求没有成功,获取错误消息
  41. String errorMessage = responseMessage.getErrorResponseAsString();
  42. // 当前方法返回失败
  43. return ResultEntity.failed("当前响应状态码=" + statusCode + " 错误消息=" + errorMessage);
  44. }
  45. } catch (Exception e) {
  46. e.printStackTrace();
  47. // 当前方法返回失败
  48. return ResultEntity.failed(e.getMessage());
  49. } finally {
  50. if (ossClient != null) {
  51. // 关闭OSSClient。
  52. ossClient.shutdown();
  53. }
  54. }
  55. }
  56. /**
  57. * 查看文件是否已经存在于 OSS 服务器
  58. * @param endpoint OSS endpoint
  59. * @param accessKeyId OSS accessKeyId
  60. * @param accessKeySecret OSS accessKeySecret
  61. * @param bucketName OSS bucketName
  62. * @param folderName 文件夹路径
  63. * @param fileName 文件 file 对象
  64. * @return true:存在;false:不存在
  65. */
  66. public static Boolean isFileExitsOnOSS(
  67. String endpoint,
  68. String accessKeyId,
  69. String accessKeySecret,
  70. String bucketName,
  71. String folderName,
  72. String fileName
  73. ) {
  74. // 创建OSSClient实例。
  75. OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
  76. // 拼接 objectName
  77. String objectName = folderName + "/" + fileName;
  78. // 是否找到文件
  79. boolean isFound = false;
  80. try {
  81. // 判断文件是否存在。doesObjectExist还有一个参数isOnlyInOSS,如果为true则忽略302重定向或镜像;如果为false,则考虑302重定向或镜像。
  82. isFound = ossClient.doesObjectExist(bucketName, objectName);
  83. } catch (Exception e) {
  84. e.printStackTrace();
  85. // 抛异常则认为没找到
  86. isFound = false;
  87. } finally {
  88. // 关闭OSSClient。
  89. ossClient.shutdown();
  90. }
  91. // 返回查询结果
  92. return isFound;
  93. }
  94. }

2.3、OSS 配置类

  • 封装 OSS 配置类,对应于编程连接阿里云 OSS 所需要的配置信息

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class OSSConfig {

  1. private String endPoint;
  2. private String bucketName;
  3. private String accessKeyId;
  4. private String accessKeySecret;
  5. private String bucketDomain;
  6. public static OSSConfig getOSSConfig(){
  7. return new OSSConfig(
  8. "<输入你的 endpoint>",
  9. "<输入你的 bucketName>",
  10. "<输入你的 accessKeyId>",
  11. "<输入你的 accessKeySecret>",
  12. "<输入你的 bucketDomain>"
  13. );
  14. }
  15. }
  • 进行单元测试,单元测试均成功后,便可进行主逻辑的编写

    public class UnitTest {

  1. @Test
  2. public void testUploadFileToOss() throws FileNotFoundException {
  3. // 获取配置信息
  4. OSSConfig ossConfig = OSSConfig.getOSSConfig();
  5. // 创建输入流
  6. InputStream is = new FileInputStream("C:\\Users\\Heygo\\Desktop\\image-20200710163305710.png");
  7. // 执行 OSS 上传
  8. ResultEntity<String> resultEntity = OSSUtil.uploadFileToOss(
  9. ossConfig.getEndPoint(),
  10. ossConfig.getAccessKeyId(),
  11. ossConfig.getAccessKeySecret(),
  12. ossConfig.getBucketDomain(),
  13. ossConfig.getBucketName(),
  14. is,
  15. "Users/Heygo/Desktop",
  16. "image-20200710163305710.png"
  17. );
  18. // 输出上传结果
  19. System.out.println(resultEntity.getResult());
  20. }
  21. @Test
  22. public void testIsFileExitsOnOSS() {
  23. // 获取配置信息
  24. OSSConfig ossConfig = OSSConfig.getOSSConfig();
  25. // 判断文件是否存在
  26. Boolean isExist = OSSUtil.isFileExitsOnOSS(
  27. ossConfig.getEndPoint(),
  28. ossConfig.getAccessKeyId(),
  29. ossConfig.getAccessKeySecret(),
  30. ossConfig.getBucketName(),
  31. "Users/Heygo/Desktop/",
  32. "image-20200710163305710.png"
  33. );
  34. // 输出结果
  35. System.out.println(isExist);
  36. }
  37. }

2.4、Typora 图片上传至 OSS

大致思路:

  • 读取 MD 文件的每一行,判断当前行是否为图片标签:

    • 如果是图片标签,则需进行进一步判断,判断该图片标签是否引用了网络图片途径:

      • 如果当前图片标签已经引用了网络 URL ,则不需做处理
      • 如果当前图片标签引用了本地链接,则证明图片需要上传至 OSS服务器,先将图片上传至阿里云 OSS 服务器,并获得图片的网络 URL 地址,替换原来图片标签中的本地引用
    • 如果不是图片标签,那么我们无需处理
  • 最后将新的文件内容写回原文件,覆盖保存即可

    public class TyporaPicSyncToOSS {

  1. public static void main(String[] args) {
  2. // MD 文件路径
  3. String destMdFilePath;
  4. // 从命令行读取 MD 文件位置,否则使用默认值
  5. if (args == null || args.length == 0) {
  6. destMdFilePath = "C:\\Users\\Heygo\\Desktop\\Typora 瘦身.md";
  7. } else {
  8. destMdFilePath = args[0];
  9. }
  10. // 上传 Typora 中的图片至 OSS 服务器
  11. doPicSyncToOSS(destMdFilePath);
  12. }
  13. /**
  14. * 上传 Typora 中的图片至 OSS 服务器
  15. *
  16. * @param destMdFilePath destMdFilePath
  17. */
  18. private static void doPicSyncToOSS(String destMdFilePath) {
  19. // MD 文件的 File 对象
  20. File destMdFile = new File(destMdFilePath);
  21. // 获取 MD 文件对应的 assets 文件夹
  22. // MD 文件所在目录
  23. String mdFileParentDir = destMdFile.getParent();
  24. // MD 文件名
  25. String mdFileName = destMdFile.getName();
  26. // 不带扩展名的 MD 文件名
  27. String mdFileNameWithoutExt = mdFileName.substring(0, mdFileName.lastIndexOf("."));
  28. // 拼接得到 assets 文件夹的路径
  29. String assetsAbsolutePath = mdFileParentDir + "\\" + mdFileNameWithoutExt + ".assets";
  30. // assets 目录的 File 对象
  31. File assetsFile = new File(assetsAbsolutePath);
  32. // 获取 assets 文件夹中的所有图片
  33. File[] allPicFiles = assetsFile.listFiles();
  34. // 将 Typora 中本地图片链接修改为 URL 链接
  35. String mdFileContent = changeLocalReferToUrlRefer(destMdFilePath, allPicFiles);
  36. // 执行保存(覆盖原文件)
  37. SaveMdContentToFile(destMdFilePath, mdFileContent);
  38. }
  39. /**
  40. * 将 Typora 中本地图片链接修改为 URL 链接
  41. *
  42. * @param destMdFilePath MD 文件的路径
  43. * @param allPicFiles 所有的本地图片的 file 数组
  44. * @return 修改图片路径后的 MD 文件内容
  45. */
  46. private static String changeLocalReferToUrlRefer(String destMdFilePath, File[] allPicFiles) {
  47. // 如果不是 MD 文件,滚蛋
  48. Boolean isMdFile = destMdFilePath.endsWith(".md");
  49. if (!isMdFile) {
  50. return "";
  51. }
  52. // 存储md文件内容
  53. StringBuilder sb = new StringBuilder();
  54. // 当前行内容
  55. String curLine;
  56. // 获取所有本地图片的名称
  57. List<String> allPicNames = new ArrayList<>();
  58. for (File curPicFile : allPicFiles) {
  59. // 获取图片名称
  60. String curPicName = curPicFile.getName();
  61. // 添加至集合中
  62. allPicNames.add(curPicName);
  63. }
  64. // 装饰者模式:FileReader 无法一行一行读取,所以使用 BufferedReader 装饰 FileReader
  65. try (
  66. FileReader fr = new FileReader(destMdFilePath);
  67. BufferedReader br = new BufferedReader(fr);
  68. ) {
  69. // 当前行有内容
  70. while ((curLine = br.readLine()) != null) {
  71. // 图片路径存储格式:![image-20200711220145723](https://heygo.oss-cn-shanghai.aliyuncs.com/Software/Typora/Typora_PicGo_CSDN.assets/image-20200711220145723.png)
  72. // 正则表达式
  73. /*
  74. ^$:匹配一行的开头和结尾
  75. \[.*\]:![image-20200711220145723]
  76. . :匹配任意字符
  77. * :出现0次或多次
  78. \(.+\):(https://heygo.oss-cn-shanghai.aliyuncs.com/Software/Typora/Typora_PicGo_CSDN.assets/image-20200711220145723.png)
  79. . :匹配任意字符
  80. + :出现1次或多次
  81. */
  82. String regex = "!\\[.*\\]\\(.+\\)";
  83. // 执行正则表达式
  84. Matcher matcher = Pattern.compile(regex).matcher(curLine);
  85. // 是否匹配到图片路径
  86. boolean isPicUrl = matcher.find();
  87. // 如果当前行是图片链接,干他
  88. if (isPicUrl) {
  89. // 检查图片是否已经是网络 URL 引用,如果已经是网络 URL 引用,则不需做任何操作
  90. Boolean isOSSUrl = curLine.contains("http://") || curLine.contains("https://");
  91. if (!isOSSUrl) {
  92. // 提取图片路径前面不变的部分
  93. Integer preStrEndIndex = curLine.indexOf("(");
  94. String preStr = curLine.substring(0, preStrEndIndex + 1);
  95. // 获取图片名称
  96. Integer picNameStartIndex = curLine.lastIndexOf("/");
  97. Integer curLineLength = curLine.length();
  98. String picName = curLine.substring(picNameStartIndex + 1, curLineLength - 1);
  99. // 拿到 URl 在 List 中的索引
  100. Integer picIndex = allPicNames.indexOf(picName);
  101. // 如果图片是真实存在于本地磁盘上的
  102. if (picIndex != -1) {
  103. // 拿到待上传的图片 File 对象
  104. File needUploadPicFile = allPicFiles[picIndex];
  105. // 检查 OSS 上是否已经有该图片的 URL
  106. String picOSSUrl = findPicOnOSS(needUploadPicFile);
  107. // 在 OSS 上找不到才执行上传
  108. if (picOSSUrl == "") {
  109. // 执行上传
  110. picOSSUrl = uploadPicToOSS(needUploadPicFile);
  111. }
  112. // 拼接得到 typora 中的图片链接
  113. curLine = preStr + picOSSUrl + ")";
  114. // 打印输出日志
  115. System.out.println("修改图片连接:" + curLine);
  116. }
  117. }
  118. }
  119. sb.append(curLine + "\r\n");
  120. }
  121. // 返回 MD 文件内容
  122. return sb.toString();
  123. } catch (IOException e) {
  124. e.printStackTrace();
  125. return "";
  126. }
  127. }
  128. /**
  129. * 上传文件至 OSS 服务器
  130. * @param curPicFile 当上传文件的 File 对象
  131. * @return 空串:上传失败;非空串:上传成功后,文件对应的 URL
  132. */
  133. private static String uploadPicToOSS(File curPicFile) {
  134. // 目录名称,注意:不建议使用特殊符号和中文,会进行 URL 编码
  135. String folderName = "images";
  136. // 获取 OSS 配置信息
  137. OSSConfig ossConfig = OSSConfig.getOSSConfig();
  138. // 执行上传
  139. InputStream is = null;
  140. try {
  141. // 文件输入流
  142. is = new FileInputStream(curPicFile);
  143. // 文件名称
  144. String curFileName = curPicFile.getName();
  145. // 执行上传
  146. ResultEntity<String> resultEntity = OSSUtil.uploadFileToOss(
  147. ossConfig.getEndPoint(),
  148. ossConfig.getAccessKeyId(),
  149. ossConfig.getAccessKeySecret(),
  150. ossConfig.getBucketDomain(),
  151. ossConfig.getBucketName(),
  152. is,
  153. folderName,
  154. curFileName
  155. );
  156. if (ResultEntity.SUCCESS.equals(resultEntity.getResult())) {
  157. // 上传成功:URL 格式:http://heygo.oss-cn-shanghai.aliyuncs.com/images/2020-07-12_204547.png
  158. String url = resultEntity.getData();
  159. return url;
  160. } else {
  161. // 上传失败,返回空串
  162. System.out.println(resultEntity.getMessage());
  163. return "";
  164. }
  165. } catch (FileNotFoundException e) {
  166. // 发生异常也返回空串
  167. e.printStackTrace();
  168. return "";
  169. }
  170. }
  171. /**
  172. * 判断 OSS 服务器上是否已经有该文件
  173. * @param curPicFile file 对象
  174. * @return 空串:不存在;非空串:存在,返回值为文件对应的 URL
  175. */
  176. private static String findPicOnOSS(File curPicFile) {
  177. // 获取 OSS 配置信息
  178. OSSConfig ossConfig = OSSConfig.getOSSConfig();
  179. // 目录名称
  180. String folderName = "images";
  181. // 获取文件路径
  182. String fileName = curPicFile.getName();
  183. // 判断是否存在于 OSS 中
  184. Boolean isExist = OSSUtil.isFileExitsOnOSS(
  185. ossConfig.getEndPoint(),
  186. ossConfig.getAccessKeyId(),
  187. ossConfig.getAccessKeySecret(),
  188. ossConfig.getBucketName(),
  189. folderName,
  190. fileName
  191. );
  192. // 不存在返回空串
  193. if (!isExist) {
  194. return "";
  195. }
  196. // 拼接图片 URL 并返回
  197. String bucketDomain = ossConfig.getBucketDomain();
  198. String picUrl = bucketDomain + "/images/" + fileName;
  199. return picUrl;
  200. }
  201. /**
  202. *
  203. * @param destMdFilePath
  204. * @param mdFileContent
  205. */
  206. private static void SaveMdContentToFile(String destMdFilePath, String mdFileContent) {
  207. // 不保存空文件
  208. if (mdFileContent == null || mdFileContent == "") {
  209. return;
  210. }
  211. // 执行保存
  212. try (FileWriter fw = new FileWriter(destMdFilePath)) {
  213. fw.write(mdFileContent);
  214. } catch (IOException e) {
  215. e.printStackTrace();
  216. }
  217. }
  218. }

3、测试

3.1、测试结果

  • 程序运行结果:图片上传成功

image-20200715172210594

image-20200715172311084

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.

  1. [ErrorCode]: NoSuchKey
  2. [RequestId]: 5F0ECEFD999ED45736FFFA7C
  3. [HostId]: heygo.oss-cn-shanghai.aliyuncs.com
  4. [ResponseError]:
  5. <?xml version="1.0" encoding="UTF-8"?>
  6. <Error>
  7. <Code>NoSuchKey</Code>
  8. <Message>The specified key does not exist.</Message>
  9. <RequestId>5F0ECEFD999ED45736FFFA7C</RequestId>
  10. <HostId>heygo.oss-cn-shanghai.aliyuncs.com</HostId>
  11. <Key>images/image-20200715172311084.png</Key>
  12. </Error>

发表评论

表情:
评论列表 (有 0 条评论,69人围观)

还没有评论,来说两句吧...

相关阅读