使用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 randomMD_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-mattermatch = re.match(r'---\n(.*?)\n---\n', content, re.DOTALL)if match:front_matter = match.group(1)return yaml.safe_load(front_matter) # 使用 yaml 解析 front-matterelse:return Nonedef remove_front_matter(file_path):"""移除 Markdown 文件中的 front-matter 部分,返回去除 front-matter 后的内容。"""with open(file_path, 'r', encoding='utf-8') as file:content = file.read()# 使用正则表达式去除 front-mattercleaned_content = re.sub(r'---\n(.*?)\n---\n', '', content, flags=re.DOTALL)return cleaned_contentdef 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-matternew_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_listdef 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 Nonewhile 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_numbersdef 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-matterfront_matter_list = extract_front_matter_from_dir(file_path)# 打印所有文件的 front-matterif 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}")continuelink = int(fm['abbrlink'])if link in abbrlink_list:print(f"ERR! {link} in abbrlink list!")continueabbrlink_list.append(link)# 获取新的abbrlinknew_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 _postson:push:paths:- 'Notes/CODE/**' # 监听 CODE 文件夹内的文件变化,没有变化不会触发actionjobs:sync:runs-on: ubuntu-lateststeps: # 检出 obsidian 仓库的代码- name: Checkout muob repositoryuses: actions/checkout@v3# 设置 Git 配置- name: Set up Gitenv:ACTIONS_KEY: ${{ secrets.HEXO_PRI_KEY }}run: |mkdir -p ~/.ssh/echo "$ACTIONS_KEY" > ~/.ssh/id_rsachmod 700 ~/.sshchmod 600 ~/.ssh/id_rsassh-keyscan github.com >> ~/.ssh/known_hostsgit config --global user.name "musnows"git config --global user.email "ezplayingd@126.com"git config --global core.quotepath falsegit config --global i18n.commitEncoding utf-8 git config --global i18n.logOutputEncoding utf-8 # 克隆 HexoBlog 仓库(私有仓库),使用 ssh 来进行认证- name: Checkout HexoBlog repositoryrun: |git clone git@github.com:musnows/Hexo-Blog.git HexoBlog# 同步文件:将 obsidain 仓库中的 CODE 文件夹内容复制到 HexoBlog 仓库的 _posts 文件夹- name: Sync files from CODE to _postsrun: |rsync -av --delete Notes/CODE/ HexoBlog/source/_posts/# 提交更改并推送到 HexoBlog 仓库- name: Commit and push changes to HexoBlog repositoryrun: |cd HexoBloggit 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仓库内了。不需要手动做那部分繁琐的操作