8、一粒云二次封装与迁移 – 数据导出程序编写(接收端)

目录 未分类

1、数据接收程序创建

首先创建项目目录

mkdir yliyunDMC && cd yliyunDMC && npm ini

拉取库

npm i figlet chalk axios minimist request request-progress progress -d && index.js

在index.js中引入所需的库。

const figlet = require('figlet');
const chalk = require('chalk');
const axios = require('axios');
const minimist = require('minimist');
const fs = require('fs');
const path = require('path');
const request = require('request');
const progress = require('request-progress');
const ProgressBar = require("progress");

 

const args = minimist(process.argv.slice(2));

const URL = args.url;
const PATH = args.path;
const SIGN = args.sign;
const CACHE = args.cache;
const HELP = args.help;

引入参数

URL  DMS 地址

PATH 数据存放目录

SIGN DMS准入秘钥

CACHE 使用上一次的缓存记录

HELP 使用帮助

if(HELP !== undefined || URL === undefined || PATH === undefined || SIGN === undefined){
    console.log("\n");
    console.log("yliYunDmc --url " + chalk.yellow("<url>") + " --path " + chalk.yellow("<path>") + " --sign " + chalk.yellow("<sign>") + " --help " + chalk.yellow("<help>") + " --cache");
    console.log("\n");
    console.log("           --url <host>                DMS Url");
    console.log("           --path <path>               迁移数据目录");
    console.log("           --sign <sign>               DMS准入秘钥");
    console.log("           --cache                     使用上一次的进度");
    console.log("           --help <help>               使用帮助");
    console.log("\n");
    console.log("\n");
    return false;
}

帮助信息的输出

MacBook-Pro yliYunDMC % node app.js --help                                                                             


yliYunDmc --url <url> --path <path> --sign <sign> --help <help> --cache


           --url <host>                DMS Url
           --path <path>               迁移数据目录
           --sign <sign>               DMS准入秘钥
           --cache                     使用上一次的进度
           --help <help>               使用帮助




MacBook-Pro yliYunDMC %

进行输出测试

console.log("\n");
const logo = figlet.textSync("        yliYun    DMC");
console.log(logo);
console.log("\n");
console.log("              从yliYun DMS,读取结构并迁移数据到指定位置");
console.log("                          启动添加--help帮助");
console.log("\n");
console.log('##################################' + chalk.yellow.bold('校验') + '#################################');
console.log("\n");

UI显示,输出彩色字符。

const PersonalFilesLimit = 1;
const PublicFilesLimit = 1;
var PersonalFilesOffset = 0;=
var PersonalFileDownNumber = 0;
var PersonalFileNumber = 0;
var PublicFilesOffset = 0;
var PublicFileNumber = 0;
var PublicFileDownNumber = 0;
var cacheStatus = false;

初始化变量,PersonalFilesLimit个人文件每次读取数据库返回的文件量,PublicFilesLimit公共文件每次读取数据库返回的文件量。PersonalFilesOffset个人文件下载偏移量,完成一次PublicFilesLimit后进行一次PersonalFilesOffset + PublicFilesLimit的偏移,PersonalFileDownNumber个人文件已下载数量,PersonalFileNumber个人文件总量。公共文件变量与个人文件变量类似。

 

let cacheFileStatus = fs.existsSync(path.join(__dirname,"cache"));

if(CACHE && cacheFileStatus){
    let data = fs.readFileSync(path.join(__dirname,"cache"));
    data = JSON.parse(data.toString());
    PersonalFilesOffset = data.PersonalFilesOffset;
    PublicFilesOffset = data.PublicFilesOffset;
    PublicFileNumber = data.PublicFileNumber;
    PublicFileDownNumber = data.PublicFileDownNumber;
    PersonalFileDownNumber = data.PersonalFileDownNumber;
    PersonalFileNumber = data.PersonalFileNumber;
    cacheStatus = true;
}

if(CACHE && !cacheFileStatus){
    console.log("\n")
    console.log(chalk.yellow("                   注意!"))
    console.log(chalk.yellow("   没有在目录下找到cache文件,--cache参数无效"))
    console.log("\n")
    return false;
}

if(cacheFileStatus && !CACHE){
    console.log("\n")
    console.log(chalk.yellow("                注意!"))
    console.log(chalk.yellow("检测到目录下存在cache数据文件,程序发生错误"))
    console.log(chalk.yellow("时自动生成该文件文件会自动保存上一次进度。如果"))
    console.log(chalk.yellow("想要使用该文件,请在启动指令中加入--cache参数,"))
    console.log(chalk.yellow("如果不想使用,请手动删除该文件"))
    console.log("\n")
    return false;
}

cacheStatus && console.log("         1.缓存:                                                " + chalk.green("[OK]"));
!cacheStatus && console.log("         1.缓存:                                       " + chalk.green("[NO]"));

对缓存进行一个判断处理,如果当前目录下存在cache文件,并且使用了–cache指令,那么就读取cache并设置变量。当存在cache但没有指定–cache时进行一个提示避免因为误操作覆盖cache。当cache不存在但使用了–cache指令时,报错。缓存不对目录进行记录,因为目录生成会非常快。

/**
 * 抛出错误信息,立即保存进度,并退出程序
 * @param {*} message 
 */
const errorExit = function(message){
    console.log("错误: " + chalk.red(message));
}

/**
 * 记录缓存
 */
const cacheWrite = function(){
    fs.writeFileSync(path.join(__dirname,"cache"),JSON.stringify({
        PersonalFileNumber: PersonalFileNumber,
        PublicFileNumber: PublicFileNumber,
        PersonalFilesOffset: PersonalFilesOffset,
        PublicFilesOffset: PublicFilesOffset,
        PublicFileNumber: PublicFileNumber,
        PublicFileDownNumber: PublicFileDownNumber,
        PersonalFileDownNumber: PersonalFileDownNumber,
        PersonalFileDownNumber: PersonalFileDownNumber
    }));
}

