Ghost 实现自动化备份

Jaxson Wang 奇淫技巧 阅读量

0x0 前言

前几天在折腾主机时候,不小心把环境搞乱了,无奈最后联系腾讯云工程师挽救,虽然最终能进入SSH备份数据,但 sudo 无法使用,无奈重装系统。这次事件发生后觉得有必要做个自动化备份。摆在以前的 WordPress 平台可以实现各种插件备份方法。

对于 Ghost 平台备份也不算复杂,直接备份数据库和平台数据就行了,至于实现就可以使用 shell 脚本实现。

0x1 数据库备份

新建 mysql 备份用户,用于只备份于 Ghost 数据,严格控制对应的权限:

mysql -uroot -p
CREATE USER 'backup'@'localhost' IDENTIFIED BY '你的密码';
GRANT ALL ON iiong.* TO 'backup'@'localhost';
FLUSH PRIVILEGES;
quit;

注意 iiong 是你 Ghost 指定的数据,不清楚可以查看 Ghost/config.production.json 文件

扩展:

SHOW GRANTS FOR 'backup'@'localhost'; # 查看权限列表
REVOKE ALL ON *.* FROM 'backup'@'localhost'; # 删除所有权限操作

创建 .my.cnf 文件

echo '[client]
user=backup
password="你的密码"' >> ~/.my.cnf

赋值权限:

sudo chmod 600 ~/.my.cnf

执行下面命令是否正常备份sql文件:

mysqldump iiong > ~/iiong.sql

因为数据库版本问题如果导出控制台报 “mysqldump: Error: 'Access denied; you need (at least one of) the PROCESS privilege(s) for this operation' when trying to dump tablespaces” 错误直接添加参数:

mysqldump iiong > ~/iiong.sql --no-tablespaces

如果没有异常说明可以继续下一步。

0x3 编写脚本

为了方便将上面备份的 sql 文件进行压缩和日期:

mysqldump iiong | gzip > ~/iiong-$(date +%Y%m%d).sql.gz

然后再备份 Ghost 目录下的 content 文件夹,这个文件夹下包含博客的图片和一些关键配置文件,也包括类似数据库文件:

tar -zcvf ~/content-$(date +%Y%m%d).tar.gz /var/www/ghost/content/

整合脚本 backup.sh,在用户根目录下新建脚本:

cd ~
mkdir backup backup/ghost
cd backup
#!/bin/bash
alipanBackupPath=Ghost博客备份/

now=$(date +'%Y-%m-%d_%H-%M')
database="$HOME/backup/ghost/iiong-$now.sql.gz"
ghostdata="$HOME/backup/ghost/content-$now.tar.gz"
aliyunpan="$HOME/backup/aliyunpan"

echo "保存数据库文件到备份文件夹"
mysqldump iiong --no-tablespaces | gzip > $database

echo "备份 Ghost 数据"
tar -zcvf $ghostdata --absolute-names /var/www/ghost/content/ > /dev/null

echo "备份数据库文件到阿里云盘"
python3 $aliyunpan/main.py upload -t 100.0 $ghostdata $alipanBackupPath
echo "备份 Ghost 文件到阿里云盘"
python3 $aliyunpan/main.py upload -t 100.0 $database $alipanBackupPath

如果提示上传超时,查看 content-xxx.tar.gz 容量很大,需要修改 -t 参数,单位为秒。

保存成功后设置脚本权限:

sudo chmod a+x ./backup.sh

backup 文件夹下拉取阿里云盘工具:

git clone --depth=1 https://github.com.cnpmjs.org/wxy1343/aliyunpan.git aliyunpan

Github 资源被墙,建议使用镜像代理。工具地址:https://github.com/wxy1343/aliyunpan

安装依赖:

sudo apt install python3-pip
pip3 install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/

需要 python3 版本,目前维护主流 Linux 环境包含 python3 环境,如果没有请自行安装。

注意修改脚本最前面的一行变量:

  • alipanBackupPath: 请在 App 新建文件夹,这个文件夹作为备份使用,请注意后面加个 / 来区分文件夹。

请到阿里云盘网页版登录后任意界面浏览器调出开发者工具(F12 或者 command+option+i)在 Application 窗口下的 Local Storage 菜单中右边窗口里面的 token 对象中的 refresh_token 复制它的值就行了,具体截图可以参考:https://github.com/wxy1343/aliyunpan
然后写入环境:

echo "refresh_token: 'xxxxx'" > ~/.config/aliyunpan.yaml

0x4 添加任务

crontab -e

