Skip to content

DRAFT 适用于VuePass的本地文章自动上传至远程服务器

约 1206 字大约 4 分钟

开发

2024-10-29

修改底部参数后,启动监听脚本,默认工作目录为脚本同级目录,当文件夹内有新文件或新的保存操作出现,则会自动上传至服务器

import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import paramiko
import os
from datetime import datetime
import socket

class FileChangeHandler(FileSystemEventHandler):
    def __init__(self, ssh_host, ssh_port, ssh_username, ssh_password, remote_path):
        self.ssh_host = ssh_host
        self.ssh_port = ssh_port
        self.ssh_username = ssh_username
        self.ssh_password = ssh_password
        self.remote_path = remote_path
        self.ssh = None
        self.max_retries = 3
        self.retry_delay = 5  # 重试间隔(秒)
        # 初始化时测试连接
        self.test_connection()
        
    def create_ssh_client(self):
        """创建一个新的SSH客户端连接"""
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        
        for attempt in range(self.max_retries):
            try:
                print(f"[{datetime.now()}] 尝试连接到服务器 (尝试 {attempt + 1}/{self.max_retries})...")
                ssh.connect(
                    self.ssh_host,
                    port=self.ssh_port,
                    username=self.ssh_username,
                    password=self.ssh_password,
                    timeout=10,
                    banner_timeout=60
                )
                return ssh
            except paramiko.SSHException as e:
                print(f"[{datetime.now()}] SSH连接错误: {str(e)}")
                if attempt < self.max_retries - 1:
                    print(f"[{datetime.now()}] {self.retry_delay}秒后重试...")
                    time.sleep(self.retry_delay)
                else:
                    raise
            except socket.error as e:
                print(f"[{datetime.now()}] 网络错误: {str(e)}")
                if attempt < self.max_retries - 1:
                    print(f"[{datetime.now()}] {self.retry_delay}秒后重试...")
                    time.sleep(self.retry_delay)
                else:
                    raise
        raise Exception("超过最大重试次数")

    def test_connection(self):
        """测试SSH连接"""
        try:
            print(f"[{datetime.now()}] 正在测试与服务器的连接...")
            # 先测试是否可以解析主机名
            socket.gethostbyname(self.ssh_host)
            
            # 测试SSH连接
            ssh = self.create_ssh_client()
            ssh.close()
            print(f"[{datetime.now()}] 连接测试成功!")
            return True
        except socket.gaierror as e:
            print(f"[{datetime.now()}] DNS解析失败: {str(e)}")
            print("请检查主机名或IP地址是否正确")
            return False
        except Exception as e:
            print(f"[{datetime.now()}] 连接测试失败: {str(e)}")
            return False
        
    def upload_to_server(self, file_path):
        try:
            # 获取相对路径
            abs_path = os.path.abspath(file_path)
            work_dir = os.path.abspath(PATH_TO_WATCH)
            rel_path = os.path.relpath(abs_path, work_dir)
            
            # 再次检查文件是否存在
            if not os.path.exists(file_path):
                print(f"[{datetime.now()}] 文件不存在: {file_path}")
                return False

            # 创建SSH客户端并尝试连接
            ssh = self.create_ssh_client()

            try:
                # 创建SFTP客户端
                sftp = ssh.open_sftp()

                # 使用相对路径构建远程路径
                remote_file_path = os.path.join(self.remote_path, rel_path)
                
                # 确保远程目录存在
                self.ensure_remote_dir(sftp, os.path.dirname(remote_file_path))

                print(f"[{datetime.now()}] 正在上传文件: {rel_path}")
                # 上传文件
                sftp.put(file_path, remote_file_path)

                print(f"[{datetime.now()}] 成功上传文件: {rel_path}")
                return True
                
            finally:
                # 确保连接被正确关闭
                if 'sftp' in locals():
                    sftp.close()
                ssh.close()

        except FileNotFoundError:
            print(f"[{datetime.now()}] 上传失败: 文件在上传过程中被删除或移动")
            return False
        except Exception as e:
            print(f"[{datetime.now()}] 上传失败: {str(e)}")
            print("详细错误信息:", e.__class__.__name__)
            return False

    def ensure_remote_dir(self, sftp, remote_dir):
        """确保远程目录存在,如果不存在则创建"""
        try:
            sftp.stat(remote_dir)
        except IOError:
            print(f"[{datetime.now()}] 创建远程目录: {remote_dir}")
            current_dir = "/"
            for dir_part in remote_dir.strip("/").split("/"):
                if not dir_part:
                    continue
                current_dir = os.path.join(current_dir, dir_part)
                try:
                    sftp.stat(current_dir)
                except IOError:
                    sftp.mkdir(current_dir)

    def on_modified(self, event):
        if not event.is_directory and not event.src_path.endswith('.DS_Store') and '.vscode' not in event.src_path:
            print(f"[{datetime.now()}] 检测到文件修改: {event.src_path}")
            # 等待文件稳定
            self.retry_upload(event.src_path)

    def retry_upload(self, file_path, max_attempts=5, initial_delay=1.0):
        """
        重试上传文件,使用递增延迟
        :param file_path: 文件路径
        :param max_attempts: 最大重试次数
        :param initial_delay: 初始延迟时间(秒)
        """
        for attempt in range(max_attempts):
            try:
                # 使用递增延迟
                current_delay = initial_delay * (attempt + 1)
                print(f"[{datetime.now()}] 等待文件就绪 {current_delay}秒...")
                time.sleep(current_delay)
                
                # 检查文件是否存在并等待文件大小稳定
                if not os.path.exists(file_path):
                    print(f"[{datetime.now()}] 文件不存在 (尝试 {attempt + 1}/{max_attempts})")
                    continue
                
                # 等待文件大小稳定
                file_size = -1
                new_size = os.path.getsize(file_path)
                while file_size != new_size:
                    file_size = new_size
                    time.sleep(0.5)  # 等待0.5秒
                    if os.path.exists(file_path):
                        new_size = os.path.getsize(file_path)
                    else:
                        print(f"[{datetime.now()}] 文件在检查大小时被删除")
                        break
                
                # 尝试读取文件以确保它已经完全写入
                try:
                    with open(file_path, 'rb') as f:
                        f.read()
                    print(f"[{datetime.now()}] 文件已就绪,开始上传...")
                except IOError as e:
                    print(f"[{datetime.now()}] 文件无法读取: {str(e)} (尝试 {attempt + 1}/{max_attempts})")
                    continue
                
                # 如果文件可以正常读取,则进行上传
                if self.upload_to_server(file_path):
                    return True
                
            except Exception as e:
                print(f"[{datetime.now()}] 上传尝试 {attempt + 1}/{max_attempts} 失败: {str(e)}")
                if attempt == max_attempts - 1:
                    print(f"[{datetime.now()}] 达到最大重试次数,放弃上传")
                    return False
                
            print(f"[{datetime.now()}] 上传失败,将在 {current_delay} 秒后重试...")
        
        return False

    def on_created(self, event):
        if not event.is_directory and not event.src_path.endswith('.DS_Store') and '.vscode' not in event.src_path:
            print(f"[{datetime.now()}] 检测到新文件: {event.src_path}")
            self.upload_to_server(event.src_path)