错误抛出和缓存记录方法,当文件下载完成后进行一个缓存的记录。

1、个人目录结构

 

/**
 * 获取个人目录下可用目录的数量
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function getPersonalFolderNumber(url,sign,callback){
    axios.post("http://"+ url + "/personal/folder/number",{
        sign: sign
    }).then((res) => {
        if(res.data.status){
            console.log("\n")
            callback && callback(res.data.data[0]["count(*)"]);
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。0001",false);
    })
}

获取个人目录下面所有的可用目录数量方便进行定位和进度展示。

/**
 * 获取个人目录下可用目录的信息
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function getPersonalFolderInfo(url,sign,callback){
    axios.post("http://"+ url + "/personal/folder/info",{
        sign: sign
    }).then((res) => {
        if(res.data.status){
            console.log("\n")
            callback && callback(res.data.data);
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。0001",false);
    });
}

获取个人目录下可用目录的信息,所谓可用目录,就是没有被删除,状态正常的目录,请求回调的数据格式如下代码段,

获取数据之后,就可以使用parent_ids来反查出路径而creater_name则是用户名称,可以用来创建一级目录进行区分。

[
  { parent_ids: '-', file_name: '测试', creater_name: '系统管理员' },
  { parent_ids: '-3-', file_name: '一级目录', creater_name: '系统管理员' },
  { parent_ids: '-3-23-', file_name: '二级目录', creater_name: '系统管理员' }
]

 

/**
 * 使用文件id标记的路径去查找出个人目录真实到路径
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function getPersonalFolderRealPath(url,sign,vPath,createrName,callback){
    axios.post("http://"+ url + "/personal/folder/path",{
        sign: sign,
        path: vPath,
        createrName: createrName
    }).then((res) => {
        if(res.data.status){
            callback && callback(res.data.data);
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。0001",false);
    });
}

parent_ids反查方法,查询返回字符串路径。

{ status: true, data: [ '系统管理员', '测试' ] }
{ status: true, data: [ '系统管理员', '测试', '一级目录' ] }

 

/**
 * 遍历获取
 * @param {*} publicFolderInfo 
 * @param {*} continuedCallback 
 * @param {*} callback 
 */
var getPersonalFolderPathYield = undefined;
function * getPersonalFolderPath(url,sign,personalFolderInfo,continuedCallback,callback){
    if(personalFolderInfo.length === 0){
        callback && callback();
    }
    if(personalFolderInfo.length !== 0){
        let data = personalFolderInfo.shift();
        if(data.parent_ids === "-"){
        getPersonalFolderPathYield = getPersonalFolderPath(url,sign,personalFolderInfo,continuedCallback,callback);
        continuedCallback && continuedCallback([data.creater_name,data.file_name]);
        continuedCallback = undefined;callback = undefined;
        }else{
        getPersonalFolderRealPath(url,sign,data.parent_ids,data.creater_name,(rPath) => {
            rPath.push(data.file_name);
            getPersonalFolderPathYield = getPersonalFolderPath(url,sign,personalFolderInfo,continuedCallback,callback);
            continuedCallback && continuedCallback(rPath);
            continuedCallback = undefined;callback = undefined;
        })
        }
    }
    yield
    getPersonalFolderPathYield = getPersonalFolderPathYield(url,sign,publicFolderInfo,continuedCallback,callback);
    getPersonalFolderPathYield.next();
    continuedCallback = undefined;callback = undefined;
}

对路径进行一个分析,如果data.parent_ids是-则代表是根路径,如果不是则交给getPersonalFolderRealPath反查路径。

[
  { parent_ids: '-', file_name: '测试', creater_name: '系统管理员' },
  { parent_ids: '-3-', file_name: '一级目录', creater_name: '系统管理员' },
  { parent_ids: '-3-23-', file_name: '二级目录', creater_name: '系统管理员' }
]
[
  { parent_ids: '-3-', file_name: '一级目录', creater_name: '系统管理员' },
  { parent_ids: '-3-23-', file_name: '二级目录', creater_name: '系统管理员' }
]
[ { parent_ids: '-3-23-', file_name: '二级目录', creater_name: '系统管理员' } ]

 

/**
 * 创建个人文件夹结构
 * @param {*} url 
 * @param {*} sign 
 * @param {*} localPath 
 * @param {*} callback 
 */
function createPersonalFolder(url,sign,localPath,callback){
    getPersonalFolderNumber(url,sign,(personalFolderNumber) => {
        PersonalFolderNumber = personalFolderNumber;
        getPersonalFolderInfo(url,sign,(personalFolderInfo) => {
            PersonalFolderInfo = personalFolderInfo;
            getPersonalFolderPathYield = getPersonalFolderPath(url,sign,PersonalFolderInfo,(folders) => {
                !fs.existsSync(path.join(localPath,"个人")) && fs.mkdirSync(path.join(localPath,"个人"));
                localFolderCreate(path.join(localPath,"个人"),folders,() => {
                    getPersonalFolderPathYield.next();
               });
            },() => {
                callback && callback();
            })
            getPersonalFolderPathYield.next();
            
        });
    });
}

获取到文件在系统中的路径之后,还需要将它拼接为本地的路径。程序设计时,将公共和个人文件分别存放于“公共”“个人”文件下,而个人文件夹中将不同用户创建的文件分类存放,所以最终路径为/个人/xxx用户/文件结构…./….当成功拼接成此路径之后,就可以进行目录的创建。

MacBook-Pro yliYunDMC % node app.js --url 192.168.0.22:1115 --path /Users/fcy/Downloads/yliYunData --sign 123456


                _ ___   __                ____  __  __  ____ 
          _   _| (_) \ / /   _ _ __      |  _ \|  \/  |/ ___|
         | | | | | |\ V / | | | '_ \     | | | | |\/| | |    
         | |_| | | | | || |_| | | | |    | |_| | |  | | |___ 
          \__, |_|_| |_| \__,_|_| |_|    |____/|_|  |_|\____|
          |___/                                              


              从yliYun DMS,读取结构并迁移数据到指定位置
                          启动添加--help帮助


