Typora 瘦身 + 标题编号 + 图片同步
Typora 瘦身 + 标题编号 + 图片同步
1、Why???
- 之前使用
PicGo
插件上传图片至阿里云OSS
服务器,结果导致所有的图片顺序全部乱掉了,于是我直接疯掉了~~~一张一张,修改图片的链接 - 话说,咱又不是没那技术,自己写个小工具,还能拓展一些自己想要实现的功能,何乐而不为呢
- 说做就做,这个小工具我已经使用并测试了很久,应该没什么大问题,但是这篇博文因为种种原因,却耽搁了许久
- 项目 GitHub 地址:oneby1314/typora-tools
2、代码思路
2.1、图片瘦身
【图片瘦身的功能】参考我的这篇博文:typora 瘦身,该博文主要思想为:利用正则表达式匹配文中所用到的图片标签:![]()
,进而获取文中所引用的图片名称,然后与本地图片对比,删除本地的无用图片
2.2、标题编号
【标题自动编号功能】参考我的这篇博文:Typora 博文标题自动编号,该博文主要思想为:利用 Markdown
语法中,各级标题的 #
语法,实现对标题的自动编号功能
2.3、图片同步
【图片同步】参考我的这篇博文:自己写个简易版 PicGo,该博文主要思想为:利用正则表达式匹配文中所用到的图片标签:![]()
,如果匹配到的图片为本地链接,则将其上传到阿里云 OSS
服务器上,并修改图片路径为网络 URL
地址
2.4、思路总结
我编写的小工具是如上三个功能的汇总版本,提供了配置文件,用户可自行修改配置文件,选择自己需要的功能
3、代码实现
3.1、项目结构
项目结构如下
HttpUtils
:阿里云OSS
必须依赖于该HttpUtils
工具类ResultEntity
:实体类,用于封装请求的返回结果OSSConfig
:OSS
配置文件所对应的实体类TyporaToolConfig
:Typora
小工具配置文件所对应的实体类TyporaTools
:程序入口OSSUtil
:阿里云OSS
上传文件的工具类TyporaFileRwUtil
:Typora
文件读写工具类TyporaOSSPicSyncUtil
:图片同步至OSS
的工具类TyporaPicCleanUtil
:图片瘦身的工具类TyporaTiltleAutoNoUtil
:标题自动编号的工具类typora-tool.properties
:配置文件pom.xml
:pom
依赖
3.2、引入依赖
在 pom
文件中引入项目所需的依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.heygo.typora</groupId>
<artifactId>typora-tools</artifactId>
<version>1.0-SNAPSHOT</version>
<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>
</project>
3.3、引入 HttpUtils
阿里云 OSS
客户端依赖于 HttpUtils
工具类,我们在项目下创建此工具类,emmm
,这个工具类翻文档,就能在 GitHub
上找到对应的源代码
package com.aliyun.api.gateway.demo.util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
public class HttpUtils {
/**
* get
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doGet(String host, String path, String method, Map<String, String> headers,
Map<String, String> querys) throws Exception {
HttpClient httpClient = wrapClient(host);
HttpGet request = new HttpGet(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
/**
* post form
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param bodys
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers,
Map<String, String> querys, Map<String, String> bodys) throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (bodys != null) {
List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>();
for (String key : bodys.keySet()) {
nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key)));
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8");
formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8");
request.setEntity(formEntity);
}
return httpClient.execute(request);
}
/**
* Post String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers,
Map<String, String> querys, String body) throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Post stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers,
Map<String, String> querys, byte[] body) throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPost request = new HttpPost(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Put String
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers,
Map<String, String> querys, String body) throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (StringUtils.isNotBlank(body)) {
request.setEntity(new StringEntity(body, "utf-8"));
}
return httpClient.execute(request);
}
/**
* Put stream
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @param body
* @return
* @throws Exception
*/
public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers,
Map<String, String> querys, byte[] body) throws Exception {
HttpClient httpClient = wrapClient(host);
HttpPut request = new HttpPut(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
if (body != null) {
request.setEntity(new ByteArrayEntity(body));
}
return httpClient.execute(request);
}
/**
* Delete
*
* @param host
* @param path
* @param method
* @param headers
* @param querys
* @return
* @throws Exception
*/
public static HttpResponse doDelete(String host, String path, String method, Map<String, String> headers,
Map<String, String> querys) throws Exception {
HttpClient httpClient = wrapClient(host);
HttpDelete request = new HttpDelete(buildUrl(host, path, querys));
for (Map.Entry<String, String> e : headers.entrySet()) {
request.addHeader(e.getKey(), e.getValue());
}
return httpClient.execute(request);
}
private static String buildUrl(String host, String path, Map<String, String> querys)
throws UnsupportedEncodingException {
StringBuilder sbUrl = new StringBuilder();
sbUrl.append(host);
if (!StringUtils.isBlank(path)) {
sbUrl.append(path);
}
if (null != querys) {
StringBuilder sbQuery = new StringBuilder();
for (Map.Entry<String, String> query : querys.entrySet()) {
if (0 < sbQuery.length()) {
sbQuery.append("&");
}
if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) {
sbQuery.append(query.getValue());
}
if (!StringUtils.isBlank(query.getKey())) {
sbQuery.append(query.getKey());
if (!StringUtils.isBlank(query.getValue())) {
sbQuery.append("=");
sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8"));
}
}
}
if (0 < sbQuery.length()) {
sbUrl.append("?").append(sbQuery);
}
}
return sbUrl.toString();
}
private static HttpClient wrapClient(String host) {
HttpClient httpClient = new DefaultHttpClient();
if (host.startsWith("https://")) {
sslClient(httpClient);
}
return httpClient;
}
private static void sslClient(HttpClient httpClient) {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
X509TrustManager tm = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(X509Certificate[] xcs, String str) {
}
public void checkServerTrusted(X509Certificate[] xcs, String str) {
}
};
ctx.init(null, new TrustManager[] {
tm }, null);
SSLSocketFactory ssf = new SSLSocketFactory(ctx);
ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ClientConnectionManager ccm = httpClient.getConnectionManager();
SchemeRegistry registry = ccm.getSchemeRegistry();
registry.register(new Scheme("https", 443, ssf));
} catch (KeyManagementException ex) {
throw new RuntimeException(ex);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
}
3.4、请求结果的实体类
创建 ResultEntity
实体类,用于封装请求的返回结果
@Data
@AllArgsConstructor
@NoArgsConstructor
public 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);
}
@Override
public String toString() {
return "ResultEntity [result=" + result + ", message=" + message + ", data=" + data + "]";
}
}
3.5、配置类与配置文件
OSSConfig
和 TyporaToolConfig
为配置文件所对应的实体类,typora-tool.properties
为配置文件
配置类
OSSConfig
:连接阿里云 OSS 所需要的配置信息/**
* @ClassName OSSConfig
* @Description TODO
* @Author Heygo
* @Date 2020/7/25 13:47
* @Version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OSSConfig {
// 连接阿里云 OSS 所需要的配置信息
private String endPoint;
private String bucketName;
private String accessKeyId;
private String accessKeySecret;
private String bucketDomain;
// 单例对象
private static OSSConfig ossConfig;
// 使用静态代码块初始化,保证线程安全
static {
// 创建单例对象
ossConfig = new OSSConfig();
// 填充属性
Properties prop = new Properties();
try (
InputStream is = TyporaToolConfig.class.getClassLoader().getResourceAsStream("typora-tool.properties");
){
prop.load(is);
ossConfig.setEndPoint(prop.getProperty("endPoint"));
ossConfig.setBucketName(prop.getProperty("bucketName"));
ossConfig.setAccessKeyId(prop.getProperty("accessKeyId"));
ossConfig.setAccessKeySecret(prop.getProperty("accessKeySecret"));
ossConfig.setBucketDomain(prop.getProperty("bucketDomain"));
} catch (IOException e) {
e.printStackTrace();
System.out.println("配置文件有点问题,你去检查下哦~~~");
}
}
// 返回单例的配置对象
public static OSSConfig getOSSConfig(){
return ossConfig;
}
}
TyporaToolConfig
:Typora
工具所需的配置信息/**
* @ClassName TyporaToolConfig
* @Description TODO
* @Author Heygo
* @Date 2020/7/25 13:47
* @Version 1.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TyporaToolConfig {
private boolean isNeedCleanPic; // 是否需要进行图片清理
private boolean isNeedTiltleAutoNo; // 是否需要进行标题编号
private boolean isNeedPicSyncOSS; // 是否需要进行图片同步
private String noteRootPath; // 笔记的根目录(也可以填入单个的 .md 文件)
// 单例对象
private static TyporaToolConfig typoraToolConfig;
// 使用静态代码块初始化,保证线程安全
static {
// 创建单例对象
typoraToolConfig = new TyporaToolConfig();
// 填充属性
Properties prop = new Properties();
try (
InputStream is = TyporaToolConfig.class.getClassLoader().getResourceAsStream("typora-tool.properties");
){
prop.load(is);
typoraToolConfig.isNeedCleanPic = Boolean.parseBoolean(prop.getProperty("isNeedCleanPic"));
typoraToolConfig.isNeedTiltleAutoNo = Boolean.parseBoolean(prop.getProperty("isNeedTiltleAutoNo"));
typoraToolConfig.isNeedPicSyncOSS = Boolean.parseBoolean(prop.getProperty("isNeedPicSyncOSS"));
typoraToolConfig.setNoteRootPath(prop.getProperty("noteRootPath"));
} catch (IOException e) {
e.printStackTrace();
System.out.println("配置文件有点问题,你去检查下哦~~~");
}
}
// 返回单例的配置对象
public static TyporaToolConfig getTyporaToolConfig(){
return typoraToolConfig;
}
}
配置文件
typora-tool.properties
配置文件
# 是否需要进行图片清理
isNeedCleanPic=true
# 是否需要进行标题编号
isNeedTiltleAutoNo=true
# 是否需要进行图片同步
isNeedPicSyncOSS=true
# 笔记的根目录(也可以填入单个的 .md 文件)
noteRootPath=<输入你的笔记存储路径>
# 阿里云 OSS 配置信息
endPoint=<输入你的 endpoint>
bucketName=<输入你的 bucketName>
accessKeyId=<输入你的 accessKeyId>
accessKeySecret=<输入你的 accessKeySecret>
bucketDomain=<输入你的 bucketDomain>
3.6、文件读写工具类
TyporaFileRwUtil
:对于 md
文件读写操作的封装
/**
* @ClassName TyporaFileRwUtil
* @Description TODO
* @Author Heygo
* @Date 2020/7/24 12:55
* @Version 1.0
*/
public class TyporaFileRwUtil {
/**
* 读取MD文件的内容
*
* @param curFile MD文件的File对象
* @return MD文件的内容
*/
public static String readMdFileContent(File curFile) {
// 存储md文件内容
StringBuilder sb = new StringBuilder();
// 当前行内容
String curLine;
// 装饰者模式:FileReader无法一行一行读取,所以使用BufferedReader装饰FileReader
try (
FileReader fr = new FileReader(curFile);
BufferedReader br = new BufferedReader(fr);
) {
// 当前行有内容
while ((curLine = br.readLine()) != null) {
sb.append(curLine + "\r\n");
}
} catch (IOException e) {
e.printStackTrace();
}
// 返回md文件内容
return sb.toString();
}
/**
* 保存MD文件
*
* @param destMdFilePath MD文件路径
* @param mdFileContent MD文件内容
*/
public 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.7、OSS 上传工具类
OSSUtil
:对于阿里云 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;
}
}
3.8、图片瘦身工具类
TyporaPicCleanUtil
:图片瘦身的工具类,之前我真是个铁头娃,没有用到的图片直接就删除了,回收站都找不到。。。现在我学聪明了,我在当前目录创建了一个 deletePic
目录,专门用于存放被删除的图片,这也算是一种软删除吧~~~
/**
* @ClassName TyporaPicCleanUtil
* @Description TODO
* @Author Heygo
* @Date 2020/7/24 11:47
* @Version 1.0
*/
public class TyporaPicCleanUtil {
/**
* 执行单个 md 文件的图片瘦身
*
* @param allPicFiles 图片 File 对象(md 文件对应的 {filename}.assets 文件夹中的所有图片)
* @param mdFileContent md 文件的内容
*/
public static void doSingleTyporaClean(File[] allPicFiles, String mdFileContent) {
// 获取文中所用到的所有图片名称
String[] usedPicNames = getUsedPicNames(mdFileContent);
// 删除无用图片
CleanUnusedPic(allPicFiles, usedPicNames);
}
/**
* 获取 md 文件对应的 {filename}.assets 文件夹中的所有图片
*
* @param destMdFile md 文件的 File 对象
* @return 所有本地图片的 File 对象数组
*/
public static File[] getLocalPicFiles(File destMdFile) {
// 获取 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 文件夹中的所有图片
return assetsFile.listFiles();
}
/**
* 获取 md 文件中使用到的图片名称
*
* @param mdFileContent md 文件的内容
* @return 使用到的图片名称
*/
private static String[] getUsedPicNames(String mdFileContent) {
// 图片名称
// 图片路径存储格式:
/*
\[.*\]:[image-20200603100128164]
. :匹配任意字符
* :出现0次或多次
\(.+\):(IDEA快捷键.assets/image-20200603100128164.png)
. :匹配任意字符
+ :出现1次或多次
*/
String regex = "!\\[.*\\]\\(.+\\)";
// 匹配文章中所有的图片标签
Matcher matcher = Pattern.compile(regex).matcher(mdFileContent);
// imageNames 用于存储匹配到的图片标签
List<String> imageNames = new ArrayList<>();
//遍历匹配项,将其添加至集合中
while (matcher.find()) {
// 得到当前图片标签
String curImageLabel = matcher.group();
// 放心大胆地使用"/"截取子串,因为文件名不能包含"/"字符
Integer picNameStartIndex = curImageLabel.lastIndexOf("/") + 1;
Integer picNameEndIndex = curImageLabel.length() - 1;
// 得到图片名称
String curImageName = curImageLabel.substring(picNameStartIndex, picNameEndIndex);
// 添加至集合中
imageNames.add(curImageName);
}
// 转换为数组返回
String[] retStrs = new String[imageNames.size()];
return imageNames.toArray(retStrs);
}
/**
* 清除无用图片
*
* @param allPicFiles 本地所有的图片 File 对象
* @param usedPicNames md 文件中使用到的图片名称
*/
private static void CleanUnusedPic(File[] allPicFiles, String[] usedPicNames) {
// assets文件夹中如果没有图片,则直接返回
if (allPicFiles == null || allPicFiles.length == 0) {
return;
}
// 临时文件夹,保存被删除的图片
File deletePicDir = new File(allPicFiles[0].getParentFile().getParent() + "\\" + "deletePic");
if (deletePicDir.exists() == false) {
deletePicDir.mkdir();
}
// 获取asset文件夹的绝对路径
String assetPath = allPicFiles[0].getParent();
// 为了便于操作,将数组转换为List
List<String> usedPicNameList = Arrays.asList(usedPicNames);
// 遍历所有本地图片,看看有哪些图片没有被使用
for (File curPicFile : allPicFiles) {
// 如果没有被使用,则添加至unusedPicNames集合
String curFileName = curPicFile.getName();
boolean isUsed = usedPicNameList.contains(curFileName);
if (!isUsed) {
// 创建File对象,用于删除
String curPicAbsolutePath = curPicFile.getAbsolutePath();
// 删除文件,看看回收站还有没有,并没有。。。
// curPicFile.delete();
// 将待删除的图片移动至 deletePic 文件夹
String destPicPath = deletePicDir.getAbsolutePath() + "\\" + curPicFile.getName();
curPicFile.renameTo(new File(destPicPath));
// 测试用:打印输出
System.out.println("已移动无用图片至:" + destPicPath);
}
}
}
}
3.9、标题编号工具类
TyporaTiltleAutoNoUtil
:标题自动编号的工具类
/**
* @ClassName TyporaTiltleAutoNoUtil
* @Description TODO
* @Author Heygo
* @Date 2020/7/24 12:43
* @Version 1.0
*/
public class TyporaTiltleAutoNoUtil {
/**
* 执行单个文件的标题自动编号
*
* @param mdFileContent md 文件的内容
* @return 标题编号之后的 md 文件内容
*/
public static String doSingleMdTitleAutoNo(String mdFileContent) {
return getAutoTitledMdContent(mdFileContent);
}
/**
* 执行单个文件的标题自动编号
*
* @param mdFileContent md 文件的内容
* @return 标题编号之后的 md 文件内容
*/
private static String getAutoTitledMdContent(String mdFileContent) {
// 标题编号
/*
标题编号规则:
- 一级标题为文章的题目,不对一级标题编号
- 二级、三级、四级标题需要级联编号
- 五级、六级标题无需级联编号,只需看上一级标题的脸色,递增即可
*/
Integer[] titleNumber = new Integer[]{
0, 0, 0, 0, 0};
// 存储md文件内容
StringBuilder sb = new StringBuilder();
// 当前行内容
String curLine;
// 当前内容是否在代码块中
boolean isCodeBLock = false;
try (
StringReader sr = new StringReader(mdFileContent);
BufferedReader br = new BufferedReader(sr);
) {
while ((curLine = br.readLine()) != null) {
// 忽略代码块
if (curLine.trim().startsWith("```")) {
isCodeBLock = !isCodeBLock;
}
if (isCodeBLock == false) {
// 判断是否为标题行,如果是标题,是几级标题
Integer curTitleLevel = calcTitleLevel(curLine);
if (curTitleLevel != -1) {
// 插入标题序号
curLine = insertTitleNumber(curLine, titleNumber);
// 重新计算标题计数器
RecalcTitleCounter(curTitleLevel, titleNumber);
}
}
// 向缓冲区中追加内容
sb.append(curLine + "\r\n");
}
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
/**
* 根据当前行的内容,计算标题的等级
*
* @param curLine 当前行的内容
* @return 当前行的标题等级:-1 表示不是标题行,>=2 的正数表示标题的等级
*/
private static Integer calcTitleLevel(String curLine) {
// 由于一级标题无需编号,所以从二级标题开始判断
boolean isTitle = curLine.startsWith("##");
if (!isTitle) {
// 返回 -1 表示非标题行
return -1;
}
// 现在来看看是几级标题
Integer titleLevel = curLine.indexOf(" ");
return titleLevel;
}
/**
* 在当前行前面插入标题的等级
*
* @param curLine 当前行的内容
* @param titleNumber 标题计数器
* @return 添加标题之后的行
*/
private static String insertTitleNumber(String curLine, Integer[] titleNumber) {
// 标题等级(以空格分隔的前提是 Typora 开启严格模式)
Integer titleLevel = curLine.indexOf(" ");
// 标题等级部分
String titleLevelStr = curLine.substring(0, titleLevel);
// 标题内容部分
String titleContent = curLine.substring(titleLevel + 1);
// 先去除之前的编号
titleContent = RemovePreviousTitleNumber(titleContent);
// 标题等级递增
Integer titleIndex = titleLevel - 2;
if (titleIndex > 5) {
System.out.println(titleIndex);
}
titleNumber[titleIndex] += 1;
// 标题序号
String titleNumberStr = "";
switch (titleLevel) {
case 2:
titleNumberStr = titleNumber[0].toString();
break;
case 3:
titleNumberStr = titleNumber[0].toString() + "." + titleNumber[1];
break;
case 4:
titleNumberStr = titleNumber[0].toString() + "." + titleNumber[1] + "." + titleNumber[2];
break;
case 5:
titleNumberStr = titleNumber[3].toString();
break;
case 6:
titleNumberStr = titleNumber[4].toString() + " ) ";
break;
}
titleNumberStr += "、";
// 插入标题序号
titleContent = titleNumberStr + titleContent;
System.out.println("已增加标题序号:" + titleContent);
// 返回带序号的标题
curLine = titleLevelStr + " " + titleContent;
return curLine;
}
/**
* 当上一级标题更新时,需重置子级标题计数器
*
* @param titleLevel 当前标题等级
* @param titleNumber 标题计数器
*/
private static void RecalcTitleCounter(Integer titleLevel, Integer[] titleNumber) {
// 二级标题更新时,三级及三级以下的标题序号重置为 0
Integer startIndex = titleLevel - 1;
for (int i = startIndex; i < titleNumber.length; i++) {
titleNumber[i] = 0;
}
}
/**
* 移除之前的标题编号
* @param curLine 当前行内容
* @return 移除标题编号之后的行
*/
private static String RemovePreviousTitleNumber(String curLine) {
// 寻找标题中的 、 字符
Integer index = curLine.indexOf("、");
if (index > 0 && index < 6) {
// 之前已经进行过标号
return curLine.substring(index + 1);
} else {
// 之前未进行过标号,直接返回
return curLine;
}
}
}
3.10、图片同步工具类
TyporaOSSPicSyncUtil
:图片同步至阿里云 OSS
服务器的工具类
/**
* @ClassName TyporaOSSPicSyncUtil
* @Description TODO
* @Author Heygo
* @Date 2020/7/24 12:44
* @Version 1.0
*/
public class TyporaOSSPicSyncUtil {
/**
* 执行单个 md 文件的图片同步
*
* @param allPicFiles 本地图片的 File 对象数组
* @param mdFileContent md 文件的内容
* @return 图片同步之后,md 文件的内容(已经将本地图片链接替换为网路链接)
*/
public static String doSingleMdPicSyncToOSS(File[] allPicFiles, String mdFileContent) {
// 如果本地图片都没有,还同步个鸡儿
if (allPicFiles == null) {
return mdFileContent;
}
return changeLocalReferToUrlRefer(allPicFiles, mdFileContent);
}
/**
* 执行单个 md 文件的图片同步
*
* @param allPicFiles 本地图片的 File 对象数组
* @param mdFileContent md 文件的内容
* @return 图片同步之后,md 文件的内容(已经将本地图片链接替换为网路链接)
*/
private static String changeLocalReferToUrlRefer(File[] allPicFiles, String mdFileContent) {
// 存储md文件内容
StringBuilder sb = new StringBuilder();
// 当前行内容
String curLine;
// 获取所有本地图片的名称
List<String> allPicNames = new ArrayList<>();
for (File curPicFile : allPicFiles) {
// 获取图片名称
String curPicName = curPicFile.getName();
// 添加至集合中
allPicNames.add(curPicName);
}
try (
StringReader sr = new StringReader(mdFileContent);
BufferedReader br = new BufferedReader(sr);
) {
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");
}
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 目标图片
* @return 图片的网络路径:如果为"",则表示图片不存在于 OSS 服务器上;如果不为"",则表示图片在 OSS 上的路径
*/
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;
}
}
3.11、程序主逻辑编写
TyporaTools
:程序入口,递归遍历笔记的根目录(也可以是单个的 .md
文件),进行图片瘦身、标题编号、图片同步等操作
/**
* @ClassName TyporaTools
* @Description TODO
* @Author Heygo
* @Date 2020/7/24 11:44
* @Version 1.0
*/
public class TyporaTools {
public static void main(String[] args) {
// 笔记存储根目录
String noteRootPath = TyporaToolConfig.getTyporaToolConfig().getNoteRootPath();
// 根据配置需要,执行 Typora 文件瘦身、标题自动编号、图片同步至 OSS 等功能
doMainBusiness(noteRootPath);
}
private static void doMainBusiness(String destPath) {
// 获取当前路径的File对象
File destPathFile = new File(destPath);
// 如果是文件,执行单个md文件的图片瘦身,然后递归返回即可
if (destPathFile.isFile()) {
doSingleMdMainBusiness(destPathFile);
return;
}
// 获取当前路径下所有的子文件和路径
File[] allFiles = destPathFile.listFiles();
// 遍历allFiles
for (File curFile : allFiles) {
// 获取curFile对象是否为文件夹
Boolean isDirectory = curFile.isDirectory();
// 获取当前curFile对象对应的绝对路径名
String absolutePath = curFile.getAbsolutePath();
// 如果是文件夹
if (isDirectory) {
// 如果是asset文件夹,则直接调过
if (absolutePath.endsWith(".assets")) {
continue;
}
}
// 如果是文件夹,则继续执行递归
doMainBusiness(absolutePath);
}
}
private static void doSingleMdMainBusiness(File destMdFile) {
// 如果不是 MD 文件,滚蛋
Boolean isMdFile = destMdFile.getName().endsWith(".md");
if (!isMdFile) {
return;
}
// 读取 MD 文件内容
String mdFileContent = TyporaFileRwUtil.readMdFileContent(destMdFile);
// 获取当前 MD 文件的图片 File 对象数组
File[] allPicFiles = TyporaPicCleanUtil.getLocalPicFiles(destMdFile);
// 获取配置文件内容
TyporaToolConfig typoraToolConfig = TyporaToolConfig.getTyporaToolConfig();
// Typora 瘦身
if (typoraToolConfig.isNeedCleanPic()) {
System.out.println(destMdFile.getName() + " 开始执行瘦身计划...");
TyporaPicCleanUtil.doSingleTyporaClean(allPicFiles, mdFileContent);
System.out.println(destMdFile.getName() + " 瘦身计划执行完毕~~~");
System.out.println();
}
// 标题自动标号
if (typoraToolConfig.isNeedTiltleAutoNo()) {
System.out.println(destMdFile.getName() + " 开始执行标题自动标号...");
mdFileContent = TyporaTiltleAutoNoUtil.doSingleMdTitleAutoNo(mdFileContent);
System.out.println(destMdFile.getName() + " 标题自动标号完成~~~");
System.out.println();
}
// 图片同步至阿里云 OSS
if (typoraToolConfig.isNeedPicSyncOSS()) {
System.out.println(destMdFile.getName() + " 开始执行图片同步至 OSS...");
mdFileContent = TyporaOSSPicSyncUtil.doSingleMdPicSyncToOSS(allPicFiles, mdFileContent);
System.out.println(destMdFile.getName() + " 图片同步至 OSS 完成~~~");
System.out.println();
}
// 执行保存
TyporaFileRwUtil.SaveMdContentToFile(destMdFile.getPath(), mdFileContent);
}
}
4、结束语
有想法就一定要付诸于行动,哪怕是造轮子,造个轮子之后,你也会对轮子的结构有更深入、更清晰的了解
还没有评论,来说两句吧...