Linux下目录对比

1、功能描述

  功能:对比不同 Linux 主机下两个目录的文件,找出其中存在不同的文件,用于文件同步。
  
  开发过程中遇到的问题:

  • 两个目录位于不同的 Linux 主机下,无法使用 diff 等命令查找目录的不同。
  • 对比目录为 Linux 下通过 cifs 协议挂载的 Windows 共享目录,经测试 inotify 无法监视挂载目录下的文件变化,所以没有使用 rsync + inotify 或类似的方式。

  设计思路:遍历目录下的所有文件,获取文件信息,将文件信息保存到文件中,并通过 Socket 传输该文件对信息进行对比。

2、功能实现

2.1 保存文件信息的数据结构

  保存文件信息的数据结构选择自平衡二叉查找树。按照文件名的顺序,将文件信息保存在平衡二叉搜索树中。
  
  选择二叉树的原因是当文件数量比较多时,进行插入、删除等操作比较方便。其他也可以使用排序数组或链表实现。
  
  整个代码的头文件代码如下:

#ifndef __FILESYNC_H
#define __FILESYNC_H

#include 
#include 
#include 

#define SYNC_SRC            1       //源地址
#define SYNC_DST            0       //目的地址

/**文件信息结构体*/
struct fileinfo_st {
    time_t time;                    //文件最后修改时间
    char *pwd;                      //文件路径
};

/**保存进文件的文件信息结构体*/
struct saveinfo_st {
    time_t time;                    //文件最后修改时间
    char pwd[260];                  //文件路径
};

/**平衡二叉查找树节点*/
struct tree_node {
    struct fileinfo_st data;
    struct tree_node *left;
    struct tree_node *right;
    int height;
};

/**文件操作*/
#define TYPE_DELETE             0
#define TYPE_CHANGE             1
#define TYPE_CREATE             2

typedef struct tree_node * SEARCH_TREE;

/**avltree.c*/
void insert_node(SEARCH_TREE *tree, struct fileinfo_st fileinfo);
void delete_node(SEARCH_TREE *tree, struct fileinfo_st fileinfo);

/**filelist.c*/
int readfilelist(char *basepath, SEARCH_TREE *tree);

/**treecompare.c*/
void free_tree(SEARCH_TREE *tree);
void save_tree_to_file(int fd, SEARCH_TREE tree);
void compare_info(int outside, int inside, PTASK_INFO tran_task, int pos);

#endif

  其中 SYNC_SRC、SYNC_DST 分别表示源地址和目的地址。由于进行目录对比的最终目的是实现文件同步,所以需要区分源地址和目的地址。
  
  fileinfo_st 结构体是二叉树节点中的文件信息,其中包含 pwd、time 两个成员。其中 pwd 是带绝对路径的文件名,time 是文件的最后修改时间,通过这两个成员简单判断文件的不同。也可以添加其他属性进行判断。
  saveinfo_st 结构体是要保存到文件中的信息,成员含义和 fileinfo_st 结构体相同。由于对比的目录位于不同的主机上,需要将二叉树上的文件信息保存到文件中,然后通过 Socket 传输出去进行对比。
  
  tree_node 结构体是二叉树的节点,成员包含文件信息 data、左孩子 left、右孩子 right 和 节点深度 height。
  
  功能接口的实现包含以下文件:
  avltree.c:二叉树的操作;
  filelist.c:获取目录下的文件信息;
  treecompare.c:对文件信息进行对比。

2.2 二叉树的操作

  二叉树的主要操作为向二叉树中插入或删除节点。
  
  相关代码如下:

#include 
#include 
#include 

#include "filesync.h"

/**
 * 获取节点高度
 * @param node          二叉树节点
 * @return              返回节点的深度值,只有一个根节点的高度为0,空树的高度-1
 */ 
int node_height(struct tree_node *node)  
{  
    return node==NULL ? -1 : node->height;  
}  

/**
 * 比较两个数的值,获取最大值
 * @param a         数值a
 * @param b         数值b
 * @return          返回最大值
 */ 