##################################校验#################################


         1.缓存:                                       [NO]
         2.DMS通讯:                                    [OK]

创建目录 : /Users/xxx/Downloads/yliYunData/个人/系统管理员
创建目录 : /Users/xxx/Downloads/yliYunData/个人/系统管理员/测试
创建目录 : /Users/xxx/Downloads/yliYunData/个人/系统管理员/测试/一级目录
创建目录 : /Users/xxx/Downloads/yliYunData/个人/系统管理员/测试/一级目录/二级目录
个人目录结构完成!

上面为创建个人文件结构的运行测试。

/**
 * 获取个人目录下有效文件的数量
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function getPersonalFileNumber(url,sign,callback){
    axios.post("http://"+ url + "/personal/files/number",{
        sign: sign
    }).then((res) => {
        if(res.data.status){
            !cacheStatus && (PersonalFileNumber = res.data.data[0]["count(*)"] - 1);
            console.log("\n")
            callback && callback(res.data.data[0]["count(*)"]);
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。",false);
    })
}

建立完个人文件夹之后就可以对个人文件进行接收。首先是获取个人目录下所有的有效文件的数量,这样可以对进度进行一个展示。

/**
 * 每次1个文件的进度获取个人文件的基本信息
 * @param {*} url DMS地址
 * @param {*} sign 通讯秘钥
 * @param {*} continuedCallback 每次读取完毕后的回调
 * @param {*} callback 完全读取完毕后的回调
 */

var getPersonalFileInfoYield = undefined;
function * getPersonalFileInfo(url,sign,continuedCallback,callback){
    if(PersonalFilesOffset > PersonalFileNumber){
        callback && callback();
        return false;
    }else{
        axios.post("http://"+ url + "/personal/files/info",{
            limit: PersonalFilesLimit,
            offset: PersonalFilesOffset,
            sign: sign
        }).then((res) => {
            if(res.data.status){
                PersonalFilesOffset = PersonalFilesOffset + PersonalFilesLimit;
                getPersonalFileInfoYield = getPersonalFileInfo(url,sign,continuedCallback,callback);
                continuedCallback && continuedCallback(res.data.data);
                continuedCallback = undefined; callback = undefined;
            }else{
                console.log("\n");
                errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确1",false);
            }        
        }).catch((err) => {
            console.log(err)
            console.log("\n");
            errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。1",false);
        })
    }
    yield;
    getPersonalFileInfoYield = getPersonalFileInfoYield(url,sign,continuedCallback,callback);
    continuedCallback = undefined; callback = undefined;
    getPersonalFileInfoYield.next();
}

每次从服务端获取一个需要下载的文件信息。

{
  status: true,
  data: [
    {
      fs_file_id: 142,
      parent_ids: '-3-23-24-',
      file_name: 'Diet Cola.js',
      creater_name: '系统管理员'
    }
  ]
}

每次获取回传的数据如上,该文件在fs_file表中的id,parent_ids字段拿来反查路径,file_name为文件的名称,文件存入文件系统之后命名是无法分辨的,需要靠此字段进行重命名。creater_name则为上传这个文件的用户名称,依靠这个来进行文件按用户归类存放。

/**
 * 获取个人文件在本地中应该所处的路径
 * @param {*} url DMS地址
 * @param {*} sign 通讯秘钥
 * @param {*} filesInfo getPersonalFileInfo返回的文件信息
 * @param {*} continuedCallback  每次读取完毕后的回调
 * @param {*} callback 完全读取完毕后的回调
 */
var getPersonalFileDfsPathYield = undefined;
function * getPersonalFileLocalPath(url,sign,filesInfo,continuedCallback,callback){
    if(filesInfo.length === 0){
        callback && callback();
    }
    if(filesInfo.length !== 0){
        let data = filesInfo.shift();
        if(data.parent_ids === "-"){
            getPersonalFileDfsPathYield = getPersonalFileLocalPath(url,sign,filesInfo,continuedCallback,callback);
            continuedCallback && continuedCallback({
                name: data.file_name,
                fsId: data.fs_file_id,
                path: [],
                createrName: data.creater_name
            });
            continuedCallback = undefined; callback = undefined;
        }
        if(data.parent_ids !== "-"){
            axios.post("http://"+ url + "/personal/files/path",{
                path: data.parent_ids,
                sign: sign
            }).then((res) => {
                if(res.data.status){
                    getPersonalFileDfsPathYield = getPersonalFileLocalPath(url,sign,filesInfo,continuedCallback,callback);
                    continuedCallback && continuedCallback({
                        name: data.file_name,
                        fsId: data.fs_file_id,
                        path: res.data.data,
                        createrName: data.creater_name
                    });
                    continuedCallback = undefined; callback = undefined;
                }else{
                    console.log("\n");
                    errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
                }        
            }).catch((err) => {
                console.log("\n");
                errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。",false);
            })
        }
    }
    yield;
    getPersonalFileDfsPathYield = getPersonalFileLocalPath(url,sign,filesInfo,continuedCallback,callback);
    continuedCallback = undefined; callback = undefined;
    getPersonalFileDfsPathYield.next();
}

对parent_ids进行反查,查出路径并返回。

/**
 * 下载公共文件
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */

