downloader.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /**
  2. * LRU 文件存储,使用该 downloader 可以让下载的文件存储在本地,下次进入小程序后可以直接使用
  3. * 详细设计文档可查看 https://juejin.im/post/5b42d3ede51d4519277b6ce3
  4. */
  5. const util = require('./util');
  6. const SAVED_FILES_KEY = 'savedFiles';
  7. const KEY_TOTAL_SIZE = 'totalSize';
  8. const KEY_PATH = 'path';
  9. const KEY_TIME = 'time';
  10. const KEY_SIZE = 'size';
  11. // 可存储总共为 6M,目前小程序可允许的最大本地存储为 10M
  12. let MAX_SPACE_IN_B = 6 * 1024 * 1024;
  13. let savedFiles = {};
  14. export default class Dowloader {
  15. constructor() {
  16. // app 如果设置了最大存储空间,则使用 app 中的
  17. if (getApp().PAINTER_MAX_LRU_SPACE) {
  18. MAX_SPACE_IN_B = getApp().PAINTER_MAX_LRU_SPACE;
  19. }
  20. wx.getStorage({
  21. key: SAVED_FILES_KEY,
  22. success: function (res) {
  23. if (res.data) {
  24. savedFiles = res.data;
  25. }
  26. },
  27. });
  28. }
  29. /**
  30. * 下载文件,会用 lru 方式来缓存文件到本地
  31. * @param {String} url 文件的 url
  32. */
  33. download(url, lru) {
  34. return new Promise((resolve, reject) => {
  35. if (!(url && util.isValidUrl(url))) {
  36. resolve(url);
  37. return;
  38. }
  39. if (!lru) {
  40. // 无 lru 情况下直接判断 临时文件是否存在,不存在重新下载
  41. wx.getFileInfo({
  42. filePath: url,
  43. success: () => {
  44. resolve(url);
  45. },
  46. fail: () => {
  47. downloadFile(url, lru).then((path) => {
  48. resolve(path);
  49. }, () => {
  50. reject();
  51. });
  52. },
  53. })
  54. return
  55. }
  56. const file = getFile(url);
  57. if (file) {
  58. // 检查文件是否正常,不正常需要重新下载
  59. wx.getSavedFileInfo({
  60. filePath: file[KEY_PATH],
  61. success: (res) => {
  62. resolve(file[KEY_PATH]);
  63. },
  64. fail: (error) => {
  65. console.error(`the file is broken, redownload it, ${JSON.stringify(error)}`);
  66. downloadFile(url, lru).then((path) => {
  67. resolve(path);
  68. }, () => {
  69. reject();
  70. });
  71. },
  72. });
  73. } else {
  74. downloadFile(url, lru).then((path) => {
  75. resolve(path);
  76. }, () => {
  77. reject();
  78. });
  79. }
  80. });
  81. }
  82. }
  83. function downloadFile(url, lru) {
  84. return new Promise((resolve, reject) => {
  85. wx.downloadFile({
  86. url: url,
  87. success: function (res) {
  88. if (res.statusCode !== 200) {
  89. console.error(`downloadFile ${url} failed res.statusCode is not 200`);
  90. reject();
  91. return;
  92. }
  93. const {
  94. tempFilePath
  95. } = res;
  96. wx.getFileInfo({
  97. filePath: tempFilePath,
  98. success: (tmpRes) => {
  99. const newFileSize = tmpRes.size;
  100. lru ? doLru(newFileSize).then(() => {
  101. saveFile(url, newFileSize, tempFilePath).then((filePath) => {
  102. resolve(filePath);
  103. });
  104. }, () => {
  105. resolve(tempFilePath);
  106. }) : resolve(tempFilePath);
  107. },
  108. fail: (error) => {
  109. // 文件大小信息获取失败,则此文件也不要进行存储
  110. console.error(`getFileInfo ${res.tempFilePath} failed, ${JSON.stringify(error)}`);
  111. resolve(res.tempFilePath);
  112. },
  113. });
  114. },
  115. fail: function (error) {
  116. console.error(`downloadFile failed, ${JSON.stringify(error)} `);
  117. reject();
  118. },
  119. });
  120. });
  121. }
  122. function saveFile(key, newFileSize, tempFilePath) {
  123. return new Promise((resolve, reject) => {
  124. wx.saveFile({
  125. tempFilePath: tempFilePath,
  126. success: (fileRes) => {
  127. const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
  128. savedFiles[key] = {};
  129. savedFiles[key][KEY_PATH] = fileRes.savedFilePath;
  130. savedFiles[key][KEY_TIME] = new Date().getTime();
  131. savedFiles[key][KEY_SIZE] = newFileSize;
  132. savedFiles['totalSize'] = newFileSize + totalSize;
  133. wx.setStorage({
  134. key: SAVED_FILES_KEY,
  135. data: savedFiles,
  136. });
  137. resolve(fileRes.savedFilePath);
  138. },
  139. fail: (error) => {
  140. console.error(`saveFile ${key} failed, then we delete all files, ${JSON.stringify(error)}`);
  141. // 由于 saveFile 成功后,res.tempFilePath 处的文件会被移除,所以在存储未成功时,我们还是继续使用临时文件
  142. resolve(tempFilePath);
  143. // 如果出现错误,就直接情况本地的所有文件,因为你不知道是不是因为哪次lru的某个文件未删除成功
  144. reset();
  145. },
  146. });
  147. });
  148. }
  149. /**
  150. * 清空所有下载相关内容
  151. */
  152. function reset() {
  153. wx.removeStorage({
  154. key: SAVED_FILES_KEY,
  155. success: () => {
  156. wx.getSavedFileList({
  157. success: (listRes) => {
  158. removeFiles(listRes.fileList);
  159. },
  160. fail: (getError) => {
  161. console.error(`getSavedFileList failed, ${JSON.stringify(getError)}`);
  162. },
  163. });
  164. },
  165. });
  166. }
  167. function doLru(size) {
  168. if (size > MAX_SPACE_IN_B) {
  169. return Promise.reject()
  170. }
  171. return new Promise((resolve, reject) => {
  172. let totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
  173. if (size + totalSize <= MAX_SPACE_IN_B) {
  174. resolve();
  175. return;
  176. }
  177. // 如果加上新文件后大小超过最大限制,则进行 lru
  178. const pathsShouldDelete = [];
  179. // 按照最后一次的访问时间,从小到大排序
  180. const allFiles = JSON.parse(JSON.stringify(savedFiles));
  181. delete allFiles[KEY_TOTAL_SIZE];
  182. const sortedKeys = Object.keys(allFiles).sort((a, b) => {
  183. return allFiles[a][KEY_TIME] - allFiles[b][KEY_TIME];
  184. });
  185. for (const sortedKey of sortedKeys) {
  186. totalSize -= savedFiles[sortedKey].size;
  187. pathsShouldDelete.push(savedFiles[sortedKey][KEY_PATH]);
  188. delete savedFiles[sortedKey];
  189. if (totalSize + size < MAX_SPACE_IN_B) {
  190. break;
  191. }
  192. }
  193. savedFiles['totalSize'] = totalSize;
  194. wx.setStorage({
  195. key: SAVED_FILES_KEY,
  196. data: savedFiles,
  197. success: () => {
  198. // 保证 storage 中不会存在不存在的文件数据
  199. if (pathsShouldDelete.length > 0) {
  200. removeFiles(pathsShouldDelete);
  201. }
  202. resolve();
  203. },
  204. fail: (error) => {
  205. console.error(`doLru setStorage failed, ${JSON.stringify(error)}`);
  206. reject();
  207. },
  208. });
  209. });
  210. }
  211. function removeFiles(pathsShouldDelete) {
  212. for (const pathDel of pathsShouldDelete) {
  213. let delPath = pathDel;
  214. if (typeof pathDel === 'object') {
  215. delPath = pathDel.filePath;
  216. }
  217. wx.removeSavedFile({
  218. filePath: delPath,
  219. fail: (error) => {
  220. console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
  221. },
  222. });
  223. }
  224. }
  225. function getFile(key) {
  226. if (!savedFiles[key]) {
  227. return;
  228. }
  229. savedFiles[key]['time'] = new Date().getTime();
  230. wx.setStorage({
  231. key: SAVED_FILES_KEY,
  232. data: savedFiles,
  233. });
  234. return savedFiles[key];
  235. }