int max(int a,int b)  
{  
    return a/**
 * LL型平衡旋转。当tree的左子树的左子树上的节点使得tree的平衡度为2时,以tree为中心进行右旋。
 * @param tree  进行右旋的根节点
 */
void LLrotate(SEARCH_TREE *tree)  
{  
    struct tree_node *tmp;

    tmp = (*tree)->left;            //保存节点的左子树   
    (*tree)->left = tmp->right;     //tmp节点的右子树作为tree的左子树
    tmp->right = (*tree);           //把tree作为tmp的右子树

    //到此树旋转完成,更新树的深度,以tree,tmp为节点的树的深度发生了变化;  
    (*tree)->height = max(node_height((*tree)->left), node_height((*tree)->right)) + 1; 
    tmp->height = max(node_height(tmp->left), node_height(tmp->right)) + 1;

    //根节点发生了变化,由k1变为了k
    (*tree) = tmp;  
}  

/**
 * RR型平衡旋转。当tree的右子树的右子树上的节点使得tree的平衡度为-2时,以tree为中心进行左旋。
 * @param tree  进行左旋的根节点
 */
void RRrotate(SEARCH_TREE *tree)  
{  
    struct tree_node *tmp;

    tmp = (*tree)->right;           //保存节点的右子树
    (*tree)->right = tmp->left;
    tmp->left = (*tree);

    //到此树旋转完成,更新树的深度,以tmp,tree为节点的树的深度发生了变化;   
    (*tree)->height = max(node_height((*tree)->left), node_height((*tree)->right)) + 1;
    tmp->height = max(node_height(tmp->left), node_height(tmp->right)) + 1;

    //根节点发生了变化,由tree变为了tmp
    (*tree) = tmp;  
}  

/**
 * LR型平衡旋转。当tree的左子树的右子树上的节点使得T的平衡度为2时,进行二叉树的平衡
 * @param tree  进行平衡的根节点
 */
void LRrotate(SEARCH_TREE *tree)
{
    //先以tree的左子树为中心进行左旋
    RRrotate(&((*tree)->left));
    //再以tree为中心进行右旋
    LLrotate(tree);
}

/**
 * RL型平衡旋转。当tree的右子树的左子树上的节点使得T的平衡度为-2时,进行二叉树的平衡
 * @param tree  进行平衡的根节点
 */
int RLrotate(SEARCH_TREE *tree)
{
    //先以tree的右子树为中心进行右旋
    LLrotate(&((*tree)->right));
    //再以tree为中心进行左旋
    RRrotate(tree);
}

/**
 * 左平衡处理
 * @param tree  进行平衡的根节点
 */ 
void left_balance(SEARCH_TREE *tree)  
{  
    struct tree_node *tmp = (*tree)->left;

    if(node_height(tmp->left) - node_height(tmp->right) == -1)   
    {  
        //右子树高于左子树,在右子树插入的  
        LRrotate(tree);             //LR型平衡旋转  
    }  
    else  
    {      
        LLrotate(tree);             //LL型平衡旋转  
    }  
}  

/**
 * 右平衡处理
 * @param tree  进行平衡的根节点
 */
void right_balance(SEARCH_TREE *tree)  
{  
    struct tree_node *tmp = (*tree)->right;

    if(node_height(tmp->right) - node_height(tmp->left) == -1)  
    {  
        //左子树比右子树高,说明在左子树插入的  
        RLrotate(tree);             //RL型平衡旋转  
    }  
    else  
    {  
        RRrotate(tree);             //RR型平衡旋转  
    }  
}  

/**
 * 平衡二叉搜索树的插入操作
 * @param tree          平衡二叉搜索树的根节点
 * @param fileinfo      插入节点的文件信息
 */
void insert_node(SEARCH_TREE *tree, struct fileinfo_st fileinfo)  
{  
    //如果当前查找的根结点为空树,表明查无此结点,故插入结点。
    if(NULL == *tree)  
    {  
        (*tree) = (struct tree_node *)malloc(sizeof(struct tree_node));
        (*tree)->data = fileinfo;  
        (*tree)->height = 0;   
        (*tree)->left = NULL;  
        (*tree)->right = NULL;
    } 
    else if(strcmp((*tree)->data.pwd, fileinfo.pwd) > 0)        //在左子树插入  
    {  
        insert_node(&((*tree)->left), fileinfo);  
        //判断是否破坏AVL树的平衡性  
        if (node_height((*tree)->left) - node_height((*tree)->right) == 2)   
            left_balance(tree);                                 //左平衡处理  
        }  
    else if(strcmp((*tree)->data.pwd, fileinfo.pwd) < 0)        //在右子树插入  
    {  
        insert_node(&((*tree)->right), fileinfo);  
        //判断是否破坏AVL树的平衡性  
        if (node_height((*tree)->right) - node_height((*tree)->left) == 2)   
            right_balance(tree);                                //右平衡处理  

    }  
    else {                                                      //已有此结点,更新节点信息  
        free((*tree)->data.pwd);
        (*tree)->data = fileinfo;
    }  

    //更新树的高度
    (*tree)->height = max(node_height((*tree)->left), node_height((*tree)->right)) + 1;            
}  

/**
 * 平衡二叉搜索树的删除操作
 * @param tree          平衡二叉搜索树的根节点
 * @param fileinfo      删除节点的文件信息
 */
void delete_node(SEARCH_TREE *tree, struct fileinfo_st fileinfo)  
{  
    if(NULL == (*tree))                                         //空树直接返回  
        return;  

    if(strcmp((*tree)->data.pwd, fileinfo.pwd) > 0)             //在左子树中查找  
    {  
        delete_node(&((*tree)->left), fileinfo);     
        if(node_height((*tree)->right) - node_height((*tree)->left) == 2)    
            right_balance(tree);                                //树右平衡处理
    }  

    else if(strcmp((*tree)->data.pwd, fileinfo.pwd) < 0)        //在右子树中查找  
    {  
        delete_node(&((*tree)->right), fileinfo);  
        if(node_height((*tree)->left) - node_height((*tree)->right) == 2)   
            left_balance(tree);                                 //树左平衡处理
    }  

    else                                                        //找到要删除的元素节点  
    {       
        if((*tree)->left == NULL)                               //左子树为空  
        {  
            struct tree_node *tmp = *tree;  
            *tree = (*tree)->right;                             //用右孩子代替此节点  
            free(tmp->data.pwd);
            free(tmp);                                          //释放内存  
        }  

        else if((*tree)->right == NULL)                         //右子树为空  
        {  
            struct tree_node *tmp = *tree;  
            *tree = (*tree)->left;                              //用左孩子代替此节点  
            free(tmp->data.pwd);
            free(tmp);   
        }  

        else                                                    //左右子树都不为空  
        {  
            //一般的删除策略是左子树的最小数据 或 右子树的最小数据 代替该节点  
            struct tree_node *tmp = (*tree)->left;              //从左子树中查找  
            while(tmp->right != NULL)  
                tmp = tmp->right;  
            //此时的tmp指向左子树中的最大元素  
            (*tree)->data = tmp->data;  

            delete_node(&((*tree)->left), tmp->data);  //递归的删除该节点  
        }  
    }  

    //更新节点的高度  
    if(*tree)  
        (*tree)->height = max(node_height((*tree)->left), node_height((*tree)->right));  
}  

  insert_node() 函数向二叉树中插入文件信息节点,参数为指向二叉树根节点地址的指针 tree,要插入的文件信息 fileinfo。
  对节点信息进行修改,如修改文件的最后修改时间,同样调用该函数。
  
  delete_node() 函数从二叉树中删除文件信息节点,参数与 insert_node() 函数项目。
  
  其他几个函数为对二叉树进行平衡和计算节点深度的功能。

2.3 获取目录下的文件信息

  遍历要监视的目录,获取所有文件(包含目录)的信息,保存到二叉树的数据结构中。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include "filesync.h"

/**
 * 获取文件完整路径
 * @param basepath      文件所在目录
 * @param name          文件名
 * @return              返回文件的完整路径
 */ 
char *get_pwd(const char *basepath, const char *name)
{
    char *path;

    path = (char *)malloc(strlen(basepath) + strlen(name) + 2);
    sprintf(path, "%s/%s", basepath, name);

    return path;
}

/**
 * 获取文件信息
 * @param pwd           文件路径
 * @return              文件信息struct fileinfo_st结构体
 */ 
struct fileinfo_st get_fileinfo(char *pwd)
{
    struct stat filestat;
    struct fileinfo_st fileinfo;
    int ret;
    int fd;


    ret = stat(pwd, &filestat);
    if (ret != 0) {
        perror("Get file infomation failed");
    }

    //文件最后修改时间
    fileinfo.time = filestat.st_mtime;

    //文件路径
    fileinfo.pwd = pwd;

    return fileinfo;
}

/**
 * 获取监视目录下文件信息
 * @param basepath      监视目录
 * @param tree          保存文件信息的二叉树
 * @return              成功返回0,失败返回-1
 */ 
int readfilelist(char *basepath, SEARCH_TREE *tree)
{
    DIR *dir;
    struct dirent *ptr;
    char base[1000];

    while ((dir=opendir(basepath)) == NULL)
    {
        perror("Open dir error...");
        sleep(1);
    }

    while ((ptr=readdir(dir)) != NULL)
    {
        if(strcmp(ptr->d_name,".") == 0 || strcmp(ptr->d_name,"..") == 0)       //忽略. 和 .. 文件
            continue;

        else if(ptr->d_type == 4) {                                             //目录文件
            struct fileinfo_st fileinfo = get_fileinfo(get_pwd(basepath, ptr->d_name));

            insert_node(tree, fileinfo);

            memset(base, '\0', sizeof(base));
            strcpy(base, basepath);
            strcat(base, "/");
            strcat(base, ptr->d_name);
            readfilelist(base, tree);
        }

        else {
            struct fileinfo_st fileinfo = get_fileinfo(get_pwd(basepath, ptr->d_name));

            insert_node(tree, fileinfo);
        }
    }

    closedir(dir);
    return 0;
}

  readfilelist() 函数实现获取文件信息的功能,参数包含监视目录 basepath、指向二叉树根节点地址的指针 tree。
  实现方式为打开监视目录后,通过 readdir() 函数获取目录下的所有文件,读取文件中所需要的信息。
  
  其他函数,get_pwd() 函数功能为在文件名前加上绝对路径;fileinfo_st() 函数获取需要的文件信息,例如文件最后修改时间。

2.4 目录信息文件对比

  将二叉树中的节点信息保存到文件中,之后需要通过 Socket 发送该文件,例如从目的地址发送到源地址,源地址将收到的文件信息与自己的文件信息做对比,找出不同。

#include 
#include 
#include 

#include "filesync.h"

/**
 * 删除整个二叉树
 * @param tree          保存文件信息的二叉树
 */ 
void free_tree(SEARCH_TREE *tree)
{
        if ((*tree) != NULL) {      
        free_tree(&((*tree)->left));
        free_tree(&((*tree)->right));

        free((*tree)->data.pwd);
        free((*tree));
    }
}

/**
 * 遍历二叉树,按照文件名顺序保存文件信息
 * @param fd            保存文件信息的文件描述符
 * @param tree          保存文件信息的二叉树
 */ 
void save_tree_to_file(int fd, SEARCH_TREE tree) 
{
    if (tree != NULL) {     
        save_tree_to_file(fd, tree->left);

        struct saveinfo_st fileinfo;
        fileinfo.time = tree->data.time;
        memcpy(fileinfo.pwd, tree->data.pwd, (strlen(tree->data.pwd) + 1));

        write(fd, &fileinfo, sizeof(struct saveinfo_st));

        save_tree_to_file(fd, tree->right);
    }
}

/**
 * 通过 Socket 将存在不同的文件从源地址发送到目的地址
 * @param pwd           发生修改的文件名
 * @param type          对文件进行的操作。取值为以下参数之一:
 *                          TYPE_DELETE:将文件从内网删除;
 *                          TYPE_CHANGE:文件发生变化,将该文件从外网复制到内网
 *                          TYPE_CREATE:文件被创建,将该文件从外网复制到内网
 */ 
void send_to_transfer(char *pwd, int type)
{
    /**该部分代码实现不做展示*/
}

/**
 * 对比外网和内网文件信息,将不同点进行文件传输
 * @param outside       保存外网文件信息的文件描述符
 * @param inside        保存内网文件信息的文件描述符
 */ 
void compare_info(int outside, int inside)
{
    struct saveinfo_st outside_info;
    struct saveinfo_st inside_info;
    int outside_ret, inside_ret;

    while (1) {
        outside_ret = read(outside, &outside_info, sizeof(struct saveinfo_st));
        inside_ret = read(inside, &inside_info, sizeof(struct saveinfo_st));

        //外网信息文件和内网信息文件都读到了结尾
        if (inside_ret == 0 && outside_ret == 0)
            break;

        //内网信息文件读到了结尾,外网信息文件没有读到结尾
        else if (inside_ret == 0 && outside_ret != 0) {
            do {
                //发消息给文件传输模块
                send_to_transfer(outside_info.pwd, TYPE_CREATE);
            } while (read(outside, &outside_info, sizeof(struct saveinfo_st)) != 0);

            break;
        }

        //外网信息文件读到了结尾,内网信息文件没有读到结尾
        else if (outside_ret == 0 && inside_ret != 0) {
            do {
                //发消息给文件传输模块
                send_to_transfer(inside_info.pwd, TYPE_DELETE);
            } while (read(inside, &inside_info, sizeof(struct saveinfo_st)) != 0);

            break;
        }

        //外网信息文件和内网信息文件都没有读到结尾
        else {
            //存在相同名字的文件
            if (strcmp(outside_info.pwd, inside_info.pwd) == 0) {
                //对比文件最后修改时间是否相等
                if (outside_info.time == inside_info.time)
                    continue;
                else 
                    send_to_transfer(outside_info.pwd, TYPE_CHANGE);

                continue;
            }

            //外网中有多的文件
            if (strcmp(outside_info.pwd, inside_info.pwd) < 0) {
                send_to_transfer(outside_info.pwd, TYPE_CREATE);
                lseek(inside, 0 - sizeof(struct saveinfo_st), SEEK_CUR);

                continue;
            }   

            //内网中有多的文件
            if (strcmp(outside_info.pwd, inside_info.pwd) > 0) {
                send_to_transfer(inside_info.pwd, TYPE_DELETE);
                lseek(outside, 0 - sizeof(struct saveinfo_st), SEEK_CUR);

                continue;
            }           
        }
    }
}

  save_tree_to_file() 函数将二叉树中的信息保存到文件中。
  
  compare_info() 函数对比源地址和目的地址的信息文件。实际使用时区分为外网和内网,分别对应源地址和目的地址。参数为信息文件的文件描述符。
  
  send_to_transfer() 函数的实现不做具体说明。基本实现方法为在源地址和目的地址之间建立 TCP 连接,先将文件名、文件大小等信息发送出去,然后发送文件内容,文件发送结束时再发送个结束的信息。

3、接口的调用

  以上接口的调用流程如下:

Created with Raphaël 2.1.2 start readfilelist() save_tree_to_file() socket compare_info() end

  • 第一步:创建二叉树根节点,调用 readfilelist() 将指定目录下的文件信息保存到二叉树中。
  • 第二步:调用 save_tree_to_file() 函数,将二叉树中的信息保存到文件中。
  • 第三步:源地址和目的地址之间建立 TCP 连接,源地址接收目的地址发送的文件。
  • 第四步:源地址调用 compare_info() 函数,将接收到的文件与自己的文件进行对比。将存在不同的文件通过 TCP 发送到目的地址。

  
  两边的目录对比完后,只需要在源地址每隔一段时间获取下文件信息,和上一次的文件信息对比即可。
  
  main() 函数的实现代码就不贴出来了,调用以上接口即可。

4、说明

  由于实际监视目录是挂载的 Windows 的共享目录,一些 Linux 下的文件同步工具自己测试时不支持,所以自己写了目录对比的功能。可能有其他文件同步工具自己没有找到,或者自己的目录对比上还存在问题,希望有大神指教。

你可能感兴趣的:(Linux)