function DownloadPersonalFiles(url,sign,localPath,callback){
    getPersonalFileNumber(url,sign,() => {
        getPersonalFileInfoYield = getPersonalFileInfo(url,sign,(filesInfo) => {
            getPersonalFileDfsPathYield = getPersonalFileLocalPath(url,sign,filesInfo,(filePathInfo) => {
                //地址补全
                
                filePathInfo.path.unshift(filePathInfo.createrName);
                filePathInfo.path.unshift("个人");
                filePathInfo.path.unshift(localPath);
                filePathInfo.path.push(filePathInfo.name);

                let localFilePathArray = path.join(...filePathInfo.path).split('');
                if(localFilePathArray.length > 48){
                    localFilePathArray.splice(10,10 + (localFilePathArray.length - 48));
                    localFilePathArray.splice(10, 0," .....")
                }

                console.log("个人文件 : " + chalk.green(filePathInfo.name));
                console.log("保存路径 : " + chalk.green(localFilePathArray.join('')));
                console.log("总进度   : " + chalk.green(PersonalFileDownNumber) + "/" + chalk.red(PersonalFileNumber));
                
                let percent = 0;let bar = new ProgressBar("进度     : [ :bar ] :percent%", { total: 50 });bar.tick(0);
                personalFilesDownload(url,sign,filePathInfo.fsId,path.join(...filePathInfo.path),(p) => {
                    bar.tick(((p.percent - percent) * 100) / 2,{
                        isSize: p.speed
                    });
                    percent = p.percent;
                },() => {
                    bar.tick(51);
                    console.log("\n");
                    PersonalFileDownNumber++;
                    cacheWrite();
                    getPersonalFileDfsPathYield.next();
                })
            },() => {
                getPersonalFileInfoYield.next();
            })
            getPersonalFileDfsPathYield.next();
        },() => {
            callback && callback();
        });
        getPersonalFileInfoYield.next();
    });
}

最初始的路径是以数组的方式存放,需要将它拼接成正常的路径,并交给personalFilesDownload函数进行下载,此函数会不停反馈下载进度。

/**
 * 下载远程文件
 * @param {*} url 地址
 * @param {*} localPath 下载目标路径
 * @param {*} downProgress 进度
 * @param {*} callback 下载完毕回调
 */
function personalFilesDownload(url,sign,fsId,localPath,downProgress,callback){
    axios.post("http://"+ url + "/personal/files/dfs/path",{
        id: fsId,
        sign: sign
    }).then((res) => {
        if(res.data.status){
            let [file] = res.data.data;
            progress(request("http://" + url + "/" + file.file_name),{
            //progress(request("http://localhost/test.txt"),{
                delay: 1000
            }).on('progress', function (state) {
                downProgress && downProgress(state)
            }).on('error', function (err) {
                errorExit("下载: " + url + "时发生错误,错误信息:" + err,true);
            }).on('end', function () {
                callback && callback();
            }).pipe(fs.createWriteStream(localPath));
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确1",false);
        }        
    }).catch((err) => {
        //console.log(err)
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启1。",false);
    })
}

从服务端下载文件。

2、公共目录结构

/**
 * 获取公共目录下可用目录的数量
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function getPublicFolderNumber(url,sign,callback){
    axios.post("http://"+ url + "/public/folder/number",{
        sign: sign
    }).then((res) => {
        console.log("         2.DMS通讯:                                    " + chalk.green("[OK]"));
        if(res.data.status){
            console.log("\n")
            callback && callback(res.data.data[0]["count(*)"]);
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。0001",false);
    })
}

获取公共目录下面所有的可用目录数量方便进行定位和进度展示。

/**
 * 获取公共目录下可用目录的信息
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function getPublicFolderInfo(url,sign,callback){
    axios.post("http://"+ url + "/public/folder/info",{
        sign: sign
    }).then((res) => {
        if(res.data.status){
            console.log("\n")
            callback && callback(res.data.data);
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。0001",false);
    });
}

获取公共目录下可用目录的信息,所谓可用目录,就是没有被删除,状态正常的目录,请求回调的数据格式如下代码段,

获取数据之后,就可以使用parent_ids来反查出路径。

 

/**
 * 使用文件id标记的路径去查找出公共目录真实到路径
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function getPublicFolderRealPath(url,sign,vPath,callback){
    axios.post("http://"+ url + "/public/folder/path",{
        sign: sign,
        path: vPath
    }).then((res) => {
        if(res.data.status){
            callback && callback(res.data.data);
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。0001",false);
    });
}

parent_ids反查方法,查询返回字符串路径。

/**
 * 创建公共文件夹结构
 * @param {*} url 
 * @param {*} sign 
 * @param {*} localPath 
 * @param {*} callback 
 */
function createPublicFolder(url,sign,localPath,callback){
    getPublicFolderNumber(url,sign,(publicFolderNumber) => {
        PublicFolderNumber = publicFolderNumber;
        getPublicFolderInfo(url,sign,(publicFolderInfo) => {
            PublicFolderInfo = publicFolderInfo;
            getPublicFolderPathYield = getPublicFolderPath(url,sign,PublicFolderInfo,(folders) => {
                !fs.existsSync(path.join(localPath,"公共")) && fs.mkdirSync(path.join(localPath,"公共"));
                localFolderCreate(path.join(localPath,"公共"),folders,() => {
                    getPublicFolderPathYield.next();
                });
            },() => {
                callback && callback();
            })
            getPublicFolderPathYield.next();
            
        });
    })
}

按反查路径进行目录创建,当存在多级目录时,调用下面的方法进行创建。

/**
 * 按照路径创建多级目录
 * @param {*} path 
 * @param {*} callback 
 */
function localFolderCreate(localPath,folders,callback){
    let folderPath = localPath;
    folders.map((item) => {
        folderPath = path.join(folderPath,item);
        let folderPathStatus = fs.existsSync(folderPath);
        if(!folderPathStatus){
            console.log("创建目录 : " + chalk.green(folderPath));
            fs.mkdirSync(folderPath)
        }
    });
    callback && callback();
}

 

2、目录结构测试

 

MacBook-Pro yliYunDMC % node app.js --url 192.168.0.22:1115 --path /Users/xxx/Downloads/yliYunData --sign 123456


                _ ___   __                ____  __  __  ____ 
          _   _| (_) \ / /   _ _ __      |  _ \|  \/  |/ ___|
         | | | | | |\ V / | | | '_ \     | | | | |\/| | |    
         | |_| | | | | || |_| | | | |    | |_| | |  | | |___ 
          \__, |_|_| |_| \__,_|_| |_|    |____/|_|  |_|\____|
          |___/                                              


              从yliYun DMS,读取结构并迁移数据到指定位置
                          启动添加--help帮助


