downloader.js 14 KB

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