downloadControllerv2.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. let OSS = require('ali-oss');
  2. let fs = require('fs');
  3. let path = require('path');
  4. var archiver = require('archiver');
  5. let request = require("request");
  6. let http = require('http');
  7. let compressPercent = 0;
  8. let taskCodeQueue = [];
  9. let taskDownloaderQueue = {};
  10. let pathConfig = {
  11. ossPrefix: 'https://hongwei-4dkankan.oss-cn-shenzhen.aliyuncs.com/', // oss前缀
  12. // serverPrefix: 'http://192.168.0.208:8887/', // 服务器前缀
  13. serverPrefix: 'https://www.ysdli.com/download_scene/controller/', // 服务器前缀
  14. localDataName: 'localData',
  15. layer: './', // 层级
  16. rootFold: 'tmpData', // 根目录文件夹
  17. staticPath: [ // 静态数据路径
  18. 'static'
  19. ],
  20. paths: [ // images/data路径
  21. 'images/images',
  22. 'data/data',
  23. 'voice/voice',
  24. 'video/video'
  25. ],
  26. editPaths:[
  27. 'images/images',
  28. 'data/data',
  29. 'voice/voice'
  30. ],
  31. filters: [],
  32. downloadUrlServer:'https://www.ysdli.com/'
  33. }
  34. function getEditDataUrl(num,cb = ()=>{}) {
  35. let URL = `https://www.ysdli.com/api/scene/getEditDataUrl?num=${num}`
  36. let data = ''
  37. request(URL,(error, response, body)=>{
  38. if (!error) {
  39. let temp = JSON.parse(body)
  40. if (temp.code==0) {
  41. data=temp.data
  42. cb(data)
  43. }
  44. }
  45. })
  46. }
  47. function downloader() {
  48. this.objArr = [];
  49. this.completeFolds = 0; // 记录已经递归完成的目录, completeFolds == foldNum, 表示所有目录递归完成
  50. this.fileNum = 0; // 需要下载的文件总数
  51. this.completeFileNum = 0; // 已经下载完成的文件总数
  52. this.threadNum = 10; // 同时并发的线程总数
  53. this.curActiveThread = 0;
  54. this.sta = 0;
  55. this.downloadProcess = 0; // 下载进度
  56. this.zipProcess = 0; // 压缩进度
  57. this.sceneCode = ''; // 场景码
  58. this.sceneCodeArr = [] // 场景码数据
  59. this.snCode = '' //sn码
  60. this.isTiles = true; // 是否下载tiles
  61. this.sceneInfo = ''; // 场景的其他json数据
  62. this.downloadResponse = null;
  63. this.timer = null; // 定时器
  64. this.dirPath = path.join(__dirname, pathConfig.rootFold);
  65. this.foldNum = pathConfig.paths.length + pathConfig.staticPath.length; // 记录目录的总数, 用于判断是否目录是否递归完成
  66. this.client = new OSS({ // 配置阿里云oss
  67. region: 'oss-cn-shenzhen',
  68. accessKeyId: 'LTAIUrvuHqj8pvry',
  69. accessKeySecret: 'JLOVl0k8Ke0aaM8nLMMiUAZ3EiiqI4',
  70. bucket: 'hongwei-4dkankan',
  71. })
  72. }
  73. /**
  74. * 创建文件夹
  75. * @param data 文件路径
  76. */
  77. downloader.prototype.createRootFold = function (data) {
  78. if (!fs.existsSync(this.dirPath)) {
  79. fs.mkdirSync(this.dirPath);
  80. } else {
  81. // console.log('文件夹已存在');
  82. }
  83. this.dirPath = path.join(this.dirPath, '/' + data);
  84. // 创建文件夹目录
  85. if (!fs.existsSync(this.dirPath)) {
  86. fs.mkdirSync(this.dirPath);
  87. } else {
  88. // console.log('文件夹已存在');
  89. }
  90. console.log('-------dirPath---------',this.dirPath)
  91. }
  92. /**
  93. * 递归列举编辑页面需要下载的文件, 记录文件夹以及所有文件的总数
  94. */
  95. downloader.prototype.listEditDir = async function (editUrl) {
  96. let that = this;
  97. editUrl.forEach(item=>{
  98. let dirArr = item.split('/')
  99. let objName = dirArr.pop()
  100. let tmpDir = '';
  101. let childDirPath;
  102. dirArr.forEach((sub) => { // 对于路径中的每一个层级, 都创建对应的文件夹
  103. tmpDir += '/' + sub;
  104. childDirPath = path.join(this.dirPath, '/' + tmpDir); // 子路径
  105. if (!fs.existsSync(childDirPath)) {
  106. fs.mkdirSync(childDirPath);
  107. // console.log('创建文件夹成功: ' + childDirPath);
  108. } else {
  109. // console.log('文件夹已存在');
  110. }
  111. });
  112. let downloadURL = pathConfig.downloadUrlServer + item; // 记录每个文件需要下载的url
  113. if (objName === '') { // 过滤非法文件, 因为阿里云有时候会把文件夹当作文件返回,
  114. return;
  115. }
  116. let writeURL = path.join(childDirPath, objName); // 下载完成后, 应当写入的路径
  117. let tack = { // 创建下载任务
  118. downloadURL: encodeURI(downloadURL),
  119. writeURL: writeURL
  120. }
  121. that.objArr.push(tack);
  122. // this.completeFolds++;
  123. // this.timer = setInterval(this.download.bind(this), 16); // 开始下载
  124. })
  125. }
  126. /**
  127. * 递归列举所有需要下载的文件, 记录文件夹以及所有文件的总数
  128. */
  129. downloader.prototype.listDir = async function (dir, prefixDir) {
  130. let that = this;
  131. let filters = pathConfig.filters
  132. if (!that.isTiles) {
  133. filters.push('tiles')
  134. }
  135. for (let i = 0; i < filters.length; i++) { // 有些文件夹不需要下载, 则进行过滤
  136. let filter = filters[i];
  137. if (dir.indexOf(filter) > 0) {
  138. console.log('过滤的路径' + dir);
  139. that.completeFolds++;
  140. if (that.completeFolds === that.foldNum) { // 已经列举完成的文件夹数目等于需要下载的文件夹总数
  141. that.fileNum = that.objArr.length;
  142. that.timer = setInterval(that.download, 16); // 开始下载
  143. }
  144. return;
  145. }
  146. }
  147. dirArr = `${prefixDir}${dir}`.split('/'); // 分割其目录层级
  148. let tmpDir = '';
  149. let childDirPath;
  150. dirArr.forEach((item) => { // 对于路径中的每一个层级, 都创建对应的文件夹
  151. tmpDir += '/' + item;
  152. childDirPath = path.join(this.dirPath, '/' + tmpDir); // 子路径
  153. if (!fs.existsSync(childDirPath)) {
  154. fs.mkdirSync(childDirPath);
  155. // console.log('创建文件夹成功: ' + childDirPath);
  156. } else {
  157. // console.log('文件夹已存在');
  158. }
  159. });
  160. // 列举当前文件夹中的所有文件, 最多1000个
  161. let result = await this.client.list({
  162. prefix: dir,
  163. delimiter: '/',
  164. 'max-keys': 1000 // 最大限制1000
  165. });
  166. // result.objects就是当前文件夹下的所有文件
  167. result.objects && result.objects.forEach(async function (obj) {
  168. let downloadURL = pathConfig.ossPrefix + obj.name; // 记录每个文件需要下载的url
  169. let arr = obj.name.split('/');
  170. let objName = arr[arr.length - 1]; // 获取文件名, 上面的obj.name除了文件名, 还包含了文件所属的路径
  171. if (objName === '') { // 过滤非法文件, 因为阿里云有时候会把文件夹当作文件返回,
  172. return;
  173. }
  174. let writeURL = path.join(childDirPath, objName); // 下载完成后, 应当写入的路径
  175. let tack = { // 创建下载任务
  176. downloadURL: encodeURI(downloadURL),
  177. writeURL: writeURL
  178. }
  179. that.objArr.push(tack); // 下载任务入列
  180. });
  181. if (result.prefixes) { // 如果当前目录还有子目录存在, 则继续递归
  182. this.foldNum--; // 移除当前目录, 因为当前目录还不是最深层级
  183. let that = this;
  184. result.prefixes.forEach(function (subDir) {
  185. that.foldNum++; // 记录当前目录的子目录
  186. that.listDir(subDir, prefixDir);
  187. });
  188. } else { // 当前目录已经递归完成
  189. this.completeFolds++;
  190. console.log(this.completeFolds + ' ' + this.foldNum)
  191. if (this.completeFolds === this.foldNum) { // 已经列举完成的文件夹数目等于需要下载的文件夹总数
  192. // console.log(this.objArr)
  193. this.fileNum = this.objArr.length; // 记录需要下载的文件总数
  194. this.timer = setInterval(this.download.bind(this), 16); // 开始下载
  195. }
  196. }
  197. }
  198. downloader.prototype.download = function () {
  199. if (this.objArr.length === 0) { // 所有文件已经下载完成, 清楚定时器
  200. clearInterval(this.timer);
  201. this.timer = null;
  202. return;
  203. }
  204. if (this.curActiveThread <= this.threadNum) { // 当前活跃的线程数<总线程数, 则继续创建创建下载队列, 控制并发
  205. let task = this.objArr.shift(); // 取队列中的第一个任务
  206. let stream = fs.createWriteStream(task.writeURL); // 根据任务中的写入路径创建流
  207. let readStream = request(task.downloadURL).on('error', function () { // 通过下载路径去请求文件
  208. console.log("文件[" + task.downloadURL + "]下载出错 ");
  209. }).pipe(stream);
  210. let that = this;
  211. readStream.on('finish', function () { // 当前流下载完成, 释放线程
  212. that.curActiveThread--;
  213. })
  214. stream.on('finish', function () { // 文件写入完毕
  215. that.completeFileNum++; // 已经下载完成的文件数+1
  216. that.downloadProcess = that.completeFileNum / that.fileNum * 100; // 计算下载进度
  217. if (that.completeFileNum === that.fileNum) { // 所有的oss文件已下载完
  218. let sceneCode = that.sceneCode; // 记录场景码
  219. let sceneCodeArr = that.sceneCodeArr; // 记录场景码数组
  220. let snCode = that.snCode
  221. let sceneCodePath = path.join(__dirname, `tmpData/${snCode}/code.txt`);
  222. fs.writeFileSync(sceneCodePath , '')
  223. sceneCodeArr.forEach((item,idx)=>{
  224. // sceneData.json的写入路径, sceneData.json记录的是场景中的其他信息, 如场景描述、场景密码等可配置信息
  225. let sceneJsonPath = path.join(__dirname, `tmpData/${snCode}/static/images/images${item.sceneCode}/sceneData.json`);
  226. // code.txt的写入路径, 其记录的是场景码, 本地浏览器需要读取该文件
  227. fs.writeFileSync(sceneJsonPath, item.sceneInfo, function (err) { // 写入sceneInfo.json
  228. if (err) {
  229. return console.log(`写入文件出错: ${err}`);
  230. }
  231. })
  232. fs.writeFileSync(sceneCodePath , item.sceneCode+(idx===sceneCodeArr.length-1?'':','),{flag:'a'}, function (err,code) { // 将场景码写入
  233. console.log('-----code-----',code)
  234. })
  235. })
  236. console.log('开始压缩' + that.sceneCode)
  237. // let zipStream = fs.createWriteStream(`${__dirname}/tmpData/${that.sceneCode}/` + `/localData.zip`);
  238. let zipStream = fs.createWriteStream(`${__dirname}/tmpData/zip/` + `${snCode}.zip`); // 创建压缩包
  239. pathConfig.localDataName = snCode;
  240. let archive = archiver('zip', {
  241. zlib: {
  242. level: 9 // 压缩等级
  243. }
  244. });
  245. zipStream.on('close', function () {
  246. let respData = 'archiver has been finalized and the output file descriptor has closed.';
  247. console.log(archive.pointer() + ' total bytes');
  248. console.log('文件压缩已写入完成');
  249. // 重置两个进度
  250. downloadPercent = 0;
  251. compressPercent = 0;
  252. });
  253. // 计算压缩进度
  254. archive.on('progress', function (process) {
  255. compressPercent = that.zipProcess = process.fs.processedBytes / process.fs.totalBytes * 100;
  256. })
  257. archive.on('error', function (err) {
  258. console.log(err);
  259. })
  260. archive.pipe(zipStream);
  261. archive.directory(`${__dirname}/tmpData/${snCode}/`, false); // 将已经下载完成的全部内容放入至压缩流中
  262. archive.directory(`${__dirname}/static/page`, false); // 追加html文件
  263. archive.directory(`${__dirname}/browser/`, false); // 将浏览器放入压缩流中
  264. archive.finalize();
  265. }
  266. })
  267. this.curActiveThread++; // 激活的线程数+1
  268. }
  269. }
  270. downloader.prototype.execute = function (data) {
  271. // let arr = ['t-updScXn','t-J3VGU9J']
  272. this.createRootFold(this.snCode);
  273. this.sceneCodeArr.forEach(item => {
  274. // getEditDataUrl(item.sceneCode, (res)=>{
  275. // this.listEditDir(res);
  276. pathConfig.paths.forEach(path => {
  277. this.listDir(path + item.sceneCode, `static/`);
  278. })
  279. pathConfig.staticPath.forEach(path => {
  280. this.listDir(path, '');
  281. })
  282. // })
  283. })
  284. }
  285. /**
  286. * 起始函数
  287. * @param {*} response 响应
  288. * @param {*} data 前端传过来的数据
  289. */
  290. function start(response, data) {
  291. let sceneCode = data.sceneCode;
  292. let sceneCodeArr = data.sceneCodeArr
  293. let snCode = data.snCode
  294. let respData;
  295. // 如果已经存在压缩包, 则直接返回压缩包的路径给浏览器进行下载
  296. if (fs.existsSync(`${__dirname}/${pathConfig.rootFold}/${snCode}/${pathConfig.localDataName}.zip`)) {
  297. respData = {
  298. sta: 1003, // 1003 文件已存在
  299. data: {
  300. percent: 100,
  301. url: `${pathConfig.serverPrefix}${pathConfig.rootFold}/${snCode}/${pathConfig.localDataName}.zip`
  302. },
  303. msg: '文件已存在, 直接返回url'
  304. };
  305. // console.log('文件已存在, 直接返回url');
  306. } else {
  307. respData = {
  308. sta: 1002, // 1002
  309. data: {
  310. percent: 0
  311. },
  312. msg: '已加入任务队列'
  313. };
  314. taskCodeQueue.push(snCode);
  315. taskDownloaderQueue[snCode] = new downloader(); // 对于每个前端发来的下载请求, 创建一个downloader实例
  316. // taskDownloaderQueue[snCode].sceneCode = data.sceneCode;
  317. taskDownloaderQueue[snCode].sceneCodeArr = sceneCodeArr;
  318. taskDownloaderQueue[snCode].snCode = snCode;
  319. // taskDownloaderQueue[snCode].sceneCode = data.sceneCode;
  320. taskDownloaderQueue[snCode].isTiles = data.isTiles;
  321. // taskDownloaderQueue[snCode].sceneInfo = data.sceneInfo; // sceneInfo就是场景中其他的可配置信息, 用于写入sceneData.json中
  322. }
  323. let json = JSON.stringify(respData)
  324. response.send(json);
  325. }
  326. /**
  327. * 查询下载进度
  328. * @param {*} response 响应
  329. * @param {*} data 前端传过来的数据
  330. */
  331. function downloadProcess(response, data) {
  332. let snCode = data.snCode;
  333. let downloader = taskDownloaderQueue[snCode];
  334. if (!downloader) return;
  335. if (downloader.downloadProcess < 100) { // 文件正在下载中
  336. let respData = {
  337. sta: 1000, // 状态码 1000-文件正在下载 1001-文件正在压缩
  338. data: {
  339. percent: downloader.downloadProcess
  340. },
  341. msg: '文件下载中'
  342. };
  343. let json = JSON.stringify(respData)
  344. response.send(json);
  345. } else { // 文件正在压缩中
  346. let respData = {
  347. sta: 1001, // 状态码 1000-文件正在下载 1001-文件正在压缩
  348. data: {
  349. percent: downloader.zipProcess
  350. },
  351. msg: '文件压缩中'
  352. };
  353. if (downloader.zipProcess === 100) {
  354. respData.data.url = `${pathConfig.serverPrefix}${pathConfig.rootFold}/zip/${pathConfig.localDataName}.zip`
  355. setTimeout(() => {
  356. fs.unlink(`${__dirname}/tmpData/zip/` + `${pathConfig.localDataName}.zip`,(err)=>{
  357. if(err)
  358. return err;
  359. console.log('删除成功');
  360. })
  361. }, 1000 * 60 * 60 * 24);
  362. }
  363. let json = JSON.stringify(respData)
  364. response.send(json);
  365. }
  366. }
  367. (function () {
  368. // 服务器的downloader队列, 每隔1s处理一个前端的下载请求, 避免服务器压力过大
  369. setInterval(function () {
  370. let taskCode = taskCodeQueue.shift();
  371. if (taskCode) {
  372. taskDownloaderQueue[taskCode].execute(taskCode);
  373. }
  374. }, 1000)
  375. })()
  376. exports.startvt = start;
  377. exports.downloadProcessvt = downloadProcess;