##################################校验#################################


         1.缓存:                                       [NO]
         2.DMS通讯:                                    [OK]




创建目录 : /Users/xxx/Downloads/yliYunData/公共/测试
创建目录 : /Users/xxx/Downloads/yliYunData/公共/测试/一级目录
创建目录 : /Users/xxx/Downloads/yliYunData/公共/测试/一级目录/二级目录
公共目录结构完成! 3




创建目录 : /Users/xxx/Downloads/yliYunData/个人/系统管理员
创建目录 : /Users/xxx/Downloads/yliYunData/个人/系统管理员/测试
创建目录 : /Users/xxx/Downloads/yliYunData/个人/系统管理员/测试/一级目录
创建目录 : /Users/xxx/Downloads/yliYunData/个人/系统管理员/测试/一级目录/二级目录
个人目录结构完成!

执行DMC,完成目录创建。

校验

3、个人文件

/**
 * 获取个人目录下有效文件的数量
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function getPersonalFileNumber(url,sign,callback){
    axios.post("http://"+ url + "/personal/files/number",{
        sign: sign
    }).then((res) => {
        if(res.data.status){
            !cacheStatus && (PersonalFileNumber = res.data.data[0]["count(*)"] - 1);
            console.log("\n")
            callback && callback(res.data.data[0]["count(*)"]);
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。",false);
    })
}

获取个人账户下的所有有效文件数量,用作进度展示和缓存。

/**
 * 每次1个文件的进度获取个人文件的基本信息
 * @param {*} url DMS地址
 * @param {*} sign 通讯秘钥
 * @param {*} continuedCallback 每次读取完毕后的回调
 * @param {*} callback 完全读取完毕后的回调
 */

var getPersonalFileInfoYield = undefined;
function * getPersonalFileInfo(url,sign,continuedCallback,callback){
    if(PersonalFilesOffset > PersonalFileNumber){
        callback && callback();
        return false;
    }else{
        axios.post("http://"+ url + "/personal/files/info",{
            limit: PersonalFilesLimit,
            offset: PersonalFilesOffset,
            sign: sign
        }).then((res) => {
            if(res.data.status){
                console.log(res.data)
                PersonalFilesOffset = PersonalFilesOffset + PersonalFilesLimit;
                getPersonalFileInfoYield = getPersonalFileInfo(url,sign,continuedCallback,callback);
                continuedCallback && continuedCallback(res.data.data);
                continuedCallback = undefined; callback = undefined;
            }else{
                console.log("\n");
                errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确1",false);
            }        
        }).catch((err) => {
            console.log(err)
            console.log("\n");
            errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。1",false);
        })
    }
    yield;
    getPersonalFileInfoYield = getPersonalFileInfoYield(url,sign,continuedCallback,callback);
    continuedCallback = undefined; callback = undefined;
    getPersonalFileInfoYield.next();
}

获取文件的详细信息,fs_file_id,parent_ids,file_name和creater_name;

{
  status: true,
  data: [
    {
      fs_file_id: 145,
      parent_ids: '-3-23-24-',
      file_name: 'Crazy.js',
      creater_name: '系统管理员'
    }
  ]
}
/**
 * 获取个人文件在本地中应该所处的路径
 * @param {*} url DMS地址
 * @param {*} sign 通讯秘钥
 * @param {*} filesInfo getPersonalFileInfo返回的文件信息
 * @param {*} continuedCallback  每次读取完毕后的回调
 * @param {*} callback 完全读取完毕后的回调
 */
var getPersonalFileDfsPathYield = undefined;
function * getPersonalFileLocalPath(url,sign,filesInfo,continuedCallback,callback){
    if(filesInfo.length === 0){
        callback && callback();
    }
    if(filesInfo.length !== 0){
        let data = filesInfo.shift();
        if(data.parent_ids === "-"){
            getPersonalFileDfsPathYield = getPersonalFileLocalPath(url,sign,filesInfo,continuedCallback,callback);
            continuedCallback && continuedCallback({
                name: data.file_name,
                fsId: data.fs_file_id,
                path: [],
                createrName: data.creater_name
            });
            continuedCallback = undefined; callback = undefined;
        }
        if(data.parent_ids !== "-"){
            axios.post("http://"+ url + "/personal/files/path",{
                path: data.parent_ids,
                sign: sign
            }).then((res) => {
                if(res.data.status){
                    getPersonalFileDfsPathYield = getPersonalFileLocalPath(url,sign,filesInfo,continuedCallback,callback);
                    continuedCallback && continuedCallback({
                        name: data.file_name,
                        fsId: data.fs_file_id,
                        path: res.data.data,
                        createrName: data.creater_name
                    });
                    continuedCallback = undefined; callback = undefined;
                }else{
                    console.log("\n");
                    errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
                }        
            }).catch((err) => {
                console.log("\n");
                errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。",false);
            })
        }
    }
    yield;
    getPersonalFileDfsPathYield = getPersonalFileLocalPath(url,sign,filesInfo,continuedCallback,callback);
    continuedCallback = undefined; callback = undefined;
    getPersonalFileDfsPathYield.next();
}

获取到parent_ids后传给getPersonalFileLocalPath方法处理出本地路径,下面为处理后的结果。根据这个数据可以得出路径为 个人/系统管理员/测试/一级目录/二级目录/Diet Cola.js

{
  name: 'Diet Cola.js',
  fsId: 142,
  path: [ '测试', '一级目录', '二级目录' ],
  createrName: '系统管理员'
}
/**
 * 下载远程文件
 * @param {*} url 地址
 * @param {*} localPath 下载目标路径
 * @param {*} downProgress 进度
 * @param {*} callback 下载完毕回调
 */
