【博客】使用GithubAction自动同步obisidian和hexo仓库

发布于:2025-03-31 ⋅ 阅读:(20) ⋅ 点赞:(0)

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

本文首发于❄慕雪的寒舍

1. 烦恼

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

  • 思源笔记:私密性高一些,不是博客的笔记都在这里面。由于思源笔记不是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;

是不是听起来都头大了?

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

image.png

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

image.png

这样就搞定啦!以后我只需要push博客到obsidian仓库中,就能自动同步到hexo仓库内了。不需要手动做那部分繁琐的操作