Typora 瘦身 + 标题编号 + 图片同步

Typora 瘦身 + 标题编号 + 图片同步

1、Why???

  1. 之前使用 PicGo 插件上传图片至阿里云 OSS 服务器,结果导致所有的图片顺序全部乱掉了,于是我直接疯掉了~~~一张一张,修改图片的链接
  2. 话说,咱又不是没那技术,自己写个小工具,还能拓展一些自己想要实现的功能,何乐而不为呢
  3. 说做就做,这个小工具我已经使用并测试了很久,应该没什么大问题,但是这篇博文因为种种原因,却耽搁了许久
  4. 项目 GitHub 地址:oneby1314/typora-tools

2、代码思路

2.1、图片瘦身

【图片瘦身的功能】参考我的这篇博文:typora 瘦身,该博文主要思想为:利用正则表达式匹配文中所用到的图片标签:![](),进而获取文中所引用的图片名称,然后与本地图片对比,删除本地的无用图片

2.2、标题编号

【标题自动编号功能】参考我的这篇博文:Typora 博文标题自动编号,该博文主要思想为:利用 Markdown 语法中,各级标题的 # 语法,实现对标题的自动编号功能

2.3、图片同步

【图片同步】参考我的这篇博文:自己写个简易版 PicGo,该博文主要思想为:利用正则表达式匹配文中所用到的图片标签:![](),如果匹配到的图片为本地链接,则将其上传到阿里云 OSS 服务器上,并修改图片路径为网络 URL 地址

2.4、思路总结

我编写的小工具是如上三个功能的汇总版本,提供了配置文件,用户可自行修改配置文件,选择自己需要的功能

3、代码实现

3.1、项目结构

项目结构如下

  1. HttpUtils:阿里云 OSS 必须依赖于该 HttpUtils 工具类
  2. ResultEntity:实体类,用于封装请求的返回结果
  3. OSSConfigOSS 配置文件所对应的实体类
  4. TyporaToolConfigTypora 小工具配置文件所对应的实体类
  5. TyporaTools:程序入口
  6. OSSUtil:阿里云 OSS 上传文件的工具类
  7. TyporaFileRwUtilTypora 文件读写工具类
  8. TyporaOSSPicSyncUtil:图片同步至 OSS 的工具类
  9. TyporaPicCleanUtil:图片瘦身的工具类
  10. TyporaTiltleAutoNoUtil:标题自动编号的工具类
  11. typora-tool.properties:配置文件
  12. pom.xmlpom 依赖

image-20200916155459609

3.2、引入依赖

pom 文件中引入项目所需的依赖

image-20200916160102066

<?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 上找到对应的源代码

image-20200916160201666

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 实体类,用于封装请求的返回结果

image-20200916160522009

@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、配置类与配置文件

OSSConfigTyporaToolConfig 为配置文件所对应的实体类,typora-tool.properties 为配置文件

image-20200916160628809

配置类

  1. 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;
        }
    
    }
    
  2. TyporaToolConfigTypora 工具所需的配置信息

    /**
     * @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 文件读写操作的封装

image-20200916161737355

/**
 * @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 上传文件、判断文件是否存在等操作的封装

image-20200916162023583

/**
 * 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 目录,专门用于存放被删除的图片,这也算是一种软删除吧~~~

image-20200916163530485

/**
 * @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](IDEA快捷键.assets/image-20200603100128164.png)
        /*
            \[.*\]:[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 :标题自动编号的工具类

image-20200916163554968

/**
 * @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 服务器的工具类

image-20200916163730086

/**
 * @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](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 上是否已经有该图片的 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 文件),进行图片瘦身、标题编号、图片同步等操作

image-20200916164416338

/**
 * @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、结束语

有想法就一定要付诸于行动,哪怕是造轮子,造个轮子之后,你也会对轮子的结构有更深入、更清晰的了解

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值