function personalFilesDownload(url,sign,fsId,localPath,downProgress,callback){
    axios.post("http://"+ url + "/personal/files/dfs/path",{
        id: fsId,
        sign: sign
    }).then((res) => {
        if(res.data.status){
            let [file] = res.data.data;
            progress(request("http://" + url + "/" + file.file_name),{
                delay: 1000
            }).on('progress', function (state) {
                downProgress && downProgress(state)
            }).on('error', function (err) {
                errorExit("下载: " + url + "时发生错误,错误信息:" + err,true);
            }).on('end', function () {
                callback && callback();
            }).pipe(fs.createWriteStream(localPath));
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确1",false);
        }        
    }).catch((err) => {
        //console.log(err)
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启1。",false);
    })
}

获取到本地路径之后就需要获取远程路径,将文件下载下来放入本地路径就完成了一次文件拉取。使用获取到的fsId值去fs_file表查出文件在文件系统中的路径。

{
  file_name: 'group1/M00/00/01/wKgAFmPEuJ6AEr_jAABagZr3bXE6759.js',
  file_md5: 'c0de229948cc3d94c82c91c6a2455748'
}

因为在DMS中已经使用express以下面的方式挂载了文件系统存储节点,所以直接访问http://xxx.xxx.xx.xxx:xxx/group1/M00/00/01/wKgAFmPEuJ6AEr_jAABagZr3bXE6759.js即可下载该文件。

app.use(`/组/子节点`,express.static(挂载磁盘路径))

 

/**
 * 下载个人文件,并展示进度
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */

function DownloadPersonalFiles(url,sign,localPath,callback){
    getPersonalFileNumber(url,sign,() => {
        getPersonalFileInfoYield = getPersonalFileInfo(url,sign,(filesInfo) => {
            getPersonalFileDfsPathYield = getPersonalFileLocalPath(url,sign,filesInfo,(filePathInfo) => {
                //地址补全
                
                filePathInfo.path.unshift(filePathInfo.createrName);
                filePathInfo.path.unshift("个人");
                filePathInfo.path.unshift(localPath);
                filePathInfo.path.push(filePathInfo.name);

                let localFilePathArray = path.join(...filePathInfo.path).split('');
                if(localFilePathArray.length > 48){
                    localFilePathArray.splice(10,10 + (localFilePathArray.length - 48));
                    localFilePathArray.splice(10, 0," .....")
                }

                console.log("个人文件 : " + chalk.green(filePathInfo.name));
                console.log("保存路径 : " + chalk.green(localFilePathArray.join('')));
                console.log("总进度   : " + chalk.green(PersonalFileDownNumber) + "/" + chalk.red(PersonalFileNumber));
                
                let percent = 0;let bar = new ProgressBar("进度     : [ :bar ] :percent%", { total: 50 });bar.tick(0);
                personalFilesDownload(url,sign,filePathInfo.fsId,path.join(...filePathInfo.path),(p) => {
                    bar.tick(((p.percent - percent) * 100) / 2,{
                        isSize: p.speed
                    });
                    percent = p.percent;
                },() => {
                    bar.tick(51);
                    console.log("\n");
                    PersonalFileDownNumber++;
                    cacheWrite();
                    getPersonalFileDfsPathYield.next();
                })
            },() => {
                getPersonalFileInfoYield.next();
            })
            getPersonalFileDfsPathYield.next();
        },() => {
            callback && callback();
        });
        getPersonalFileInfoYield.next();
    });
}

进行个人文件下载,展示进度,并存储到对应路径。下载结果可以使用file_md5中的数据进行文件完整性校验。但此程序中不做此功能。

个人文件 : 测试.docx
保存路径 : /Users/xxx .....iYunData/个人/系统管理员/测试/测试.docx
总进度   : 0/305
进度     : [ ================================================== ] 100%


个人文件 : Diet Cola.js
保存路径 : /Users/xxx .....理员/测试/一级目录/二级目录/Diet Cola.js
总进度   : 1/305
进度     : [ ================================================== ] 100%


个人文件 : Crazy.js
保存路径 : /Users/xxx ...../系统管理员/测试/一级目录/二级目录/Crazy.js
总进度   : 2/305
进度     : [ ================================================== ] 100%


个人文件 : Def Leppard.js
保存路径 : /Users/xxx ...../测试/一级目录/二级目录/Def Leppard.js
总进度   : 3/305
进度     : [ ================================================== ] 100%


个人文件 : Decimal.js
保存路径 : /Users/xxx .....统管理员/测试/一级目录/二级目录/Decimal.js
总进度   : 4/305
进度     : [ ================================================== ] 100%


个人文件 : Cursive.js
保存路径 : /Users/xxx .....统管理员/测试/一级目录/二级目录/Cursive.js
总进度   : 5/305
进度     : [ ================================================== ] 100%


个人文件 : Wireshark-win64-3.6.10.exe
保存路径 : /Users/xxx .....试/Wireshark-win64-3.6.10.exe
总进度   : 6/305
进度     : [ ================================================== ] 100%

进行测试。

查看下载数据。

4、公共文件

公共账户文件获取流程和个人账户一样,不过公共文件不涉及用户分割。

/**
 * 获取公共目录下有效文件的数量
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function getPublicFileNumber(url,sign,callback){
    axios.post("http://"+ url + "/public/files/number",{
        sign: sign
    }).then((res) => {
        if(res.data.status){
            !cacheStatus && (PublicFileNumber = res.data.data[0]["count(*)"] - 1);
            console.log("\n")
            callback && callback(res.data.data[0]["count(*)"]);
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。",false);
    })
}

获取公共账户下的所有有效文件数量,用作进度展示和缓存。

 

/**
 * 每次1个文件的进度获取公共文件的基本信息
 * @param {*} url DMS地址
 * @param {*} sign 通讯秘钥
 * @param {*} continuedCallback 每次读取完毕后的回调
 * @param {*} callback 完全读取完毕后的回调
 */
