'use strict';
const _ = require('lodash');
const RNFS = require('react-native-fs');
const {
DocumentDirectoryPath
} = RNFS;
const SHA1 = require("crypto-js/sha1");
const URL = require('url-parse');
const defaultOptions = {
useQueryParamsInCacheKey: false
};
const activeDownloads = {};
const BaseDirPath = 'baseDirPath';
function getBasePath(){
return DocumentDirectoryPath + '/' + BaseDirPath;
}
function serializeObjectKeys(obj) {
return _(obj)
.toPairs()
.sortBy(a => a[0])
.map(a => a[1])
.value();
}
function getQueryForCacheKey(url, useQueryParamsInCacheKey) {
if (_.isArray(useQueryParamsInCacheKey)) {
return serializeObjectKeys(_.pick(url.query, useQueryParamsInCacheKey));
}
if (useQueryParamsInCacheKey) {
return serializeObjectKeys(url.query);
}
return '';
}
function generateCacheKey(url, options) {
const parsedUrl = new URL(url, null, true);
const pathParts = parsedUrl.pathname.split('/');
// last path part is the file name
const fileName = pathParts.pop();
const filePath = pathParts.join('/');
const parts = fileName.split('.');
// TODO - try to figure out the file type or let the user provide it, for now use jpg as default
const type = parts.length > 1 ? parts.pop() : 'jpg';
const cacheable = filePath + fileName + type + getQueryForCacheKey(parsedUrl, options.useQueryParamsInCacheKey);
return SHA1(cacheable) + '.' + type;
}
function getCachePath(url, options) {
if (options.cacheGroup) {
return options.cacheGroup;
}
const parsedUrl = new URL(url);
return parsedUrl.host;
}
function getCachedImageFilePath(url, options) {
const cacheKey = generateCacheKey(url, options);
const cachePath = getCachePath(url, options);
const dirPath = getBasePath() + '/' + cachePath;
return dirPath + '/' + cacheKey;
}
function deleteFile(filePath) {
return RNFS.exists(filePath)
.then(res => res && RNFS.unlink(filePath))
.catch((err) => {
// swallow error to always resolve
});
}
function ensurePath(filePath) {
const parts = filePath.split('/');
const dirPath = _.initial(parts).join('/');
return RNFS.mkdir(dirPath, {NSURLIsExcludedFromBackupKey: true});
}
/**
* returns a promise that is resolved when the download of the requested file
* is complete and the file is saved.
* if the download fails, or was stopped the partial file is deleted, and the
* promise is rejected
* @param fromUrl
* @param toFile
* @returns {Promise}
*/
function downloadImage(fromUrl, toFile) {
// use toFile as the key as is was created using the cacheKey
if (!_.has(activeDownloads, toFile)) {
// create an active download for this file
activeDownloads[toFile] = new Promise((resolve, reject) => {
const downloadOptions = {
fromUrl,
toFile
};
RNFS.downloadFile(downloadOptions).promise
.then(res => {
if (Math.floor(res.statusCode / 100) == 2) {
resolve(toFile);
} else {
return Promise.reject('Failed to successfully download image')
}
})
.catch(err => {
return deleteFile(toFile)
.then(() => reject(err))
})
.finally(() => {
// cleanup
delete activeDownloads[toFile];
});
});
}
return activeDownloads[toFile];
}
function createPrefetcer(list) {
const urls = _.clone(list);
return {
next() {
return urls.shift();
}
};
}
function runPrefetchTask(prefetcher, options) {
const url = prefetcher.next();
if (!url) {
return Promise.resolve();
}
// if url is cacheable - cache it
if (isCacheable(url)) {
// check cache
return getCachedImagePath(url, options)
// if not found download
.catch(() => cacheImage(url, options))
// then run next task
.then(() => runPrefetchTask(prefetcher, options));
}
// else get next
return runPrefetchTask(prefetcher, options);
}
// API
function isCacheable(url) {
return _.isString(url) && (_.startsWith(url, 'http://') || _.startsWith(url, 'https://'));
}
function getCachedImagePath(url, options = defaultOptions) {
const filePath = getCachedImageFilePath(url, options);
return RNFS.stat(filePath)
.then(res => {
if (!res.isFile()) {
// reject the promise if res is not a file
throw new Error('Failed to get image from cache');
}
if (!res.size) {
// something went wrong with the download, file size is 0, remove it
return deleteFile(filePath)
.then(() => {
throw new Error('Failed to get image from cache');
});
}
return filePath;
})
.catch(err => {
throw err;
})
}
function cacheImage(url, options = defaultOptions) {
const filePath = getCachedImageFilePath(url, options);
return ensurePath(filePath)
.then(() => downloadImage(url, filePath));
}
function deleteCachedImage(url, options = defaultOptions) {
const filePath = getCachedImageFilePath(url, options);
return deleteFile(filePath);
}
function cacheMultipleImages(urls, options = defaultOptions) {
const prefetcher = createPrefetcer(urls);
const numberOfWorkers = urls.length;
const promises = _.times(numberOfWorkers, () =>
runPrefetchTask(prefetcher, options)
);
return Promise.all(promises);
}
function deleteMultipleCachedImages(urls, options = defaultOptions) {
return _.reduce(urls, (p, url) =>
p.then(() => deleteCachedImage(url, options)),
Promise.resolve()
);
}
function clearCache() {
deleteFile(getBasePath());
ensurePath(getBasePath());
}
function getStat(path) {
return RNFS.stat(path)
.then((res) => {
if (res) {
return {
...res,
path: path
};
}
return null;
}, (error) => {
return 0;
})
}
function getDirSize(path) {
return new Promise((resolve, reject) => {
let total = 0;
RNFS.readdir(path)
.then((subItems) => {
const statArr = [];
const funcArr = [];
if (!subItems || subItems.length <= 0) {
resolve(0);
return;
}
subItems.forEach((itemPath) => {
statArr.push(getStat(path + '/' + itemPath))
});
Promise.all(statArr)
.then((arr) => {
if (!arr || arr.length <= 0) {
resolve(0);
return;
}
arr.forEach((item) => {
if (item) {
if (item.isFile()) {
total += item.size;
} else {
funcArr.push(getDirSize(item.path));
}
}
});
if (funcArr.length > 0) {
Promise.all(funcArr)
.then((sizeList) => {
total += sizeList.reduce((a, b) => {
return a + b;
});
resolve(total);
});
} else {
resolve(total);
}
});
}, (error) => {
resolve(0);
});
});
}
function getCacheSize() {
return getDirSize(getBasePath());
}
module.exports = {
isCacheable,
getCachedImagePath,
cacheImage,
deleteCachedImage,
cacheMultipleImages,
deleteMultipleCachedImages,
clearCache,
getCacheSize
};
网友评论