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