var getPublicFileInfoYield = undefined;
function * getPublicFileInfo(url,sign,continuedCallback,callback){
    if(PublicFilesOffset > PublicFileNumber){
        callback && callback();
        return false;
    }else{
        axios.post("http://"+ url + "/public/files/info",{
            limit: PublicFilesLimit,
            offset: PublicFilesOffset,
            sign: sign
        }).then((res) => {
            if(res.data.status){
                PublicFilesOffset = PublicFilesOffset + PublicFilesLimit;
                getPublicFileInfoYield = getPublicFileInfo(url,sign,continuedCallback,callback);
                continuedCallback && continuedCallback(res.data.data);
                continuedCallback = undefined; callback = undefined;
            }else{
                console.log("\n");
                errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确1",false);
            }        
        }).catch((err) => {
            console.log(err)
            console.log("\n");
            errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。1",false);
        })
    }
    yield;
    getPublicFileInfoYield = getPublicFileInfoYield(url,sign,continuedCallback,callback);
    continuedCallback = undefined; callback = undefined;
    getPublicFileInfoYield.next();
}

获取文件的详细信息,fs_file_id,parent_ids,file_name,与个人文件不同的是,不需要使用creater_name字段。

 

/**
 * 获取公共文件在本地中应该所处的路径
 * @param {*} url DMS地址
 * @param {*} sign 通讯秘钥
 * @param {*} filesInfo getPublicFileInfo返回的文件信息
 * @param {*} continuedCallback  每次读取完毕后的回调
 * @param {*} callback 完全读取完毕后的回调
 */
var getPublicFileDfsPathYield = undefined;
function * getPublicFileLocalPath(url,sign,filesInfo,continuedCallback,callback){
    if(filesInfo.length === 0){
        callback && callback();
    }
    if(filesInfo.length !== 0){
        let data = filesInfo.shift();
        if(data.parent_ids === "-"){
            getPublicFileDfsPathYield = getPublicFileLocalPath(url,sign,filesInfo,continuedCallback,callback);
            continuedCallback && continuedCallback({
                name: data.file_name,
                fsId: data.fs_file_id,
                path: res.data.data
            });
            continuedCallback = undefined; callback = undefined;
        }
        if(data.parent_ids !== "-"){
            axios.post("http://"+ url + "/public/files/path",{
                path: data.parent_ids,
                sign: sign
            }).then((res) => {
                if(res.data.status){
                    getPublicFileDfsPathYield = getPublicFileLocalPath(url,sign,filesInfo,continuedCallback,callback);
                    continuedCallback && continuedCallback({
                        name: data.file_name,
                        fsId: data.fs_file_id,
                        path: res.data.data
                    });
                    continuedCallback = undefined; callback = undefined;
                }else{
                    console.log("\n");
                    errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确",false);
                }        
            }).catch((err) => {
                console.log("\n");
                errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启。",false);
            })
        }
    }
    yield;
    getPublicFileDfsPathYield = getPublicFileLocalPath(url,sign,filesInfo,continuedCallback,callback);
    continuedCallback = undefined; callback = undefined;
    getPublicFileDfsPathYield.next();
}

获取到parent_ids后传给getPersonalFileLocalPath方法处理出本地路径,下面为处理后的结果。根据这个数据可以得出路径为 公共/xxx/xxxx/xxxx/Diet Cola.js

 

/**
 * 下载远程文件
 * @param {*} url 地址
 * @param {*} localPath 下载目标路径
 * @param {*} downProgress 进度
 * @param {*} callback 下载完毕回调
 */
function publicFilesDownload(url,sign,fsId,localPath,downProgress,callback){
    axios.post("http://"+ url + "/public/files/dfs/path",{
        id: fsId,
        sign: sign
    }).then((res) => {
        if(res.data.status){
            let [file] = res.data.data;
            progress(request("http://" + url + "/" + file.file_name),{
                delay: 1000
            }).on('progress', function (state) {
                downProgress && downProgress(state)
            }).on('error', function (err) {
                errorExit("下载: " + url + "时发生错误,错误信息:" + err,true);
            }).on('end', function () {
                callback && callback();
            }).pipe(fs.createWriteStream(localPath));
        }else{
            console.log("\n");
            errorExit("无法从DMS获取文件数据,请确认通讯秘钥是否正确1",false);
        }        
    }).catch((err) => {
        console.log("\n");
        errorExit("与DMS通讯失败,请检查网络链接是否正常,DMS对应端口是否开启1。",false);
    })
}

获取到本地路径之后就需要获取远程路径,将文件下载下来放入本地路径就完成了一次文件拉取。使用获取到的fsId值去fs_file表查出文件在文件系统中的路径。

 

/**
 * 下载公共文件
 * @param {*} url 
 * @param {*} sign 
 * @param {*} callback 
 */
function DownloadPublicFiles(url,sign,localPath,callback){
    getPublicFileNumber(url,sign,() => {
        getPublicFileInfoYield = getPublicFileInfo(url,sign,(filesInfo) => {
            getPublicFileDfsPathYield = getPublicFileLocalPath(url,sign,filesInfo,(filePathInfo) => {
                //地址补全
                filePathInfo.path.unshift("公共");filePathInfo.path.unshift(localPath);filePathInfo.path.push(filePathInfo.name);

                let localFilePathArray = path.join(...filePathInfo.path).split('');
                if(localFilePathArray.length > 48){
                    localFilePathArray.splice(10,10 + (localFilePathArray.length - 48));
                    localFilePathArray.splice(10, 0," .....");
                }

                console.log("公共文件 : " + chalk.green(filePathInfo.name));
                console.log("保存路径 : " + chalk.green(localFilePathArray.join('')));
                console.log("总进度   : " + chalk.green(PublicFileDownNumber) + "/" + chalk.red(PublicFileNumber));
                
                let percent = 0;let bar = new ProgressBar("进度     : [ :bar ] :percent%", { total: 50 });bar.tick(0);
                publicFilesDownload(url,sign,filePathInfo.fsId,path.join(...filePathInfo.path),(p) => {
                    bar.tick(((p.percent - percent) * 100) / 2,{
                        isSize: p.speed
                    });
                    percent = p.percent;
                },() => {
                    bar.tick(51);
                    console.log("\n");
                    PublicFileDownNumber++;
                    cacheWrite();
                    getPublicFileDfsPathYield.next();
                })
            },() => {
                getPublicFileInfoYield.next();
            })
            getPublicFileDfsPathYield.next();
        },() => {
            callback && callback();
        });
        getPublicFileInfoYield.next();
    });
}

