使用GithubAction自动同步obisidian和hexo仓库,避免手动操作。

烦恼

先来说说慕雪现在的笔记和博客是怎么管理的吧,我正在使用两套笔记软件

  • 思源笔记:私密性高一些,不是博客的笔记都在这里面。由于思源笔记不是markdown编辑器,不能直接和hexo对接;
  • obisdian:专门管理hexo的博客;

然后我的hexo博客和obsidian又有分离,hexo配置仓库是一个单独的git仓库(后文简称为hexo仓库),obsidian博客库也是一个单独的git仓库(后文简称为obisidian仓库)。

我采用的操作特别繁琐,步骤如下:

  1. 在obsidian里面写好博客之后,手动使用FreeFileSync软件,将obisidian/blog目录同步到hexo/source/_posts目录中(这两个目录完全一样);
  2. 然后再到hexo本地仓库中执行hexo三板斧命令,给新的博客生成abbrlink,push到hexo的github仓库;
  3. 再用FreeFileSync反向将hexo/source/_posts目录同步回obisidian/blog目录,因为新的博客会多出abbrlink;

是不是听起来都头大了?

曾经的想法

先前我一直在想怎么让这套流程简化,考虑过几个方案都不太满意。我想过直接把obsidian vaults丢到hexo/source/_posts目录里面,但是考虑到我的obsidian中还有博客模板这种不需要上传到博客里面的内容,此项并不方便(虽然hexo其实可以跳过渲染某些md文件)

现在就想出了自动化的方案,也就是用github action来同步obsidian和hexo的仓库,当obisidian/blog目录有变动的时候,触发action,自动将这个目录的内容拷贝到hexo/source/_posts仓库目录中,并push到hexo仓库。

这里就有一个问题,abbrlink是基于hexo插件生成的,如果用这种方式那就没办法给新的博客md文件生成一个固定abbrlink了。不管是怎么让github action执行hexo g命令,最后都会出现远程仓库md文件中有abbrlink,但本地需要pull才能更新的问题,这会对我后续的博客编写和git操作带来不便(毕竟之前都是无脑push上去的)

之前每次想折腾github action的时候就会发现这个问题(由于没记笔记导致折腾的时候忘记了之前为啥没搞定……),然后又不了了之。

今天突然想起来,既然问题是在abbrlink插件上,那我不用hexo来生成abbrlink不就行了?反正abbrlink本质上和随机数没啥关系,我只要给新的博客手动加上一个和其他博客不冲突的abbrlink不就ok了?

注:hexo的abbrlink插件是通过crc16/crc32算法计算得到文件的abbrlink的,并非随机数生成。但对于abbrlink的作用来看,只要博客上每个文章都有一个独立的abbrlink其实就够了,所以abbrlink说它是随机数也没啥问题。

解决方法明了:用别的方法给新博客生成abbrlink,然后再用github action自动化同步obsidian仓库和hexo仓库。

解决步骤

生成abbrlink的python脚本

其实obsidian中是有一个abbrlink插件的,首先感谢插件作者能提供一个hexo-abbrlink插件的替代品。但是,这个插件不太符合本人的需求,因为它直接针对于obsidian全局,会把我的其他文件以及博客模板文件都加上abbrlink。

折腾了一会后,感觉不如返璞归真,直接写个python脚本,把所有博客文件的abbrlink遍历出来,然后生成30个不冲突的abbrlink写入到一个文件里面,每次写新博客的时候从这个文件里面取一个abbrlink出来用就完事啦!

说干就干,GPT,启动!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# 生成不冲突的abbrlink
import yaml
import re
import os
import random

MD_FILE_PATH = '../../Notes/CODE'
"""博客md文件路径"""
NEW_ABBRLINK_SIZE = 20
"""生成几个abbrlink"""
NEW_ABBRLINK_MD_FILE = '../../Notes/ABBRLINK归档.md'
"""生成的abbrlink写入这个md文件里面"""

def extract_front_matter(file_path):
"""
提取 Markdown 文件中的 front-matter 内容。
假设 front-matter 是以 '---' 包围的 YAML 格式内容。
"""
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()

