自己写个简易版 PicGo
自己写个简易版 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{
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);
}
@Override
public 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);
// 拼接 objectName
String 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
@NoArgsConstructor
public 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 {
@Test
public 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());
}
@Test
public 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 装饰 FileReader
try (
FileReader fr = new FileReader(destMdFilePath);
BufferedReader br = new BufferedReader(fr);
) {
// 当前行有内容
while ((curLine = br.readLine()) != null) {
// 图片路径存储格式:
// 正则表达式
/*
^$:匹配一行的开头和结尾
\[.*\]:![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 上是否已经有该图片的 URL
String 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.png
String 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
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>
还没有评论,来说两句吧...