进行公共文件下载,展示进度,并存储到对应路径。下载结果可以使用file_md5中的数据进行文件完整性校验。但此程序中不做此功能。

createPublicFolder(URL,SIGN,PATH,() => {
    createPersonalFolder(URL,SIGN,PATH,() => {
        DownloadPublicFiles(URL,SIGN,PATH,() => {
            console.log(`公共数据下载完成!共下载文件${PublicFileDownNumber--}个`);
             DownloadPersonalFiles(URL,SIGN,PATH,() => {
                 console.log(`个人数据下载完成!共下载文件${PersonalFileDownNumber--}个`);
             })
        });
    })
});

执行创建目录和拉取文件。

5、测试

root@yliyun:/opt/yliYunDMS# ./bin/node-linux-x64 app.js --P yliyun123@ --s 123456 --dg group1 --dp M00=/yliyun_data_01/data@M01=/yliyun_data_02/data@M02=/yliyun_data_03/data@M03=/yliyun_data_04/data


                _ ___   __                ____  __  __ ____  
          _   _| (_) \ / /   _ _ __      |  _ \|  \/  / ___| 
         | | | | | |\ V / | | | '_ \     | | | | |\/| \___ \ 
         | |_| | | | | || |_| | | | |    | |_| | |  | |___) |
          \__, |_|_| |_| \__,_|_| |_|    |____/|_|  |_|____/ 
          |___/                                              


            配合yliYun DMC,将YliYun系统中的目录结构与文件
                           完整的迁移至外部
                             --help帮助




#################################数据库################################
         1.地址:                                    localhost
         2.用户名:                                  root
         3.密码:                                    yliyun123@
         4.端口:                                    3306
         5.连接表:                                  yliyun
         5.数据库:                                  [OK]
         6.目录记录:                                3
         7.文件记录:                                519


##################################服务#################################
         1.迁移服务:                                [OK]
         2.地址:                                    :::1115
         3.密钥:                                    123456


##################################存储#################################
         组:                                        group1
         M00:                                       /yliyun_data_01/data
         M01:                                       /yliyun_data_02/data
         M02:                                       /yliyun_data_03/data
         M03:                                       /yliyun_data_04/data

服务端执行DMS

MacBook-Pro yliYunDMC % node app.js --url 192.168.0.22:1115 --path /Users/xxx/Downloads/yliYunData --sign 123456


                _ ___   __                ____  __  __  ____ 
          _   _| (_) \ / /   _ _ __      |  _ \|  \/  |/ ___|
         | | | | | |\ V / | | | '_ \     | | | | |\/| | |    
         | |_| | | | | || |_| | | | |    | |_| | |  | | |___ 
          \__, |_|_| |_| \__,_|_| |_|    |____/|_|  |_|\____|
          |___/                                              


              从yliYun DMS,读取结构并迁移数据到指定位置
                          启动添加--help帮助


##################################校验#################################


         1.缓存:                                       [NO]
         2.DMS通讯:                                    [OK]










公共文件 : .travis(1).yml
保存路径 : /Users/xxx ...../测试/一级目录/二级目录/.travis(1).yml
总进度   : 0/4
进度     : [ ================================================== ] 100%%


公共文件 : .eslintrc
保存路径 : /Users/xxx .....ta/公共/测试/一级目录/二级目录/.eslintrc
总进度   : 1/4
进度     : [ ================================================== ] 100%%


公共文件 : .editorconfig
保存路径 : /Users/xxx .....共/测试/一级目录/二级目录/.editorconfig
总进度   : 2/4
进度     : [ ================================================== ] 100%%


公共文件 : .jscs.json
保存路径 : /Users/xxx .....a/公共/测试/一级目录/二级目录/.jscs.json
总进度   : 3/4
进度     : [ ================================================== ] 100%%


公共文件 : .travis(2).yml
保存路径 : /Users/xxx ...../测试/一级目录/二级目录/.travis(2).yml
总进度   : 4/4
进度     : [ ================================================== ] 100%%


公共数据下载完成!共下载文件5个


个人文件 : 测试.docx
保存路径 : /Users/xxx .....iYunData/个人/系统管理员/测试/测试.docx
总进度   : 0/6
进度     : [ ================================================== ] 100%%


个人文件 : pm2(1)
保存路径 : /Users/xxx .....Data/个人/系统管理员/测试/一级目录/pm2(1)
总进度   : 1/6
进度     : [ ================================================== ] 100%%


个人文件 : package(1).json
保存路径 : /Users/xxx .....统管理员/测试/一级目录/package(1).json
总进度   : 2/6
进度     : [ ================================================== ] 100%%


个人文件 : package(2).json
保存路径 : /Users/xxx .....统管理员/测试/一级目录/package(2).json
总进度   : 3/6
进度     : [ ================================================== ] 100%%


个人文件 : Worker.js
保存路径 : /Users/xxx .....系统管理员/测试/一级目录/二级目录/Worker.js
总进度   : 4/6
进度     : [ ================================================== ] 100%%


个人文件 : which.js
保存路径 : /Users/xxx ...../系统管理员/测试/一级目录/二级目录/which.js
总进度   : 5/6
进度     : [ ================================================== ] 100%%


个人文件 : xdg-open
保存路径 : /Users/xxx ...../系统管理员/测试/一级目录/二级目录/xdg-open
总进度   : 6/6
进度     : [ ================================================== ] 100%%


个人数据下载完成!共下载文件7个

DMC拉取数据。

数据校验。