# 使用正则表达式匹配 front-matter
match = re.match(r'---\n(.*?)\n---\n', content, re.DOTALL)

if match:
front_matter = match.group(1)
return yaml.safe_load(front_matter) # 使用 yaml 解析 front-matter
else:
return None

def remove_front_matter(file_path):
"""
移除 Markdown 文件中的 front-matter 部分,返回去除 front-matter 后的内容。
"""
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()

# 使用正则表达式去除 front-matter
cleaned_content = re.sub(r'---\n(.*?)\n---\n', '', content, flags=re.DOTALL)
return cleaned_content

def update_front_matter(file_path, new_front_matter):
"""
更新 Markdown 文件中的 front-matter 内容。
"""
with open(file_path, 'r+', encoding='utf-8') as file:
content = file.read()

# 使用正则表达式替换 front-matter
new_front_matter_str = yaml.dump(new_front_matter, default_flow_style=False)
content = re.sub(r'---\n(.*?)\n---\n', f'---\n{new_front_matter_str}\n---\n', content, flags=re.DOTALL)

# 写回文件
with open(file_path, 'w', encoding='utf-8') as file:
file.write(content)

def extract_front_matter_from_dir(directory_path):
"""
遍历指定目录及其子目录下的所有 .md 文件,提取它们的 front-matter 内容,并将所有内容添加到列表中。
"""
front_matter_list = []

# 遍历目录中的所有文件和子目录
for root, dirs, files in os.walk(directory_path):
for filename in files:
file_path = os.path.join(root, filename)

# 只处理 .md 文件
if filename.endswith('.md'):
front_matter = extract_front_matter(file_path)
if front_matter:
front_matter_list.append(front_matter)

return front_matter_list

def generate_unique_10digit_numbers(existing_numbers, n):
"""
生成 n 个不在 existing_numbers 列表中的 10 位数字。

:param existing_numbers: 已存在的整数列表
:param n: 需要生成的数字数量
:return: 不重复的 10 位数字列表
"""
unique_numbers = set(existing_numbers) # 将现有的数字转换为集合,加速查找
generated_numbers = []

# 如果现有数字数量已经非常大,可能无法生成足够的唯一数字
if len(unique_numbers) > 9999999999 - 1000000000:
return None

while len(generated_numbers) < n:
num = random.randint(1000000000, 9999999999) # 随机生成一个10位数字

if num not in unique_numbers:
generated_numbers.append(num)
unique_numbers.add(num) # 将新生成的数字添加到现有数字集合中

return generated_numbers

def write_int_list_to_md(file_path, int_list):
"""
将整数列表的成员按行写入一个Markdown文件。

:param file_path: Markdown文件的路径
:param int_list: 要写入文件的整数列表
"""
with open(file_path, 'w', encoding='utf-8') as file:
for number in int_list:
file.write(f"{number}\n") # 每个数字写一行


# 示例使用
if __name__ == "__main__":
# 假设 markdown 文件目录路径为 "../../Notes/CODE"
file_path = MD_FILE_PATH

# 提取所有 .md 文件的 front-matter
front_matter_list = extract_front_matter_from_dir(file_path)

# 打印所有文件的 front-matter
if not front_matter_list:
print("没有找到 front-matter 或目录为空。")
os.abort()

abbrlink_list = []
for fm in front_matter_list:
# print(f"文件的 front-matter 内容:", fm)
if 'abbrlink' not in fm:
print(f"ERR! abbrlink not in {fm}")
continue

link = int(fm['abbrlink'])
if link in abbrlink_list:
print(f"ERR! {link} in abbrlink list!")
continue

abbrlink_list.append(link)

# 获取新的abbrlink
new_abbrlink = generate_unique_10digit_numbers(abbrlink_list, NEW_ABBRLINK_SIZE)
for link in new_abbrlink:
print(link)

print("Gen abbrlink success")

write_int_list_to_md(NEW_ABBRLINK_MD_FILE, new_abbrlink)

print("Write abbrlink to", NEW_ABBRLINK_MD_FILE)

