yhkylin-backup-tools/common/utils.cpp

597 lines
18 KiB
C++
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "utils.h"
#include <QByteArray>
#include <QDateTime>
#include <QFile>
#include <QTextStream>
#include <QDir>
#include <QRegularExpression>
#include <QThread>
#include <QUuid>
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <memory>
#include <functional>
#include <QRegularExpression>
#include <QProcess>
#include "mylittleparse.h"
#include "mydefine.h"
QString Utils::m_sysRootPath = "/";
/**
* @brief initSysRootPath, 根据应用程序路径推断系统根目录
* @param qsAppPath应用程序所在路径
* @note
* 本方法依赖于发布后应用程序会部署到目录/usr/bin中
* 1. grub引导中根目录为/root
* 2. livecd中根目录为/target
* 3. 正常使用中根目录为/
*/
void Utils::initSysRootPath(const QString& qsAppPath)
{
QString sysRootPath = qsAppPath;
if (sysRootPath.contains(DEFAULT_APP_PATH)) {
sysRootPath.replace(DEFAULT_APP_PATH, "/");
} else {
sysRootPath = "/";
}
m_sysRootPath = sysRootPath;
}
/**
* @brief customMessageHandler日志重定向句柄
* @param type 日志类型debug等
* @param context 上下文
* @param msg 日志信息
*/
void Utils::customMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& msg)
{
QByteArray localMsg = msg.toLocal8Bit();
QString strMsg("");
switch (type) {
case QtDebugMsg:
strMsg = QString("[Debug]");
break;
case QtWarningMsg:
strMsg = QString("[Warning]");
break;
case QtCriticalMsg:
strMsg = QString("[Critical]");
break;
case QtFatalMsg:
strMsg = QString("[Fatal]");
break;
default:
strMsg = QString("[Debug]");
break;
}
// 设置输出信息格式
QString strDateTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss ddd");
QString strMessage = strMsg + QString("DateTime:%1 ThreadId:%2 Message:%3 File:%4(%5)")
.arg(strDateTime).arg(QString::number(quintptr(QThread::currentThreadId()))).arg(localMsg.constData()).arg(context.file).arg(context.line);
std::cout << strMessage.toUtf8().data() << std::endl;
// 输出信息至文件中(读写、追加形式)
QString fileName = m_sysRootPath + PROC_LOG;
fileName.replace("//", "/");
QFile file(fileName);
file.open(QIODevice::ReadWrite | QIODevice::Append);
QTextStream stream(&file);
stream << strMessage << endl;
stream.flush();
file.close();
}
/**
* @brief 文件锁,锁定应用程序
* @param frontUid锁定程序的用户id
* @return 锁文件的文件描述符
* @note 使用方式类似于QLockFile后面时间充足时也可以自行封装一个独立类来处理文件锁
* 这个文件锁的作用是,在关机时关机模块会自行校验/tmp/lock路径下的锁文件如果存在则提醒用户xx程序在运行不允许关机等
*/
int Utils::lockProgram(int frontUid)
{
QDir dir(LOCK_FILE_PATH);
if (!dir.exists()) {
dir.mkdir(LOCK_FILE_PATH);
chmod(LOCK_FILE_PATH, S_IRWXU | S_IRWXG | S_IRWXO);
}
int lock_file_fd = ::open(LOCK_FILE, O_CREAT | O_RDWR, 0666);
if (0 > lock_file_fd) {
return -2;
}
fchmod(lock_file_fd, S_IRWXU | S_IRWXG | S_IRWXO);
int lock_ret = flock(lock_file_fd, LOCK_EX | LOCK_NB);
if (0 > lock_ret) {
return -11;
}
ftruncate(lock_file_fd, 0);
char write_string[PID_STRING_LEN] = { 0 };
snprintf(write_string, PID_STRING_LEN, "%d\n%s\n", frontUid, BACKUP_CLI_NAME);
write(lock_file_fd, write_string, strlen(write_string));
fdatasync(lock_file_fd);
return lock_file_fd;
}
/**
* @brief 解除应用程序文件锁
* @param lock_file_fd 锁文件的文件描述符
* @return 0解除成功1解除失败
*/
int Utils::unLockProgram(int lock_file_fd)
{
int lock_ret = flock(lock_file_fd, LOCK_UN);
if (lock_ret < 0) {
qCritical("unlock fail!");
return 1;
}
qDebug("unlock success!");
rmLockFile();
return 0;
}
/**
* @brief 删除锁文件
* @return bool
*/
bool Utils::rmLockFile()
{
bool res = QFile::remove(LOCK_FILE);
if (!res)
qCritical() << QString("remove %s fail").arg(LOCK_FILE);
return res;
}
/**
* @brief 检查/etc/.bootinfo是否存在并可读里面存放备份分区的UUID等信息
* @return bool
*/
bool Utils::checkBootInfoExists()
{
QString bootinfoPath = Utils::m_sysRootPath + BOOTINFO_PATH;
bootinfoPath.replace("//", "/");
QFile bootinfoFile(bootinfoPath);
if (!bootinfoFile.exists()) {
qCritical("%s is not exists!", qUtf8Printable(bootinfoPath));
return false;
}
if (!bootinfoFile.open(QIODevice::ReadOnly)) {
qCritical("%s file can't open!", qUtf8Printable(bootinfoPath));
return false;
}
bootinfoFile.close();
return true;
}
/**
* @brief 获取备份分区的UUID
* @return 备份分区的UUID
*/
QString Utils::getBackupPartitionUuid()
{
QString bootinfoPath = Utils::m_sysRootPath + BOOTINFO_PATH;
bootinfoPath.replace("//", "/");
MyLittleParse parse(bootinfoPath);
QString restoreUuid;
parse.find("RECOVERY_DEV_UUID", restoreUuid);
if (restoreUuid.isEmpty()) {
QString fstab = Utils::m_sysRootPath + FSTAB_PATH;
fstab.replace("//", "/");
QFile file(fstab);
if (file.exists()) {
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine();
if (line.startsWith("UUID=") && line.contains("/backup")) {
int indexOfSpace = line.indexOf(QRegularExpression("[ \t]"), 0);
QString uuid = line.mid(0, indexOfSpace);
uuid.replace("UUID=", "");
restoreUuid = uuid.trimmed();
break ;
}
}
}
}
}
return restoreUuid;
}
/**
* @brief 路径不存在则创建,不支持递归创建
* @param path
*/
void Utils::mkdir(const QString& path)
{
QDir dir(path);
if (!dir.exists())
dir.mkdir(path);
}
/**
* @brief 创建路径,支持递归创建
* @param path
* @return bool
*/
bool Utils::mkpath(const QString& path)
{
QDir dir(path);
if (!dir.exists()) {
return dir.mkpath(path);
}
return true;
}
/**
* @brief 备份还原时,需要排除绑定挂载的路径,因为数据不是存放在挂载路径内
* @param excludes存放需要排除的绑定路径
*/
void Utils::excludeFstabBindPath(QStringList &excludes)
{
QString fstabPath = Utils::m_sysRootPath + FSTAB_PATH;
fstabPath.replace("//", "/");
QFile file(fstabPath);
if (!file.open(QIODevice::ReadOnly))
return;
QTextStream in(&file);
while (!in.atEnd()) {
const QString &line = in.readLine();
if (line.startsWith("#"))
continue ;
if (line.startsWith("UUID="))
continue ;
if (line.startsWith("/dev/"))
continue ;
if (line.isEmpty())
continue ;
if (!line.contains("bind"))
continue ;
// 配置文件/etc/fstab每行6个域: <file system> <mount point> <type> <options> <dump> <pass>, 形如:
// UUID=232f5fb4-53e0-46b9-ba9b-22bfec64f2a2 /boot ext4 rw,relatime 0 0
QStringList list = line.split(QRegularExpression("[ \t]"));
QStringList fields;
for (int i = 0; i < list.size(); ++i) {
QString field = list.at(i);
field = field.trimmed();
if (field.isEmpty())
continue;
fields << field;
}
// 配置文件/etc/fstab每行6个域第二个域为挂载路径, 第一个域就是待排除路径
if (6 == fields.size())
excludes << fields.at(0);
}
file.close();
}
/**
* @brief 生成rsync --exclude-from排除路径规则文件
* @return
*/
bool Utils::generateExcludePathsFile()
{
QString excludeFile = Utils::m_sysRootPath + EXCLUDE_FILE_PATH;
excludeFile.replace("//", "/");
QFile excludePathFile(excludeFile);
// 暂时改为每次都重写文件内容,以便能随版本更新排除路径
if (!excludePathFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
qCritical("%s create failed", qUtf8Printable(excludeFile));
return false;
}
QTextStream in(&excludePathFile);
in << "/backup" << endl; //分区
// in << "/boot/efi" << endl;
in << "/cdrom" << endl;
in << "/dev" << endl;
// efi原始目录在/boot/efi备份到目标目录为/efi下再还原时已经单独处理了批量还原时应该屏蔽此目录
in << "/efi" << endl;
// 安全模块会将文件/usr/share/kysec-utils/data/readonly_list中的文件列表限制只读无法修改、备份包含扩展属性时、删除等
// 现在里面仅有/etc/uid_list先暂时排除掉等后续安全模块有其它保护方案后再进一步修改
in << "/etc/uid_list" << endl;
in << "/ghost" << endl; //ghost镜像文件
in << "/lost+found" << endl;
in << "/media" << endl;
in << "/mnt" << endl;
in << "/proc" << endl;
in << "/run" << endl;
in << "/swap_file" << endl;
in << "/sys" << endl; //添加*(/sys/*),表示如果/sys目录不存在则会拷贝/sys但不会拷贝/sys下的内容
in << "/tmp" << endl;
in << "/var/lib/docker/overlay2" << endl;
// 安卓兼容的这个里面很多文件都是设置了特殊扩展文件属性lsetxattr无法设置成功听取安卓兼容模块同事的意见不用管这个文件夹其实是从home下挂载的
in << "/var/lib/kmre" << endl;
in << "/var/lib/udisks2" << endl;
in << "/var/log" << endl;
// 系统安装后有的会将/data/home /data/root挂载到的/home /root上实际文件是存放在/data/home /data/root下面为了统一标准保留/home /root排除/data/home /data/root
QStringList excludes;
Utils::excludeFstabBindPath(excludes);
for (const QString& item : excludes) {
in << item << endl;
}
in.flush();
excludePathFile.close();
return true;
}
/**
* @brief 判断目录是否存在
* @param 目录路径
* @return true,存在false,不存在
*/
bool Utils::isDirExist(const QString& fullDirName)
{
QDir dir(fullDirName);
if (dir.exists())
return true;
return false;
}
/**
* @brief 获取系统备份还原排除路径列表, rsync --exclude-from排除路径规则文件中读取
* @return 系统备份还原排除路径列表
*/
QStringList Utils::getFromExcludePathsFile()
{
QStringList list;
list << "/backup";
// list << "/boot/efi";
list << "/cdrom";
list << "/dev";
list << "/efi";
list << "/etc/uid_list";
list << "/ghost";
list << "/lost+found";
list << "/media";
list << "/mnt";
list << "/proc";
list << "/run";
list << "/swap_file";
list << "/sys";
list << "/tmp";
list << "/var/lib/docker/overlay2";
list << "/var/lib/kmre";
list << "/var/lib/udisks2";
list << "/var/log";
// 系统安装后有的会将/data/home /data/root挂载到的/home /root上实际文件是存放在/data/home /data/root下面
QStringList excludes;
Utils::excludeFstabBindPath(excludes);
for (const QString& item : excludes) {
list << item;
}
return list;
}
/**
* @brief 生成Uuid
* @return UUID
*/
QString Utils::createUuid()
{
QString uuid;
do {
uuid = QUuid::createUuid().toString();
} while (uuid == AUTO_BACKUP_UUID || uuid == FACTORY_BACKUP_UUID);
return uuid;
}
/**
* @brief 将列表中内容写入指定文件中
* @param fileName文件全路径
* @param lines内容列表
* @return true成功写入false写入失败
*/
bool Utils::writeFileByLines(const QString& fileName, const QStringList& lines)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadWrite | QIODevice::Truncate)) {
return false;
}
QTextStream out(&file);
for (const QString& line : lines) {
out << line << endl;
}
out.flush();
file.close();
return true;
}
/**
* @brief 判断文件是否存在
* @param fileName 文件明
* @return booltrue-存在false-不存在
* @author zhaominyong
* @since 2021/05/29
*/
bool Utils::filsExists(const QString &fileName)
{
struct stat buffer;
return (stat(fileName.toStdString().data(), &buffer) == 0);
}
/**
* @brief 记录备份日志
* @param line日志内容
* @return bool, true-成功false-失败
* @author zhaominyong
* @since 2021/05/29
* @note
* 因为系统恢复成功后马上需要reboot但是此时的文件缓存可能还未能落盘故此增加此函数其中调用了系统函数fdatasync确保缓存落盘
*/
bool Utils::writeBackupLog(QString line)
{
line = line + "\n";
QString logFile = Utils::getSysRootPath() + BACKUP_LOG_TEXT_PATH;
logFile.replace("//", "/");
std::string fileName(logFile.toStdString());
// 判断文件是否存在
bool exists = filsExists(logFile);
std::unique_ptr<std::FILE, int (*)(std::FILE*)> fp(std::fopen(fileName.data(), "a+"), std::fclose);
if (!fp) {
std::perror("file opening failed!");
return false;
}
if (!exists) {
std::fputs("#This file must not be deleted and it is for the backup tool.\n", fp.get());
std::fputs("#0:new system backup\n", fp.get());
std::fputs("#1:update system backup\n", fp.get());
std::fputs("#2:new data backup\n", fp.get());
std::fputs("#3:update data backup\n", fp.get());
std::fputs("#4:restore system\n", fp.get());
std::fputs("#5:retaining user data\n", fp.get());
std::fputs("#6:restore data\n", fp.get());
std::fputs("#8:delete backup\n", fp.get());
std::fputs("#for example: 17-04-25 10:43:56,{uuidxxxxx},0,this is a note\n", fp.get());
}
std::fputs(line.toStdString().data(), fp.get());
std::fflush(fp.get());
fdatasync(fileno(fp.get()));
return true;
}
/**
* @brief 立即写文件
* @param fileName 文件名,包含路径
* @param content 文件内容
* @return bool
*/
bool Utils::syncWriteFile(const QString &fileName, const QString& content)
{
std::unique_ptr<std::FILE, int (*)(std::FILE*)> fp(std::fopen(fileName.toStdString().data(), "a+"), std::fclose);
if (!fp) {
std::perror("file opening failed!");
return false;
}
QStringList lines = content.split("\n");
for (const QString& line : lines) {
std::fputs(line.toStdString().data(), fp.get());
}
std::fflush(fp.get());
fdatasync(fileno(fp.get()));
return true;
}
/**
* @brief 将字节大小转换为GB等表示的字符串
* @param sizeqint64空间大小单位字节
* @return GB/MB/KB等表示的字符串型大小
*/
QString Utils::StringBySize(qint64 size)
{
#define GB (1024 * 1024 * 1024)
#define MB (1024 * 1024)
#define KB (1024)
float fcount = size * 1.0;
if (size > GB)
return QString(QString::number(fcount / GB, 10, 2) + "GB");
else if (size > MB)
return QString(QString::number(fcount / MB, 10, 2) + "MB");
else
return QString(QString::number(fcount / KB, 10, 2) + "KB");
}
/**
* @brief 获取挂接的移动设备列表
* @return MOUNTPOINT,PATH键值对列表
* @author zhaominyong
* @since 2021/06/07
* @note
* for bug 59636 【cpm】【HUAWEI】【KOS】【L0】【V3试制】备份时选择移动设备会出现一个dev/sdc的路径(一般+必现+不常用功能)
* QStorageInfo::mountedVolumes获取的磁盘列表区分不出来是否移动设备
*/
QHash<QString, QString> Utils::getRemovableStorages()
{
QHash<QString, QString> removalbeStorages;
QProcess process_lsblk;
process_lsblk.start("lsblk -P -o PATH,RM,TYPE,MOUNTPOINT,TRAN");
if (!process_lsblk.waitForStarted(-1)) {
return removalbeStorages;
}
if (!process_lsblk.waitForFinished(-1)) {
return removalbeStorages;
}
QString result = process_lsblk.readAllStandardOutput();
/*
result like bellow :
PATH="/dev/sdb" RM="1" TYPE="disk" MOUNTPOINT="" TRAN="usb"
PATH="/dev/sdb4" RM="1" TYPE="part" MOUNTPOINT="/media/zhaominyong/31 GB" TRAN=""
PATH="/dev/sr0" RM="1" TYPE="rom" MOUNTPOINT="/media/zhaominyong/Kylin-Desktop-V10-SP1" TRAN=""
*/
QStringList items = result.split("\n");
QStringList usbDevs;
for (QStringList::const_iterator it = items.constBegin(); it != items.constEnd(); ++it) {
const QString &line = (*it);
if (line.contains(" TRAN=\"usb\"")) {
QStringList storageAttrs = line.split("\" ");
if (5 != storageAttrs.size())
continue ;
QString path = storageAttrs.at(0);
path = path.replace("PATH=\"", "");
usbDevs.append(path);
}
if (line.contains(" RM=\"1\" ") || line.contains(" MOUNTPOINT=\"/media/")) {
if (line.contains(" TYPE=\"rom\" "))
continue;
if (line.contains(" MOUNTPOINT=\"\""))
continue;
// "PATH RM TYPE MOUNTPOINT TRAN" for each row, split by "\" "
QStringList storageAttrs = line.split("\" ");
if (5 != storageAttrs.size())
continue ;
QString path = storageAttrs.at(0);
path = path.replace("PATH=\"", "");
bool isSubPart = false;
for (const QString& usbDev : usbDevs) {
if (path.contains(usbDev)) {
isSubPart = true;
break;
}
}
if (!isSubPart)
continue;
QString mount_point = storageAttrs.at(3);
mount_point = mount_point.replace("MOUNTPOINT=\"", "");
mount_point = mount_point.left(mount_point.length() - 1);
removalbeStorages.insert(path, mount_point);
}
}
return removalbeStorages;
}