使用Github Action自动同步obisidian和hexo仓库,避免手动操作。
本文首发于❄慕雪的寒舍
1. 烦恼
先来说说慕雪现在的笔记和博客是怎么管理的吧,我正在使用两套笔记软件
- 思源笔记:私密性高一些,不是博客的笔记都在这里面。由于思源笔记不是markdown编辑器,不能直接和hexo对接;
- obisdian:专门管理hexo的博客;
然后我的hexo博客和obsidian又有分离,hexo配置仓库是一个单独的git仓库(后文简称为hexo仓库),obsidian博客库也是一个单独的git仓库(后文简称为obisidian仓库)。
我采用的操作特别繁琐,步骤如下:
- 在obsidian里面写好博客之后,手动使用FreeFileSync软件,将
obisidian/blog
目录同步到hexo/source/_posts
目录中(这两个目录完全一样); - 然后再到hexo本地仓库中执行hexo三板斧命令,给新的博客生成abbrlink,push到hexo的github仓库;
- 再用FreeFileSync反向将
hexo/source/_posts
目录同步回obisidian/blog
目录,因为新的博客会多出abbrlink;
是不是听起来都头大了?
2. 曾经的想法
先前我一直在想怎么让这套流程简化,考虑过几个方案都不太满意。我想过直接把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仓库。
3. 解决步骤
3.1. 生成abbrlink的python脚本
其实obsidian中是有一个abbrlink插件的,首先感谢插件作者能提供一个hexo-abbrlink插件的替代品。但是,这个插件不太符合本人的需求,因为它直接针对于obsidian全局,会把我的其他文件以及博客模板文件都加上abbrlink。
折腾了一会后,感觉不如返璞归真,直接写个python脚本,把所有博客文件的abbrlink遍历出来,然后生成30个不冲突的abbrlink写入到一个文件里面,每次写新博客的时候从这个文件里面取一个abbrlink出来用就完事啦!
说干就干,GPT,启动!
# 生成不冲突的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就完事啦。
❯ 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
3.2. Github Action配置
接下来就是配置Github Action来同步两个仓库了。让GPT写了个大概,发现GPT在瞎说,它给出https的仓库clone链接,并表示用自带的GITHUB_TOKEN
就能克隆私有仓库了,但实际上完全没用。最后还是得用老办法ssh密钥对来实现。
首先使用如下命令生成一个ssh密钥,弹出的提示中填写一个文件名字(不然会覆盖默认目录的ssh密钥对)
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文件如下,将该文件写入obsidian仓库的.github/workflows/sync-code-to-posts.yml
即可,每个步骤都写了注释。
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命令
rsync -av --delete 源文件夹 目标文件夹
解释一下这里的几个命令参数,-a
用于保持文件的原有属性,-v
代表verbose,会输出详细的日志,--delete
用于在目标目录中删除源目录中不存在的文件(同步删除操作)。
4. 测试效果
我在目录中创建了一个测试文件,push到了远端仓库中,触发了action
hexo配置仓库成功被push了,有更新的文件也是在obsidain仓库中被修改的文件,符合预期。
这样就搞定啦!以后我只需要push博客到obsidian仓库中,就能自动同步到hexo仓库内了。不需要手动做那部分繁琐的操作