脚本运行效果如下,会生成新的abbrlink链接数字,然后写入到指定的md文件中。这样在obsidian里面就能看到这个md文件,取用里面的abbrlink了。用完了之后再手动执行一下脚本更新abbrlink就完事啦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
❯ python3 gen_abbrlink.py
8608489065
7885829874
8484489314
4284761477
1589125738
9151131777
4800824161
7141292217
2714461943
5131440419
2816690027
9574459795
6572894529
2920325088
2724835080
7631222809
1802821635
3120273636
2860205445
3100823185
Gen abbrlink success
Write abbrlink to ../../Notes/ABBRLINK归档.md

Github Action配置

接下来就是配置Github Action来同步两个仓库了。让GPT写了个大概,发现GPT在瞎说,它给出https的仓库clone链接,并表示用自带的GITHUB_TOKEN就能克隆私有仓库了,但实际上完全没用。最后还是得用老办法ssh密钥对来实现。

首先使用如下命令生成一个ssh密钥,弹出的提示中填写一个文件名字(不然会覆盖默认目录的ssh密钥对)

1
ssh-keygen -t rsa -C "github action" 

然后,搞清楚同步的方向,我的需要是将obsidian仓库中的内容同步到hexo仓库,所以公钥放在hexo仓库,私钥放在obsidian仓库中。

在hexo仓库(被推送的仓库)中,仓库设置Settings->Deploy keys->Add deploy key添加公钥,命名为HEXO_PUB_KEY注意需要勾选允许write写入仓库,不然默认权限只允许pull和clone仓库。

在obsidian仓库中,仓库设置Settings->Secrets and variables->Secrets添加私钥,命名为HEXO_PRI_KEY

最后的Github Action Workflow文件如下,将该文件写入仓库的.github/workflows/sync-code-to-posts.yml即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
name: Sync CODE to _posts

on:
push:
paths:
- 'Notes/CODE/**' # 监听 CODE 文件夹内的文件变化,没有变化不会触发action

jobs:
sync:
runs-on: ubuntu-latest

steps:
# 检出 obsidian 仓库的代码
- name: Checkout muob repository
uses: actions/checkout@v3

# 设置 Git 配置
- name: Set up Git
env:
ACTIONS_KEY: ${{ secrets.HEXO_PRI_KEY }}
run: |
mkdir -p ~/.ssh/
echo "$ACTIONS_KEY" > ~/.ssh/id_rsa
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_rsa
ssh-keyscan github.com >> ~/.ssh/known_hosts
git config --global user.name "musnows"
git config --global user.email "ezplayingd@126.com"
git config --global core.quotepath false
git config --global i18n.commitEncoding utf-8
git config --global i18n.logOutputEncoding utf-8
# 克隆 HexoBlog 仓库(私有仓库),使用 ssh 来进行认证
- name: Checkout HexoBlog repository
run: |
git clone git@github.com:musnows/Hexo-Blog.git HexoBlog
# 同步文件:将 obsidain 仓库中的 CODE 文件夹内容复制到 HexoBlog 仓库的 _posts 文件夹
- name: Sync files from CODE to _posts
run: |
rsync -av --delete Notes/CODE/ HexoBlog/source/_posts/
# 提交更改并推送到 HexoBlog 仓库
- name: Commit and push changes to HexoBlog repository
run: |
cd HexoBlog
git add .
git commit -m "Sync CODE to _posts at $(TZ='Asia/Shanghai' date '+%Y-%m-%d %H:%M:%S')"
git push origin hexo

第二步的Git操作中,我们将仓库配置的secrets.HEXO_PRI_KEY映射成环境变量ACTIONS_KEY,然后写入执行action的ubuntu环境的~/.ssh/id_rsa私钥文件中,这样就能操作另外一个仓库了。

第四步的Sync操作使用了rsync命令

1
rsync -av --delete 源文件夹 目标文件夹

解释一下这里的几个命令参数,-a用于保持文件的原有属性,-v代表verbose,会输出详细的日志,--delete用于在目标目录中删除源目录中不存在的文件(同步删除操作)。

测试效果

我在目录中创建了一个测试文件,push到了远端仓库中,触发了action

image.png

hexo配置仓库成功被push了,有更新的文件也是在obsidain仓库中被修改的文件,符合预期