def start_monitoring(path_to_watch, ssh_host, ssh_port, ssh_username, ssh_password, remote_path):
    # 创建事件处理器
    event_handler = FileChangeHandler(
        ssh_host=ssh_host,
        ssh_port=ssh_port,
        ssh_username=ssh_username,
        ssh_password=ssh_password,
        remote_path=remote_path
    )
    
    # 创建观察者
    observer = Observer()
    observer.schedule(event_handler, path_to_watch, recursive=False)
    observer.start()
    
    try:
        print(f"[{datetime.now()}] 开始监控目录: {path_to_watch}")
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
        print(f"[{datetime.now()}] 停止监控")
    
    observer.join()

if __name__ == "__main__":
    # 配置参数
    PATH_TO_WATCH = "."  # 监控当前目录
    SSH_HOST = "101.43.124.15"
    SSH_PORT = 22
    SSH_USERNAME = "root"
    SSH_PASSWORD = "lXl20222"
    REMOTE_PATH = "/app/blog/docs"
    
    try:
        start_monitoring(
            PATH_TO_WATCH,
            SSH_HOST,
            SSH_PORT,
            SSH_USERNAME,
            SSH_PASSWORD,
            REMOTE_PATH
        )
    except Exception as e:
        print(f"[{datetime.now()}] 程序异常退出: {str(e)}")
[2024-10-29 15:33:19.048216] 成功上传文件: 火烧云快捷查看,油猴实现.md
[2024-10-29 15:34:09.171598] 检测到文件修改: /Users/xxx/Desktop/blogDocs/hosunyun-plugin.md
[2024-10-29 15:34:09.174863] 尝试连接到服务器 (尝试 1/3)...
[2024-10-29 15:34:09.766900] 正在上传文件: hosunyun-plugin.md
[2024-10-29 15:34:09.851142] 成功上传文件: hosunyun-plugin.md