diff --git a/README.md b/README.md index 6502490..4982408 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# react-native-image-cache-wrapper +# react-native-image-cache-wrapper - Strange error fixed version - Алдаа зассан хувилбар [![npm](https://img.shields.io/npm/v/react-native-image-cache-wrapper.svg?style=flat-square)](https://www.npmjs.com/package/react-native-image-cache-wrapper) The best react native image cache wrapper. diff --git a/index.js b/index.js index 8779642..fbec0c7 100644 --- a/index.js +++ b/index.js @@ -5,216 +5,269 @@ * This source code is licensed under the MIT-style license found in the * LICENSE file in the root directory of this source tree. */ -'use strict'; -import React, {Component} from 'react'; -import { - View, - Image, - ImageBackground, - Platform -} from 'react-native'; +"use strict"; +import React, { Component } from "react"; +import { View, Image, ImageBackground, Platform } from "react-native"; -import RNFetchBlob from 'rn-fetch-blob'; +import RNFetchBlob from "rn-fetch-blob"; +import RNFS from "react-native-fs"; +const SHA1 = require("crypto-js/sha1"); -const SHA1 = require('crypto-js/sha1'); - -const defaultImageTypes = ['png', 'jpeg', 'jpg', 'gif', 'bmp', 'tiff', 'tif']; +const defaultImageTypes = ["png", "jpeg", "jpg", "gif", "bmp", "tiff", "tif"]; export default class CachedImage extends Component { - - static defaultProps = { - expiration: 86400 * 7, // default cache a week - activityIndicator: null, // default not show an activity indicator - }; - - static cacheDir = RNFetchBlob.fs.dirs.CacheDir + "/CachedImage/"; - - /** - * delete a cache file - * @param url - */ - static deleteCache = url => { - const cacheFile = _getCacheFilename(url); - return _unlinkFile(cacheFile); - }; - - /** - * clear all cache files - */ - static clearCache = () => _unlinkFile(CachedImage.cacheDir); - - /** - * check if a url is cached - */ - static isUrlCached = (url: string, success: Function, failure: Function) => { - const cacheFile = _getCacheFilename(url); - RNFetchBlob.fs.exists(cacheFile) - .then((exists) => { - success && success(exists); - }) - .catch((error) => { - failure && failure(error); - }); - }; - - /** - * make a cache filename - * @param url - * @returns {string} - */ - static getCacheFilename = (url) => { - return _getCacheFilename(url); + static defaultProps = { + expiration: 86400 * 7, // default cache a week + activityIndicator: null // default not show an activity indicator + }; + + static cacheDir = RNFetchBlob.fs.dirs.CacheDir + "/CachedImage/"; + + static sameURL = []; + /** + * delete a cache file + * @param url + */ + static deleteCache = url => { + const cacheFile = _getCacheFilename(url); + CachedImage.sameURL.splice(CachedImage.sameURL.indexOf(cacheFile)); + + return _unlinkFile(cacheFile); + }; + + /** + * clear all cache files + */ + static clearCache = async () => { + let obj = await RNFS.readDir(CachedImage.cacheDir); + + for (let file of obj) { + _unlinkFile(file.path); } - - /** - * Same as ReactNaive.Image.getSize only it will not download the image if it has a cached version - * @param url - * @param success callback (width,height)=>{} - * @param failure callback (error:string)=>{} - */ - static getSize = (url: string, success: Function, failure: Function) => { - - CachedImage.prefetch(url, 0, - (cacheFile) => { - if (Platform.OS === 'android') { - url = "file://" + cacheFile; - } else { - url = cacheFile; - } - Image.getSize(url, success, failure); - }, - (error) => { - Image.getSize(url, success, failure); - }); - - }; - - /** - * prefech an image - * - * @param url - * @param expiration if zero or not set, no expiration - * @param success callback (cacheFile:string)=>{} - * @param failure callback (error:string)=>{} - */ - static prefetch = (url: string, expiration: number, success: Function, failure: Function) => { - - // source invalidate - if (!url || url.toString() !== url) { - failure && failure("no url."); - return; + CachedImage.sameURL = []; + }; + + static async getCacheSize() { + let hasCachedImageFolder = await RNFS.exists(CachedImage.cacheDir); + if (hasCachedImageFolder) { + let obj = await RNFS.readDir(CachedImage.cacheDir); + let size = 0; + for (let item of obj) { + size += item.size; + } + return (size / 1024 / 1024).toFixed(2); + } + return 0; + } + + /** + * check if a url is cached + */ + static isUrlCached = (url: string, success: Function, failure: Function) => { + const cacheFile = _getCacheFilename(url); + RNFetchBlob.fs + .exists(cacheFile) + .then(exists => { + success && success(exists); + }) + .catch(error => { + failure && failure(error); + }); + }; + + /** + * make a cache filename + * @param url + * @returns {string} + */ + static getCacheFilename = url => { + return _getCacheFilename(url); + }; + + /** + * Same as ReactNaive.Image.getSize only it will not download the image if it has a cached version + * @param url + * @param success callback (width,height)=>{} + * @param failure callback (error:string)=>{} + */ + static getSize = (url: string, success: Function, failure: Function) => { + CachedImage.prefetch( + url, + 0, + cacheFile => { + if (Platform.OS === "android") { + url = "file://" + cacheFile; + } else { + url = cacheFile; } - - const cacheFile = _getCacheFilename(url); - - RNFetchBlob.fs.stat(cacheFile) - .then((stats) => { - // if exist and not expired then use it. - if (!Boolean(expiration) || (expiration * 1000 + stats.lastModified) > (new Date().getTime())) { - success && success(cacheFile); - } else { - _saveCacheFile(url, success, failure); - } - }) - .catch((error) => { - // not exist - _saveCacheFile(url, success, failure); - }); - }; - - constructor(props) { - - super(props); - this.state = { - source: null, - }; - - this._useDefaultSource = false; - this._downloading = false; - this._mounted = false; + Image.getSize(url, success, failure); + }, + error => { + Image.getSize(url, success, failure); + } + ); + }; + + /** + * prefech an image + * + * @param url + * @param expiration if zero or not set, no expiration + * @param success callback (cacheFile:string)=>{} + * @param failure callback (error:string)=>{} + */ + static prefetch = ( + url: string, + expiration: number, + success: Function, + failure: Function + ) => { + // source invalidate + if (!url || url.toString() !== url) { + failure && failure("no url."); + return; } - componentDidMount() { - this._mounted = true; + const cacheFile = _getCacheFilename(url); + if (CachedImage.sameURL.includes(cacheFile)) { + success && success(cacheFile); + return; } + CachedImage.sameURL.push(cacheFile); + + RNFetchBlob.fs + .stat(cacheFile) + .then(stats => { + // if exist and not expired then use it. + if ( + !Boolean(expiration) || + expiration * 1000 + stats.lastModified > new Date().getTime() + ) { + success && success(cacheFile); + } else { + _saveCacheFile(url, success, failure); + } + }) + .catch(error => { + // not exist + // success && success(cacheFile) + + _saveCacheFile(url, success, failure); + }); + }; + + constructor(props) { + super(props); + this.state = { + source: null + }; - componentWillUnmount() { - this._mounted = false; + this._useDefaultSource = false; + this._downloading = false; + this._mounted = false; + } + + componentDidMount() { + this._mounted = true; + } + + componentWillUnmount() { + this._mounted = false; + } + + render() { + if (this.props.source && this.props.source.uri) { + if (!this.state.source && !this._downloading) { + this._downloading = true; + CachedImage.prefetch( + this.props.source.uri, + this.props.expiration, + cacheFile => { + setTimeout(() => { + if (this._mounted) { + this.setState({ source: { uri: "file://" + cacheFile } }); + } + this._downloading = false; + }, 0); + }, + error => { + // cache failed use original source + if (this._mounted) { + setTimeout(() => { + this.setState({ source: this.props.source }); + }, 0); + } + this._downloading = false; + } + ); + } + } else { + this.state.source = this.props.source; } - render() { - - if (this.props.source && this.props.source.uri) { - if (!this.state.source && !this._downloading) { - this._downloading = true; - CachedImage.prefetch(this.props.source.uri, - this.props.expiration, - (cacheFile) => { - setTimeout(() => { - if (this._mounted) { - this.setState({source: {uri: "file://" + cacheFile}}); - } - this._downloading = false; - }, 0); - }, (error) => { - // cache failed use original source - if (this._mounted) { - setTimeout(() => { - this.setState({source: this.props.source}); - }, 0); - } - this._downloading = false; - }); + if (this.state.source) { + const renderImage = (props, children) => + children != null ? ( + {children} + ) : ( + + ); + + const result = renderImage( + { + ...this.props, + source: this.state.source, + onError: error => { + // error happened, delete cache + if (this.props.source && this.props.source.uri) { + CachedImage.deleteCache(this.props.source.uri); } - } else { - this.state.source = this.props.source; - } - - if (this.state.source) { - - const renderImage = (props, children) => (children != null ? - {children} : - ); - - const result = renderImage({ - ...this.props, - source: this.state.source, - onError: (error) => { - // error happened, delete cache - if (this.props.source && this.props.source.uri) { - CachedImage.deleteCache(this.props.source.uri); - } - if (this.props.onError) { - this.props.onError(error); - } else { - if (!this._useDefaultSource && this.props.defaultSource) { - this._useDefaultSource = true; - setTimeout(() => { - this.setState({source: this.props.defaultSource}); - }, 0); - } - } - } - }, this.props.children); - - return (result); - } else { - return ( - - {this.props.activityIndicator} - ); - } + if (this.props.onError) { + this.props.onError(error); + } else { + if (!this._useDefaultSource && this.props.defaultSource) { + this._useDefaultSource = true; + setTimeout(() => { + if (this.props.source && this.props.source.uri) { + this.setState({ source: this.props.source }); + } else this.setState({ source: this.props.defaultSource }); + }, 0); + } + } + } + }, + this.props.children + ); + + return result; + } else { + return ( + + {this.props.activityIndicator} + + ); } + } } async function _unlinkFile(file) { - try { - return await RNFetchBlob.fs.unlink(file); - } catch (e) { - } + try { + CachedImage.sameURL.splice(CachedImage.sameURL.indexOf(file)); + return await RNFetchBlob.fs.unlink(file); + } catch (e) {} } /** @@ -223,13 +276,12 @@ async function _unlinkFile(file) { * @returns {string} */ function _getCacheFilename(url) { + if (!url || url.toString() !== url) return ""; - if (!url || url.toString() !== url) return ""; - - let ext = url.replace(/.+\./, "").toLowerCase(); - if (defaultImageTypes.indexOf(ext) === -1) ext = "png"; - let hash = SHA1(url); - return CachedImage.cacheDir + hash + "." + ext; + let ext = url.replace(/.+\./, "").toLowerCase(); + if (defaultImageTypes.indexOf(ext) === -1) ext = "png"; + let hash = SHA1(url); + return CachedImage.cacheDir + hash + "." + ext; } /** @@ -240,77 +292,84 @@ function _getCacheFilename(url) { * @param success callback (cacheFile:string)=>{} * @param failure callback (error:string)=>{} */ -async function _saveCacheFile(url: string, success: Function, failure: Function) { - - try { - const isNetwork = !!(url && url.match(/^https?:\/\//)); - const isBase64 = !!(url && url.match(/^data:/)); - const cacheFile = _getCacheFilename(url); - - if (isNetwork) { - const tempCacheFile = cacheFile + '.tmp'; - _unlinkFile(tempCacheFile); - RNFetchBlob.config({ - // response data will be saved to this path if it has access right. - path: tempCacheFile, +async function _saveCacheFile( + url: string, + success: Function, + failure: Function +) { + try { + const isNetwork = !!(url && url.match(/^https?:\/\//)); + const isBase64 = !!(url && url.match(/^data:/)); + const cacheFile = _getCacheFilename(url); + + if (isNetwork) { + const tempCacheFile = cacheFile + ".tmp"; + _unlinkFile(tempCacheFile); + RNFetchBlob.config({ + // response data will be saved to this path if it has access right. + path: tempCacheFile + }) + .fetch("GET", url) + .then(async res => { + if ( + res && + res.respInfo && + res.respInfo.headers && + !res.respInfo.headers["Content-Encoding"] && + !res.respInfo.headers["Transfer-Encoding"] && + res.respInfo.headers["Content-Length"] + ) { + const expectedContentLength = + res.respInfo.headers["Content-Length"]; + let actualContentLength; + + try { + const fileStats = await RNFetchBlob.fs.stat(res.path()); + + if (!fileStats || !fileStats.size) { + throw new Error("FileNotFound:" + url); + } + + actualContentLength = fileStats.size; + } catch (error) { + throw new Error("DownloadFailed:" + url); + } + + if (expectedContentLength != actualContentLength) { + throw new Error("DownloadFailed:" + url); + } + } + + _unlinkFile(cacheFile); + RNFetchBlob.fs + .mv(tempCacheFile, cacheFile) + .then(() => { + success && success(cacheFile); }) - .fetch( - 'GET', - url - ) - .then(async (res) => { - - if (res && res.respInfo && res.respInfo.headers && !res.respInfo.headers["Content-Encoding"] && !res.respInfo.headers["Transfer-Encoding"] && res.respInfo.headers["Content-Length"]) { - const expectedContentLength = res.respInfo.headers["Content-Length"]; - let actualContentLength; - - try { - const fileStats = await RNFetchBlob.fs.stat(res.path()); - - if (!fileStats || !fileStats.size) { - throw new Error("FileNotFound:"+url); - } - - actualContentLength = fileStats.size; - } catch (error) { - throw new Error("DownloadFailed:"+url); - } - - if (expectedContentLength != actualContentLength) { - throw new Error("DownloadFailed:"+url); - } - } - - _unlinkFile(cacheFile); - RNFetchBlob.fs - .mv(tempCacheFile, cacheFile) - .then(() => { - success && success(cacheFile); - }) - .catch(async (error) => { - throw error; - }); - }) - .catch(async (error) => { - _unlinkFile(tempCacheFile); - _unlinkFile(cacheFile); - failure && failure(error); - }); - } else if (isBase64) { - let data = url.replace(/data:/i, ''); - RNFetchBlob.fs - .writeFile(cacheFile, data, 'base64') - .then(() => { - success && success(cacheFile); - }) - .catch(async (error) => { - _unlinkFile(cacheFile); - failure && failure(error); - }); - } else { - failure && failure(new Error("NotSupportedUrl")); - } - } catch (error) { - failure && failure(error); + .catch(async error => { + throw error; + }); + }) + .catch(async error => { + _unlinkFile(tempCacheFile); + _unlinkFile(cacheFile); + failure && failure(error); + }); + } else if (isBase64) { + let data = url.replace(/data:/i, ""); + RNFetchBlob.fs + .writeFile(cacheFile, data, "base64") + .then(() => { + success && success(cacheFile); + }) + .catch(async error => { + _unlinkFile(cacheFile); + failure && failure(error); + }); + } else { + failure && failure(new Error("NotSupportedUrl")); } -} \ No newline at end of file + } catch (error) { + failure && failure(error); + } +}