image cache provider

'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
};

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容