# 在任务表添加下列任务
0 0 * * * ~/backup.sh # 每天 0 点执行

*/5 * * * * ~/backup.sh # 每5分钟执行一次 可以拿这个测试

效果如下:

IMG_0232

0x5 通知

大概使用几天后发现莫名其妙不备份了,查看一看是阿里云盘的 token 失效了,所以花了时间写了个通知脚本,利用脚本执行 shell 在成功或者失败回调进行通知,通知方式我是使用企业微信通知,胜在稳定吧,如果需要可以在 backup.sh 同级目录下新建 index.js 脚本:

const { execSync } = require('child_process')
const https = require('https')

// 企业微信密钥
const corpid = 'xxx'
const agentid = 'xxxx'
const corpsecret = 'xxxx'

// QQ key
const qqkey = 'xxxx'

// server酱
const wxServerKey = 'xxxx'

try {
  const stdout = execSync('./backup.sh', [])
  const message = `${parseTime(new Date())} 备份成功!- ${stdout.toString()}`

  // 企业微信通知
  postData(`https://api.htm.fun/api/Wechat/text/?corpid=${corpid}&corpsecret=${corpsecret}&agentid=${agentid}&text=${message}`)
  // QQ 通知
  postData(`https://qmsg.zendee.cn/send/${qqkey}?msg=${encodeURIComponent(message)}`)
  // Server 通知
  // postData(`https://sctapi.ftqq.com/${wxServerKey}.send?title=${message}`)

} catch (error) {
  console.log('系统异常', error)
  const message = `${parseTime(new Date())} 备份失败! - ${error}`

  // 企业微信通知
  postData(`https://api.htm.fun/api/Wechat/text/?corpid=${corpid}&corpsecret=${corpsecret}&agentid=${agentid}&text=${message}`)
  // QQ 通知
  postData(`https://qmsg.zendee.cn/send/${qqkey}?msg=${encodeURIComponent(message)}`)
  // Server 通知
  // postData(`https://sctapi.ftqq.com/${wxServerKey}.send?title=${message}`)
}

/**
 * 发送消息
 * @param {*} url 
 */
function postData(url) {
  https.get(url, response => {
    console.log(`Got response: ${response.statusCode}`)
    let data = ''
    response.on('data', chunk => {
      data += chunk
    })
    response.on('end', chunk => {
      console.log('data')
      console.log("Got a response: ", data)
    })
  }).on('error', error => {
    console.log(`Got error: ${error.message}`)
  })
}

/**
 * 时间格式化
 * @param {*} time 
 * @param {*} cFormat 
 * @returns 
 */
function parseTime(time, cFormat) {
  if (arguments.length === 0) {
    return null
  }
  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
  let date
  if (typeof time === 'object') {
    date = time
  } else {
    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
      time = parseInt(time)
    }
    if ((typeof time === 'number') && (time.toString().length === 10)) {
      time = time * 1000
    }
    date = new Date(time)
  }
  const formatObj = {
    y: date.getFullYear(),
    m: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    i: date.getMinutes(),
    s: date.getSeconds(),
    a: date.getDay()
  }
  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
    let value = formatObj[key]
    // Note: getDay() returns 0 on Sunday
    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
    if (result.length > 0 && value < 10) {
      value = '0' + value
    }
    return value || 0
  })
  return time_str
}

上面涉及到的 api 参考文档:

企业微信密钥:https://www.htm.fun/archives/python-flask-api-server-jiang.html

QQ通知:https://qmsg.zendee.cn/api.html

Server酱:https://sct.ftqq.com/sendkey

如果觉得太多可以注释不使用即可。

然后删除之前的任务并且添加下面定时任务:

# 在任务表添加下列任务
0 0 * * * cd ~/backup && node index.js # 每天 0 点执行

*/5 * * * * cd ~/backup && node index.js # 每5分钟执行一次 可以拿这个测试

IMG_0338

IMG_0341

喵~
Jaxson Wang
永远年轻,永远热泪盈眶!
腾讯云开启 swap 分区
腾讯云开启 swap 分区

自从使用 MySQL8 大版本后,每次升级 ghost 就会提示内存不够的错误。但如果你要执行 ghost update --no-mem-check 来无视内存检测的话,你会发现在编译依赖包终端直接卡死。

2 分钟阅读
Nest.js 参数校验和自定义返回数据格式
Nest.js 参数校验和自定义返回数据格式

参数校验大部分业务是使用 Nest.js 中的管道方法实现,具体可以查阅文档。不过编写过程中遇到一些问题,虽然文档讲得比较晦涩。

4 分钟阅读