363 lines
10 KiB
JavaScript
363 lines
10 KiB
JavaScript
/**
|
||
* LRU 文件存储,使用该 downloader 可以让下载的文件存储在本地,下次进入小程序后可以直接使用
|
||
* 详细设计文档可查看 https://juejin.im/post/5b42d3ede51d4519277b6ce3
|
||
*/
|
||
const util = require('./util');
|
||
const sha1 = require('./sha1');
|
||
|
||
const SAVED_FILES_KEY = 'savedFiles';
|
||
const KEY_TOTAL_SIZE = 'totalSize';
|
||
const KEY_PATH = 'path';
|
||
const KEY_TIME = 'time';
|
||
const KEY_SIZE = 'size';
|
||
|
||
// 可存储总共为 6M,目前小程序可允许的最大本地存储为 10M
|
||
let MAX_SPACE_IN_B = 6 * 1024 * 1024;
|
||
let savedFiles = {};
|
||
|
||
export default class Dowloader {
|
||
constructor() {
|
||
// app 如果设置了最大存储空间,则使用 app 中的
|
||
if (getApp().PAINTER_MAX_LRU_SPACE) {
|
||
MAX_SPACE_IN_B = getApp().PAINTER_MAX_LRU_SPACE;
|
||
}
|
||
wx.getStorage({
|
||
key: SAVED_FILES_KEY,
|
||
success: function (res) {
|
||
if (res.data) {
|
||
savedFiles = res.data;
|
||
}
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 下载文件,会用 lru 方式来缓存文件到本地
|
||
* @param {String} url 文件的 url
|
||
*/
|
||
download(url, lru) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!(url && util.isValidUrl(url))) {
|
||
resolve(url);
|
||
return;
|
||
}
|
||
const fileName = getFileName(url);
|
||
if (!lru) {
|
||
// 无 lru 情况下直接判断 临时文件是否存在,不存在重新下载
|
||
wx.getFileInfo({
|
||
filePath: fileName,
|
||
success: () => {
|
||
resolve(url);
|
||
},
|
||
fail: () => {
|
||
if (util.isOnlineUrl(url)) {
|
||
downloadFile(url, lru).then((path) => {
|
||
resolve(path);
|
||
}, () => {
|
||
reject();
|
||
});
|
||
} else if (util.isDataUrl(url)) {
|
||
transformBase64File(url, lru).then(path => {
|
||
resolve(path);
|
||
}, () => {
|
||
reject();
|
||
});
|
||
}
|
||
},
|
||
})
|
||
return
|
||
}
|
||
|
||
const file = getFile(fileName);
|
||
|
||
if (file) {
|
||
if (file[KEY_PATH].indexOf('//usr/') !== -1) {
|
||
wx.getFileInfo({
|
||
filePath: file[KEY_PATH],
|
||
success() {
|
||
resolve(file[KEY_PATH]);
|
||
},
|
||
fail(error) {
|
||
console.error(`base64 file broken, ${JSON.stringify(error)}`);
|
||
transformBase64File(url, lru).then(path => {
|
||
resolve(path);
|
||
}, () => {
|
||
reject();
|
||
});
|
||
}
|
||
})
|
||
} else {
|
||
// 检查文件是否正常,不正常需要重新下载
|
||
wx.getSavedFileInfo({
|
||
filePath: file[KEY_PATH],
|
||
success: (res) => {
|
||
resolve(file[KEY_PATH]);
|
||
},
|
||
fail: (error) => {
|
||
console.error(`the file is broken, redownload it, ${JSON.stringify(error)}`);
|
||
downloadFile(url, lru).then((path) => {
|
||
resolve(path);
|
||
}, () => {
|
||
reject();
|
||
});
|
||
},
|
||
});
|
||
}
|
||
} else {
|
||
if (util.isOnlineUrl(url)) {
|
||
downloadFile(url, lru).then((path) => {
|
||
resolve(path);
|
||
}, () => {
|
||
reject();
|
||
});
|
||
} else if (util.isDataUrl(url)) {
|
||
transformBase64File(url, lru).then(path => {
|
||
resolve(path);
|
||
}, () => {
|
||
reject();
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function getFileName(url) {
|
||
if (util.isDataUrl(url)) {
|
||
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(url) || [];
|
||
const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
|
||
return fileName;
|
||
} else {
|
||
return url;
|
||
}
|
||
}
|
||
|
||
function transformBase64File(base64data, lru) {
|
||
return new Promise((resolve, reject) => {
|
||
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
|
||
if (!format) {
|
||
console.error('base parse failed');
|
||
reject();
|
||
return;
|
||
}
|
||
const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
|
||
const path = `${wx.env.USER_DATA_PATH}/${fileName}`;
|
||
const buffer = wx.base64ToArrayBuffer(bodyData.replace(/[\r\n]/g, ""));
|
||
wx.getFileSystemManager().writeFile({
|
||
filePath: path,
|
||
data: buffer,
|
||
encoding: 'binary',
|
||
success() {
|
||
wx.getFileInfo({
|
||
filePath: path,
|
||
success: (tmpRes) => {
|
||
const newFileSize = tmpRes.size;
|
||
lru ? doLru(newFileSize).then(() => {
|
||
saveFile(fileName, newFileSize, path, true).then((filePath) => {
|
||
resolve(filePath);
|
||
});
|
||
}, () => {
|
||
resolve(path);
|
||
}) : resolve(path);
|
||
},
|
||
fail: (error) => {
|
||
// 文件大小信息获取失败,则此文件也不要进行存储
|
||
console.error(`getFileInfo ${path} failed, ${JSON.stringify(error)}`);
|
||
resolve(path);
|
||
},
|
||
});
|
||
},
|
||
fail(err) {
|
||
console.log(err)
|
||
}
|
||
})
|
||
});
|
||
}
|
||
|
||
function downloadFile(url, lru) {
|
||
return new Promise((resolve, reject) => {
|
||
const downloader = url.startsWith('cloud://')?wx.cloud.downloadFile:wx.downloadFile
|
||
downloader({
|
||
url: url,
|
||
fileID: url,
|
||
success: function (res) {
|
||
if (res.statusCode !== 200) {
|
||
console.error(`downloadFile ${url} failed res.statusCode is not 200`);
|
||
reject();
|
||
return;
|
||
}
|
||
const {
|
||
tempFilePath
|
||
} = res;
|
||
wx.getFileInfo({
|
||
filePath: tempFilePath,
|
||
success: (tmpRes) => {
|
||
const newFileSize = tmpRes.size;
|
||
lru ? doLru(newFileSize).then(() => {
|
||
saveFile(url, newFileSize, tempFilePath).then((filePath) => {
|
||
resolve(filePath);
|
||
});
|
||
}, () => {
|
||
resolve(tempFilePath);
|
||
}) : resolve(tempFilePath);
|
||
},
|
||
fail: (error) => {
|
||
// 文件大小信息获取失败,则此文件也不要进行存储
|
||
console.error(`getFileInfo ${res.tempFilePath} failed, ${JSON.stringify(error)}`);
|
||
resolve(res.tempFilePath);
|
||
},
|
||
});
|
||
},
|
||
fail: function (error) {
|
||
console.error(`downloadFile failed, ${JSON.stringify(error)} `);
|
||
reject();
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
function saveFile(key, newFileSize, tempFilePath, isDataUrl = false) {
|
||
return new Promise((resolve, reject) => {
|
||
if (isDataUrl) {
|
||
const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
|
||
savedFiles[key] = {};
|
||
savedFiles[key][KEY_PATH] = tempFilePath;
|
||
savedFiles[key][KEY_TIME] = new Date().getTime();
|
||
savedFiles[key][KEY_SIZE] = newFileSize;
|
||
savedFiles['totalSize'] = newFileSize + totalSize;
|
||
wx.setStorage({
|
||
key: SAVED_FILES_KEY,
|
||
data: savedFiles,
|
||
});
|
||
resolve(tempFilePath);
|
||
return;
|
||
}
|
||
wx.saveFile({
|
||
tempFilePath: tempFilePath,
|
||
success: (fileRes) => {
|
||
const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
|
||
savedFiles[key] = {};
|
||
savedFiles[key][KEY_PATH] = fileRes.savedFilePath;
|
||
savedFiles[key][KEY_TIME] = new Date().getTime();
|
||
savedFiles[key][KEY_SIZE] = newFileSize;
|
||
savedFiles['totalSize'] = newFileSize + totalSize;
|
||
wx.setStorage({
|
||
key: SAVED_FILES_KEY,
|
||
data: savedFiles,
|
||
});
|
||
resolve(fileRes.savedFilePath);
|
||
},
|
||
fail: (error) => {
|
||
console.error(`saveFile ${key} failed, then we delete all files, ${JSON.stringify(error)}`);
|
||
// 由于 saveFile 成功后,res.tempFilePath 处的文件会被移除,所以在存储未成功时,我们还是继续使用临时文件
|
||
resolve(tempFilePath);
|
||
// 如果出现错误,就直接情况本地的所有文件,因为你不知道是不是因为哪次lru的某个文件未删除成功
|
||
reset();
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 清空所有下载相关内容
|
||
*/
|
||
function reset() {
|
||
wx.removeStorage({
|
||
key: SAVED_FILES_KEY,
|
||
success: () => {
|
||
wx.getSavedFileList({
|
||
success: (listRes) => {
|
||
removeFiles(listRes.fileList);
|
||
},
|
||
fail: (getError) => {
|
||
console.error(`getSavedFileList failed, ${JSON.stringify(getError)}`);
|
||
},
|
||
});
|
||
},
|
||
});
|
||
}
|
||
|
||
function doLru(size) {
|
||
if (size > MAX_SPACE_IN_B) {
|
||
return Promise.reject()
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
let totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
|
||
|
||
if (size + totalSize <= MAX_SPACE_IN_B) {
|
||
resolve();
|
||
return;
|
||
}
|
||
// 如果加上新文件后大小超过最大限制,则进行 lru
|
||
const pathsShouldDelete = [];
|
||
// 按照最后一次的访问时间,从小到大排序
|
||
const allFiles = JSON.parse(JSON.stringify(savedFiles));
|
||
delete allFiles[KEY_TOTAL_SIZE];
|
||
const sortedKeys = Object.keys(allFiles).sort((a, b) => {
|
||
return allFiles[a][KEY_TIME] - allFiles[b][KEY_TIME];
|
||
});
|
||
|
||
for (const sortedKey of sortedKeys) {
|
||
totalSize -= savedFiles[sortedKey].size;
|
||
pathsShouldDelete.push(savedFiles[sortedKey][KEY_PATH]);
|
||
delete savedFiles[sortedKey];
|
||
if (totalSize + size < MAX_SPACE_IN_B) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
savedFiles['totalSize'] = totalSize;
|
||
|
||
wx.setStorage({
|
||
key: SAVED_FILES_KEY,
|
||
data: savedFiles,
|
||
success: () => {
|
||
// 保证 storage 中不会存在不存在的文件数据
|
||
if (pathsShouldDelete.length > 0) {
|
||
removeFiles(pathsShouldDelete);
|
||
}
|
||
resolve();
|
||
},
|
||
fail: (error) => {
|
||
console.error(`doLru setStorage failed, ${JSON.stringify(error)}`);
|
||
reject();
|
||
},
|
||
});
|
||
});
|
||
}
|
||
|
||
function removeFiles(pathsShouldDelete) {
|
||
for (const pathDel of pathsShouldDelete) {
|
||
let delPath = pathDel;
|
||
if (typeof pathDel === 'object') {
|
||
delPath = pathDel.filePath;
|
||
}
|
||
if (delPath.indexOf('//usr/') !== -1) {
|
||
wx.getFileSystemManager().unlink({
|
||
filePath: delPath,
|
||
fail(error) {
|
||
console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
|
||
}
|
||
})
|
||
} else {
|
||
wx.removeSavedFile({
|
||
filePath: delPath,
|
||
fail: (error) => {
|
||
console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
|
||
},
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
function getFile(key) {
|
||
if (!savedFiles[key]) {
|
||
return;
|
||
}
|
||
savedFiles[key]['time'] = new Date().getTime();
|
||
wx.setStorage({
|
||
key: SAVED_FILES_KEY,
|
||
data: savedFiles,
|
||
});
|
||
return savedFiles[key];
|
||
} |