From 735abf2d3203deeec2872d142f3c2fca1985a507 Mon Sep 17 00:00:00 2001 From: cuianbing Date: Wed, 14 Jan 2026 21:27:38 +0800 Subject: [PATCH] first commit --- Dockerfile | 23 +++ README.md | 140 +++++++++++++ auto_check_files.py | 471 ++++++++++++++++++++++++++++++++++++++++++++ config.ini | 8 + requirements.txt | 3 + 5 files changed, 645 insertions(+) create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 auto_check_files.py create mode 100644 config.ini create mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ba9a724 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# 使用官方Python镜像作为基础镜像 +FROM python:3.9-slim + +# 设置工作目录 +WORKDIR /app + +# 复制requirements.txt文件到工作目录 +COPY requirements.txt . + +# 安装依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 复制所有项目文件到工作目录 +COPY . . + +# 创建目标目录 +RUN mkdir -p /opt/doc/_post + +# 设置环境变量 +ENV PYTHONUNBUFFERED=1 + +# 运行主程序 +CMD ["python", "auto_check_files.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..b04b1db --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +# Markdown文件自动处理与定时推送系统 + +## 功能说明 + +本系统用于自动处理Markdown文件,将24小时内更新的文件复制到指定目录并提交Git变更,然后定时推送到远程仓库。 + +## 主要功能 + +1. **遍历目录**:遍历指定目录下的所有Markdown文件(包含子文件夹) +2. **解析frontmatter**:解析每个文件的YAML frontmatter属性 +3. **检查更新时间**:优先使用frontmatter中的`updated`字段,否则使用文件修改时间 +4. **筛选24小时内更新的文件**:只处理最近24小时内更新的文件 +5. **同名文件处理**:支持三种策略:overwrite(覆盖), skip(跳过), rename(重命名) +6. **Git提交**:自动提交变更,提交信息格式:`feat: 📝 {title}` +7. **定时推送**:每天早上六点、中午12点、晚上零点推送到远程仓库 + +## 文件说明 + +- `parse_frontmatter.py`:主脚本,用于处理Markdown文件 +- `push_to_remote.py`:推送脚本,用于将本地提交推送到远程仓库 +- `push_to_remote.bat`:Windows批处理文件,用于运行推送脚本 +- `push_to_remote.sh`:Linux shell脚本,用于运行推送脚本 +- `example.md`:示例Markdown文件 + +## 配置说明 + +### 1. 添加远程仓库 + +在目标目录下执行以下命令添加远程仓库(使用SSH协议): + +```bash +git remote add origin git@github.com:username/repo.git +``` + +### 2. SSH密钥配置 + +确保SSH密钥已正确配置,密钥文件应位于`~/.ssh/`目录下: + +- 检查SSH密钥是否存在: + ```bash + ls -la ~/.ssh/ + ``` + +- 如果没有SSH密钥,生成新密钥: + ```bash + ssh-keygen -t rsa -b 4096 -C "your_email@example.com" + ``` + +- 将公钥添加到GitHub/GitLab等平台 + +### 3. 定时任务配置 + +#### Windows系统(使用任务计划程序) + +1. 打开"任务计划程序"(搜索"Task Scheduler") +2. 点击"创建基本任务" +3. 输入任务名称和描述 +4. 选择"每天"触发器 +5. 设置起始时间为"00:00:00",重复任务间隔为"12"小时,持续时间为"1天" +6. 选择"启动程序"操作 +7. 浏览并选择`push_to_remote.bat`文件 +8. 勾选"当单击完成时,打开此任务属性的对话框" +9. 切换到"触发器"选项卡,点击"新建" +10. 设置另一个触发器为"每天",起始时间为"12:00:00" +11. 点击"确定"完成配置 + +#### Linux系统(使用cron) + +1. 打开cron配置文件: + ```bash + crontab -e + ``` + +2. 添加以下行,设置每天零点、六点、十二点执行推送脚本: + ```bash + 0 0 * * * /path/to/push_to_remote.sh + 0 6 * * * /path/to/push_to_remote.sh + 0 12 * * * /path/to/push_to_remote.sh + ``` + +3. 保存并退出(对于vim编辑器,按`Esc`,然后输入`:wq`) + +### 4. 脚本参数配置 + +可以根据需要修改脚本中的默认参数: + +- **parse_frontmatter.py**: + - `source_dir`:源目录路径 + - `destination_dir`:目标目录路径 + - `overwrite_strategy`:同名文件处理策略 + +- **push_to_remote.py**: + - `repo_path`:Git仓库路径 + - `remote_name`:远程仓库名称 + - `branch`:分支名称 + +## 使用说明 + +1. **运行主脚本**: + ```bash + python parse_frontmatter.py + ``` + +2. **手动运行推送脚本**: + - Windows: + ```bash + push_to_remote.bat + ``` + - Linux: + ```bash + chmod +x push_to_remote.sh + ./push_to_remote.sh + ``` + +## 注意事项 + +1. 确保Python环境已正确安装,并且已安装所需依赖: + ```bash + pip install pyyaml gitpython + ``` + +2. 确保目标目录已初始化Git仓库,或者主脚本会自动初始化 + +3. 确保已正确配置SSH密钥,并且有权限访问远程仓库 + +4. 定时任务运行时,确保系统处于开机状态 + +## 日志说明 + +脚本会输出详细的日志信息,包括: +- 处理的文件数量 +- 成功复制的文件数量 +- 提交的Git变更 +- 推送到远程仓库的结果 + +可以根据需要将日志重定向到文件,以便查看历史记录: + +```bash +python parse_frontmatter.py > log.txt 2>&1 +``` diff --git a/auto_check_files.py b/auto_check_files.py new file mode 100644 index 0000000..cc21c62 --- /dev/null +++ b/auto_check_files.py @@ -0,0 +1,471 @@ +import os +from pickle import FALSE +import yaml +import shutil +import datetime +import configparser +from typing import Dict, List, Optional +from git import Repo +from apscheduler.schedulers.blocking import BlockingScheduler + +# 配置文件路径,使用绝对路径确保能找到 +CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.ini') + +# 全局变量,用于存储配置 +config = configparser.ConfigParser() + +# 全局变量,用于存储上一次检查的时间 +last_check_time = None + + +def parse_markdown_frontmatter(file_path: str) -> Dict: + """ + 读取Markdown文件并解析其frontmatter YAML属性 + + Args: + file_path (str): Markdown文件的路径 + + Returns: + dict: 解析后的frontmatter属性字典,如果没有frontmatter则返回空字典 + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # 检查文件是否以frontmatter开头 + if not content.startswith('---'): + return {} + + # 找到frontmatter的结束位置 + end_index = content.find('---', 3) + if end_index == -1: + return {} + + # 提取frontmatter内容 + frontmatter_content = content[3:end_index].strip() + + # 解析YAML + frontmatter = yaml.safe_load(frontmatter_content) + + # 确保返回的是字典类型 + if frontmatter is None: + frontmatter = {} + + return frontmatter + except FileNotFoundError: + print(f"文件 {file_path} 不存在") + return {} + except yaml.YAMLError as e: + print(f"解析文件 {file_path} 的frontmatter时出错: {e}") + return {} + except Exception as e: + print(f"处理文件 {file_path} 时出错: {e}") + return {} + + +def is_updated_since_last_check(frontmatter: Dict, file_path: str, last_check: datetime.datetime) -> bool: + """ + 检查文件是否在上一次检查时间之后更新 + + Args: + frontmatter (dict): 文件的frontmatter属性 + file_path (str): 文件路径 + last_check (datetime.datetime): 上一次检查的时间 + + Returns: + bool: 如果文件在上一次检查时间之后更新则返回True,否则返回False + """ + # 尝试从frontmatter获取updated时间 + updated_value = frontmatter.get('updated') + + # 如果frontmatter没有updated字段,使用文件的修改时间 + if not updated_value: + try: + mtime = os.path.getmtime(file_path) + updated_dt = datetime.datetime.fromtimestamp(mtime) + except OSError as e: + print(f"无法获取文件 {file_path} 的修改时间: {e}") + return False + else: + # 检查updated_value是否已经是datetime对象 + if isinstance(updated_value, datetime.datetime): + updated_dt = updated_value + else: + # 解析updated字符串为datetime对象 + try: + # 支持多种时间格式 + date_formats = [ + '%Y-%m-%d %H:%M:%S', + '%Y-%m-%d %H:%M', + '%Y-%m-%d', + '%Y/%m/%d %H:%M:%S', + '%Y/%m/%d %H:%M', + '%Y/%m/%d' + ] + + updated_dt = None + for fmt in date_formats: + try: + updated_dt = datetime.datetime.strptime(updated_value, fmt) + break + except ValueError: + continue + + if updated_dt is None: + print(f"文件 {file_path} 的updated时间格式不正确: {updated_value}") + return False + except Exception as e: + print(f"解析文件 {file_path} 的updated时间时出错: {e}") + return False + + # 检查是否在上一次检查时间之后更新 + return updated_dt >= last_check + + +def copy_file_to_destination(source_path: str, destination_dir: str, repo: Repo, frontmatter: Dict) -> bool: + """ + 将文件复制到目标目录并提交Git变更 + + Args: + source_path (str): 源文件路径 + destination_dir (str): 目标目录路径 + repo (Repo): Git仓库对象 + frontmatter (dict): 文件的frontmatter属性 + + Returns: + bool: 复制和提交成功返回True,否则返回False + """ + try: + # 从配置文件获取同名文件处理策略 + overwrite_strategy = config['DEFAULT'].get('overwrite_strategy', 'overwrite') + + # 确保目标目录存在 + if not os.path.exists(destination_dir): + os.makedirs(destination_dir, exist_ok=True) + print(f"创建目标目录: {destination_dir}") + + # 获取文件名和目标路径 + file_name = os.path.basename(source_path) + file_base, file_ext = os.path.splitext(file_name) + destination_path = os.path.join(destination_dir, file_name) + existsResult = FALSE + # 处理同名文件 + if os.path.exists(destination_path): + existsResult = True + if overwrite_strategy == 'skip': + print(f"跳过已存在文件: {destination_path}") + return False + elif overwrite_strategy == 'rename': + # 生成带时间戳的新文件名 + timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') + new_file_name = f"{file_base}_{timestamp}{file_ext}" + destination_path = os.path.join(destination_dir, new_file_name) + print(f"文件已存在,重命名为: {new_file_name}") + # 默认overwrite策略,直接覆盖 + + # 复制文件,保留元数据 + shutil.copy2(source_path, destination_path) + print(f"成功拷贝: {source_path} -> {destination_path}") + + # 获取提交信息 + title = frontmatter.get('title', file_name) + if existsResult: + commit_message = f"update: 📝 更新 《{title}》文章内容" + else: + commit_message = f"feat: 📝 新建《{title}》文章" + + # 添加文件到Git暂存区 + repo.git.add(".") + + # 提交变更 + repo.git.commit('-m', commit_message) + print(f"成功提交: {commit_message}") + + return True + except Exception as e: + print(f"拷贝或提交失败 {source_path}: {e}") + return False + + +def check_and_process_files(repo: Repo) -> Dict: + """ + 检查并处理指定目录下的所有Markdown文件,将上一次检查后更新的文件复制到目标目录并提交Git变更 + + Args: + repo (Repo): Git仓库对象 + + Returns: + dict: 处理结果统计 + """ + global last_check_time + + # 从配置文件获取参数 + source_dir = config['DEFAULT'].get('source_dir') + destination_dir = config['DEFAULT'].get('destination_dir') + overwrite_strategy = config['DEFAULT'].get('overwrite_strategy', 'overwrite') + + # 替换为当前目录进行测试(如果默认目录不存在) + if not os.path.exists(source_dir): + source_dir = os.getcwd() + print(f"默认源目录不存在,使用当前目录进行测试: {source_dir}") + + # 确保目标目录存在 + if not os.path.exists(destination_dir): + os.makedirs(destination_dir, exist_ok=True) + print(f"创建目标目录: {destination_dir}") + + stats = { + 'total_files': 0, + 'md_files': 0, + 'with_frontmatter': 0, + 'updated_since_last_check': 0, + 'copied_successfully': 0, + 'copy_failed': 0 + } + + current_time = datetime.datetime.now() + print(f"\n{'='*80}") + print(f"开始检查文件,时间: {current_time.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"上一次检查时间: {last_check_time.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"源目录: {source_dir}") + print(f"目标目录: {destination_dir}") + print(f"同名文件处理策略: {overwrite_strategy}") + print(f"{'='*80}") + + # 遍历源目录下的所有文件,忽略以`.`开头的文件夹 + for root, dirs, files in os.walk(source_dir): + # 过滤掉以`.`开头的文件夹 + dirs[:] = [d for d in dirs if not d.startswith('.')] + + for file in files: + # 过滤掉以`.`开头的文件 + if file.startswith('.'): + continue + # 只处理Markdown文件 + if not file.lower().endswith('.md'): + continue + + stats['total_files'] += 1 + stats['md_files'] += 1 + file_path = os.path.join(root, file) + + # 解析frontmatter + frontmatter = parse_markdown_frontmatter(file_path) + + if frontmatter: + stats['with_frontmatter'] += 1 + + # 检查是否在上一次检查时间之后更新 + if is_updated_since_last_check(frontmatter, file_path, last_check_time): + stats['updated_since_last_check'] += 1 + + # 复制到目标目录并提交 + if copy_file_to_destination(file_path, destination_dir, repo, frontmatter): + stats['copied_successfully'] += 1 + else: + stats['copy_failed'] += 1 + else: + # 对于没有frontmatter的文件,创建一个默认的frontmatter + default_frontmatter = { + 'title': os.path.basename(file_path) + } + + # 使用文件修改时间判断 + try: + mtime = os.path.getmtime(file_path) + updated_dt = datetime.datetime.fromtimestamp(mtime) + + if updated_dt >= last_check_time: + stats['updated_since_last_check'] += 1 + if copy_file_to_destination(file_path, destination_dir, repo, default_frontmatter): + stats['copied_successfully'] += 1 + else: + stats['copy_failed'] += 1 + except Exception as e: + print(f"无法处理文件 {file_path}: {e}") + + # 更新最后检查时间 + last_check_time = current_time + + # 打印统计结果 + print(f"\n{'='*80}") + print("处理结果统计:") + print(f"总文件数: {stats['total_files']}") + print(f"Markdown文件数: {stats['md_files']}") + print(f"包含frontmatter的文件数: {stats['with_frontmatter']}") + print(f"上一次检查后更新的文件数: {stats['updated_since_last_check']}") + print(f"成功复制的文件数: {stats['copied_successfully']}") + print(f"复制失败的文件数: {stats['copy_failed']}") + print(f"{'='*80}") + + return stats + + +def load_config(): + """ + 从配置文件中读取配置,并为缺失的参数设置默认值 + """ + global config, last_check_time + + # 读取配置文件 + config.read(CONFIG_FILE) + + # 确保DEFAULT section存在 + if 'DEFAULT' not in config: + config['DEFAULT'] = {} + + # 设置默认值 + default_config = { + 'source_dir': '/opt/src', + 'destination_dir': '/opt/doc/_post', + 'overwrite_strategy': 'overwrite', + 'remote_name': 'origin', + 'branch': 'master' + } + + # 更新配置文件中的缺失参数 + for key, value in default_config.items(): + if key not in config['DEFAULT'] or not config['DEFAULT'][key]: + config['DEFAULT'][key] = value + + # 获取上一次检查时间 + last_check_time_str = config['DEFAULT'].get('last_check_time') + if last_check_time_str: + last_check_time = datetime.datetime.strptime(last_check_time_str, '%Y-%m-%d %H:%M:%S') + else: + last_check_time = datetime.datetime.now() + # 将默认的上一次检查时间写入配置文件 + config['DEFAULT']['last_check_time'] = last_check_time.strftime('%Y-%m-%d %H:%M:%S') + + print(f"从配置文件中读取配置成功") + print(f"上一次检查时间: {last_check_time.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"源目录: {config['DEFAULT'].get('source_dir')}") + print(f"目标目录: {config['DEFAULT'].get('destination_dir')}") + print(f"同名文件处理策略: {config['DEFAULT'].get('overwrite_strategy')}") + print(f"远程仓库名称: {config['DEFAULT'].get('remote_name')}") + print(f"分支名称: {config['DEFAULT'].get('branch')}") + + +def save_config(): + """ + 将配置写入配置文件,特别是更新上一次检查时间 + """ + global config, last_check_time + + # 更新上一次检查时间 + config['DEFAULT']['last_check_time'] = last_check_time.strftime('%Y-%m-%d %H:%M:%S') + + # 写入配置文件 + with open(CONFIG_FILE, 'w') as f: + config.write(f) + + print(f"配置文件保存成功,上一次检查时间更新为: {last_check_time.strftime('%Y-%m-%d %H:%M:%S')}") + + +def push_to_remote(repo: Repo) -> bool: + """ + 将本地提交推送到远程仓库 + + Args: + repo (Repo): Git仓库对象 + + Returns: + bool: 推送成功返回True,否则返回False + """ + try: + # 从配置文件获取远程仓库名称和分支名称 + remote_name = config['DEFAULT'].get('remote_name', 'origin') + branch = config['DEFAULT'].get('branch', 'master') + + # 检查远程仓库是否存在 + if remote_name not in repo.remotes: + print(f"远程仓库 {remote_name} 不存在,无法推送") + return False + + # 推送到远程仓库 + repo.git.push(remote_name, branch) + print(f"成功推送到远程仓库 {remote_name} 的 {branch} 分支") + return True + except Exception as e: + print(f"推送失败: {e}") + return False + + +def job(): + """ + 定时任务函数,执行文件检查和处理 + """ + global last_check_time + + # 从配置文件获取参数 + source_dir = config['DEFAULT'].get('source_dir') + destination_dir = config['DEFAULT'].get('destination_dir') + + # 替换为当前目录进行测试(如果默认目录不存在) + if not os.path.exists(source_dir): + source_dir = os.getcwd() + destination_dir = os.path.join(source_dir, 'recent_posts') + print(f"默认源目录不存在,使用当前目录进行测试: {source_dir}") + + # 初始化或打开Git仓库 + try: + # 检查目标目录是否已经是Git仓库 + if os.path.exists(os.path.join(destination_dir, '.git')): + repo = Repo(destination_dir) + print(f"打开现有Git仓库: {destination_dir}") + else: + # 初始化新的Git仓库 + repo = Repo.init(destination_dir) + print(f"初始化新Git仓库: {destination_dir}") + + # 检查并处理文件 + check_and_process_files(repo) + + # 更新上一次检查时间 + last_check_time = datetime.datetime.now() + + # 保存配置,特别是更新上一次检查时间 + save_config() + + # 推送到远程仓库 + push_to_remote(repo) + + print("任务完成!") + + except Exception as e: + print(f"Git操作失败: {e}") + print("任务失败!") + + +def main(): + """ + 主函数,启动定时任务 + """ + # 加载配置文件 + load_config() + + # 立即执行一次检查 + print(f"\n立即执行第一次检查") + job() + + # 创建调度器 + scheduler = BlockingScheduler() + + # 添加定时任务,每隔5分钟执行一次 + scheduler.add_job(job, 'interval', minutes=5) + + print(f"\n定时任务已启动,每隔5分钟执行一次") + print(f"下次执行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"按 Ctrl+C 停止任务") + + try: + # 启动调度器 + scheduler.start() + except KeyboardInterrupt: + print(f"\n定时任务已停止") + except Exception as e: + print(f"定时任务执行出错: {e}") + + +if __name__ == "__main__": + main() diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..1c9e8f2 --- /dev/null +++ b/config.ini @@ -0,0 +1,8 @@ +[DEFAULT] +source_dir = source +destination_dir = recent_posts +overwrite_strategy = overwrite +remote_name = origin +branch = master +last_check_time = 2026-01-10 15:13:57 + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f1bccd3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pyyaml>=6.0 +gitpython>=3.1.0 +apscheduler>=3.10.0