downloader.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. /**
  2. * LRU 文件存储,使用该 downloader 可以让下载的文件存储在本地,下次进入小程序后可以直接使用
  3. * 详细设计文档可查看 https://juejin.im/post/5b42d3ede51d4519277b6ce3
  4. */
  5. const util = require('./util');
  6. const sha1 = require('./sha1');
  7. const SAVED_FILES_KEY = 'savedFiles';
  8. const KEY_TOTAL_SIZE = 'totalSize';
  9. const KEY_PATH = 'path';
  10. const KEY_TIME = 'time';
  11. const KEY_SIZE = 'size';
  12. // 可存储总共为 6M,目前小程序可允许的最大本地存储为 10M
  13. let MAX_SPACE_IN_B = 6 * 1024 * 1024;
  14. let savedFiles = {};
  15. export default class Dowloader {
  16. constructor() {
  17. // app 如果设置了最大存储空间,则使用 app 中的
  18. if (getApp().PAINTER_MAX_LRU_SPACE) {
  19. MAX_SPACE_IN_B = getApp().PAINTER_MAX_LRU_SPACE;
  20. }
  21. wx.getStorage({
  22. key: SAVED_FILES_KEY,
  23. success: function (res) {
  24. if (res.data) {
  25. savedFiles = res.data;
  26. }
  27. },
  28. });
  29. }
  30. /**
  31. * 下载文件,会用 lru 方式来缓存文件到本地
  32. * @param {String} url 文件的 url
  33. */
  34. download(url, lru) {
  35. return new Promise((resolve, reject) => {
  36. if (!(url && util.isValidUrl(url))) {
  37. resolve(url);
  38. return;
  39. }
  40. const fileName = getFileName(url);
  41. if (!lru) {
  42. // 无 lru 情况下直接判断 临时文件是否存在,不存在重新下载
  43. wx.getFileInfo({
  44. filePath: fileName,
  45. success: () => {
  46. resolve(url);
  47. },
  48. fail: () => {
  49. if (util.isOnlineUrl(url)) {
  50. downloadFile(url, lru).then((path) => {
  51. resolve(path);
  52. }, () => {
  53. reject();
  54. });
  55. } else if (util.isDataUrl(url)) {
  56. transformBase64File(url, lru).then(path => {
  57. resolve(path);
  58. }, () => {
  59. reject();
  60. });
  61. }
  62. },
  63. })
  64. return
  65. }
  66. const file = getFile(fileName);
  67. if (file) {
  68. if (file[KEY_PATH].indexOf('//usr/') !== -1) {
  69. wx.getFileInfo({
  70. filePath: file[KEY_PATH],
  71. success() {
  72. resolve(file[KEY_PATH]);
  73. },
  74. fail(error) {
  75. console.error(`base64 file broken, ${JSON.stringify(error)}`);
  76. transformBase64File(url, lru).then(path => {
  77. resolve(path);
  78. }, () => {
  79. reject();
  80. });
  81. }
  82. })
  83. } else {
  84. // 检查文件是否正常,不正常需要重新下载
  85. wx.getSavedFileInfo({
  86. filePath: file[KEY_PATH],
  87. success: (res) => {
  88. resolve(file[KEY_PATH]);
  89. },
  90. fail: (error) => {
  91. console.error(`the file is broken, redownload it, ${JSON.stringify(error)}`);
  92. downloadFile(url, lru).then((path) => {
  93. resolve(path);
  94. }, () => {
  95. reject();
  96. });
  97. },
  98. });
  99. }
  100. } else {
  101. if (util.isOnlineUrl(url)) {
  102. downloadFile(url, lru).then((path) => {
  103. resolve(path);
  104. }, () => {
  105. reject();
  106. });
  107. } else if (util.isDataUrl(url)) {
  108. transformBase64File(url, lru).then(path => {
  109. resolve(path);
  110. }, () => {
  111. reject();
  112. });
  113. }
  114. }
  115. });
  116. }
  117. }
  118. function getFileName(url) {
  119. if (util.isDataUrl(url)) {
  120. const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(url) || [];
  121. const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
  122. return fileName;
  123. } else {
  124. return url;
  125. }
  126. }
  127. function transformBase64File(base64data, lru) {
  128. return new Promise((resolve, reject) => {
  129. const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
  130. if (!format) {
  131. console.error('base parse failed');
  132. reject();
  133. return;
  134. }
  135. const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
  136. const path = `${wx.env.USER_DATA_PATH}/${fileName}`;
  137. const buffer = wx.base64ToArrayBuffer(bodyData.replace(/[\r\n]/g, ""));
  138. wx.getFileSystemManager().writeFile({
  139. filePath: path,
  140. data: buffer,
  141. encoding: 'binary',
  142. success() {
  143. wx.getFileInfo({
  144. filePath: path,
  145. success: (tmpRes) => {
  146. const newFileSize = tmpRes.size;
  147. lru ? doLru(newFileSize).then(() => {
  148. saveFile(fileName, newFileSize, path, true).then((filePath) => {
  149. resolve(filePath);
  150. });
  151. }, () => {
  152. resolve(path);
  153. }) : resolve(path);
  154. },
  155. fail: (error) => {
  156. // 文件大小信息获取失败,则此文件也不要进行存储
  157. console.error(`getFileInfo ${path} failed, ${JSON.stringify(error)}`);
  158. resolve(path);
  159. },
  160. });
  161. },
  162. fail(err) {
  163. console.log(err)
  164. }
  165. })
  166. });
  167. }
  168. function downloadFile(url, lru) {
  169. return new Promise((resolve, reject) => {
  170. const downloader = url.startsWith('cloud://')?wx.cloud.downloadFile:wx.downloadFile
  171. downloader({
  172. url: url,
  173. fileID: url,
  174. success: function (res) {
  175. if (res.statusCode !== 200) {
  176. console.error(`downloadFile ${url} failed res.statusCode is not 200`);
  177. reject();
  178. return;
  179. }
  180. const {
  181. tempFilePath
  182. } = res;
  183. wx.getFileInfo({
  184. filePath: tempFilePath,
  185. success: (tmpRes) => {
  186. const newFileSize = tmpRes.size;
  187. lru ? doLru(newFileSize).then(() => {
  188. saveFile(url, newFileSize, tempFilePath).then((filePath) => {
  189. resolve(filePath);
  190. });
  191. }, () => {
  192. resolve(tempFilePath);
  193. }) : resolve(tempFilePath);
  194. },
  195. fail: (error) => {
  196. // 文件大小信息获取失败,则此文件也不要进行存储
  197. console.error(`getFileInfo ${res.tempFilePath} failed, ${JSON.stringify(error)}`);
  198. resolve(res.tempFilePath);
  199. },
  200. });
  201. },
  202. fail: function (error) {
  203. console.error(`downloadFile failed, ${JSON.stringify(error)} `);
  204. reject();
  205. },
  206. });
  207. });
  208. }
  209. function saveFile(key, newFileSize, tempFilePath, isDataUrl = false) {
  210. return new Promise((resolve, reject) => {
  211. if (isDataUrl) {
  212. const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
  213. savedFiles[key] = {};
  214. savedFiles[key][KEY_PATH] = tempFilePath;
  215. savedFiles[key][KEY_TIME] = new Date().getTime();
  216. savedFiles[key][KEY_SIZE] = newFileSize;
  217. savedFiles['totalSize'] = newFileSize + totalSize;
  218. wx.setStorage({
  219. key: SAVED_FILES_KEY,
  220. data: savedFiles,
  221. });
  222. resolve(tempFilePath);
  223. return;
  224. }
  225. wx.saveFile({
  226. tempFilePath: tempFilePath,
  227. success: (fileRes) => {
  228. const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
  229. savedFiles[key] = {};
  230. savedFiles[key][KEY_PATH] = fileRes.savedFilePath;
  231. savedFiles[key][KEY_TIME] = new Date().getTime();
  232. savedFiles[key][KEY_SIZE] = newFileSize;
  233. savedFiles['totalSize'] = newFileSize + totalSize;
  234. wx.setStorage({
  235. key: SAVED_FILES_KEY,
  236. data: savedFiles,
  237. });
  238. resolve(fileRes.savedFilePath);
  239. },
  240. fail: (error) => {
  241. console.error(`saveFile ${key} failed, then we delete all files, ${JSON.stringify(error)}`);
  242. // 由于 saveFile 成功后,res.tempFilePath 处的文件会被移除,所以在存储未成功时,我们还是继续使用临时文件
  243. resolve(tempFilePath);
  244. // 如果出现错误,就直接情况本地的所有文件,因为你不知道是不是因为哪次lru的某个文件未删除成功
  245. reset();
  246. },
  247. });
  248. });
  249. }
  250. /**
  251. * 清空所有下载相关内容
  252. */
  253. function reset() {
  254. wx.removeStorage({
  255. key: SAVED_FILES_KEY,
  256. success: () => {
  257. wx.getSavedFileList({
  258. success: (listRes) => {
  259. removeFiles(listRes.fileList);
  260. },
  261. fail: (getError) => {
  262. console.error(`getSavedFileList failed, ${JSON.stringify(getError)}`);
  263. },
  264. });
  265. },
  266. });
  267. }
  268. function doLru(size) {
  269. if (size > MAX_SPACE_IN_B) {
  270. return Promise.reject()
  271. }
  272. return new Promise((resolve, reject) => {
  273. let totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
  274. if (size + totalSize <= MAX_SPACE_IN_B) {
  275. resolve();
  276. return;
  277. }
  278. // 如果加上新文件后大小超过最大限制,则进行 lru
  279. const pathsShouldDelete = [];
  280. // 按照最后一次的访问时间,从小到大排序
  281. const allFiles = JSON.parse(JSON.stringify(savedFiles));
  282. delete allFiles[KEY_TOTAL_SIZE];
  283. const sortedKeys = Object.keys(allFiles).sort((a, b) => {
  284. return allFiles[a][KEY_TIME] - allFiles[b][KEY_TIME];
  285. });
  286. for (const sortedKey of sortedKeys) {
  287. totalSize -= savedFiles[sortedKey].size;
  288. pathsShouldDelete.push(savedFiles[sortedKey][KEY_PATH]);
  289. delete savedFiles[sortedKey];
  290. if (totalSize + size < MAX_SPACE_IN_B) {
  291. break;
  292. }
  293. }
  294. savedFiles['totalSize'] = totalSize;
  295. wx.setStorage({
  296. key: SAVED_FILES_KEY,
  297. data: savedFiles,
  298. success: () => {
  299. // 保证 storage 中不会存在不存在的文件数据
  300. if (pathsShouldDelete.length > 0) {
  301. removeFiles(pathsShouldDelete);
  302. }
  303. resolve();
  304. },
  305. fail: (error) => {
  306. console.error(`doLru setStorage failed, ${JSON.stringify(error)}`);
  307. reject();
  308. },
  309. });
  310. });
  311. }
  312. function removeFiles(pathsShouldDelete) {
  313. for (const pathDel of pathsShouldDelete) {
  314. let delPath = pathDel;
  315. if (typeof pathDel === 'object') {
  316. delPath = pathDel.filePath;
  317. }
  318. if (delPath.indexOf('//usr/') !== -1) {
  319. wx.getFileSystemManager().unlink({
  320. filePath: delPath,
  321. fail(error) {
  322. console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
  323. }
  324. })
  325. } else {
  326. wx.removeSavedFile({
  327. filePath: delPath,
  328. fail: (error) => {
  329. console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
  330. },
  331. });
  332. }
  333. }
  334. }
  335. function getFile(key) {
  336. if (!savedFiles[key]) {
  337. return;
  338. }
  339. savedFiles[key]['time'] = new Date().getTime();
  340. wx.setStorage({
  341. key: SAVED_FILES_KEY,
  342. data: savedFiles,
  343. });
  344. return savedFiles[key];
  345. }