imgcache.js
imgcache.js copied to clipboard
What if images are corrupted?
I'm using imgcache.js in a Cordova App deployed to iOS and Android. Some Android users reported that some images are corrupted (incomplete or completely missing). I investigated and I think that the download of the images was started but, exiting the App just after the download begins, the images in memory are incomplete. Is there a way to see if an image is corrupted (and maybe substitute it) or to avoid saving incomplete images?
I guess we could dowload images into temp files, and rename/move them only once dowload completes. That would solve this problem I guess. I will have a look into this at some point, if someone wants to send me a pull request in the meantime I wouldn't mind.
I tried to apply this kind of solutions. I modified method cacheFile adding, after line 594: if (Helpers.isCordovaAndroid()) filePath = filePath + '.tmp';
and after line 600: if (Helpers.isCordovaAndroid()) entry.moveTo(ImgCache.attributes.dirEntry, entry.name.substr(0, entry.name.length - 4));
The reason why I applied this patch only to Android is because I found that with current release of plugin org.apache.cordova.file-transfer (0.4.6, but also starting from 0.4.3 I think) because of a bug of the plugin (with various issues about it: https://issues.apache.org/jira/browse/CB-6720, https://issues.apache.org/jira/browse/CB-6750, https://issues.apache.org/jira/browse/CB-6525) on iOS the success callback of fileTransfer.download is never fired. (BTW: this could also explain issue #73 ) I've tried may ways of avoid this, but I didn't find how. I found that the on_progress callback is called: comparing progressEvent.loaded and progressEvent.total I can find if a download is finished, but how can I know which file it was downloading when I use, as I do, many async downloads? Any idea? Thank you.
Thanks for testing this solution out.
So by using the on_progress callback, did you manage to find out when the download has completed in the end?
To know which file you were downloading, I suppose you could use javascript closure to know which input file you were working on. For example:
var myImg = ...;
ImgCache.cacheFile(myImg, function () {
// success callback
// I can use myImg inside which was the image cached
});
But perhaps I didn't get your question..
Thank you for your answer.
In on_progress callback I know when a download is finished: it's when (progressEvent.loaded == progressEvent.total)
I'm not very expert in javascript closure... If I well understood your answer, in on_progress I will have a reference to "img_src" and "filePath", but how can I get a reference to object "entry" like the one used in success callback?
This is the code I would like to use:
ImgCache.cacheFile = function (img_src, success_callback, error_callback, on_progress) {
if (!Private.isImgCacheLoaded() || !img_src) {
return;
}
img_src = Helpers.sanitizeURI(img_src);
var filePath = Private.getCachedFileFullPath(img_src);
filePath = filePath + '.tmp';
var fileTransfer = new Private.FileTransferWrapper(ImgCache.attributes.filesystem);
fileTransfer.download(
img_src,
filePath,
function (entry) {
if (Helpers.isCordovaAndroid()) {
entry.moveTo(ImgCache.attributes.dirEntry, entry.name.substr(0, entry.name.length - 4));
entry.getMetadata(function (metadata) {
if (metadata && metadata.hasOwnProperty('size')) {
Helpers.logging('Cached file size: ' + metadata.size, LOG_LEVEL_INFO);
Private.setCurrentSize(ImgCache.getCurrentSize() + parseInt(metadata.size, 10));
} else {
Helpers.logging('No metadata size property available', LOG_LEVEL_INFO);
}
});
Helpers.logging('Download complete: ' + Helpers.EntryGetPath(entry), LOG_LEVEL_INFO);
// iOS: the file should not be backed up in iCloud
// new from cordova 1.8 only
if (entry.setMetadata) {
entry.setMetadata(
function () {
/* success*/
Helpers.logging('com.apple.MobileBackup metadata set', LOG_LEVEL_INFO);
},
function () {
/* failure */
Helpers.logging('com.apple.MobileBackup metadata could not be set', LOG_LEVEL_WARNING);
},
{ 'com.apple.MobileBackup': 1 }
// 1=NO backup oddly enough..
);
}
if (success_callback) { success_callback(); }
}
},
function (error) {
if (error.source) { Helpers.logging('Download error source: ' + error.source, LOG_LEVEL_ERROR); }
if (error.target) { Helpers.logging('Download error target: ' + error.target, LOG_LEVEL_ERROR); }
Helpers.logging('Download error code: ' + error.code, LOG_LEVEL_ERROR);
if (error_callback) { error_callback(); }
},
function (progressEvent) {
if (! Helpers.isCordovaAndroid()) {
var entry = ?????????????????????;
if (progressEvent.loaded == progressEvent.total) {
entry.moveTo(ImgCache.attributes.dirEntry, entry.name.substr(0, entry.name.length - 4));
entry.getMetadata(function (metadata) {
if (metadata && metadata.hasOwnProperty('size')) {
Helpers.logging('Cached file size: ' + metadata.size, LOG_LEVEL_INFO);
Private.setCurrentSize(ImgCache.getCurrentSize() + parseInt(metadata.size, 10));
} else {
Helpers.logging('No metadata size property available', LOG_LEVEL_INFO);
}
});
Helpers.logging('Download complete: ' + Helpers.EntryGetPath(entry), LOG_LEVEL_INFO);
// iOS: the file should not be backed up in iCloud
// new from cordova 1.8 only
if (entry.setMetadata) {
entry.setMetadata(
function () {
/* success*/
Helpers.logging('com.apple.MobileBackup metadata set', LOG_LEVEL_INFO);
},
function () {
/* failure */
Helpers.logging('com.apple.MobileBackup metadata could not be set', LOG_LEVEL_WARNING);
},
{ 'com.apple.MobileBackup': 1 }
// 1=NO backup oddly enough..
);
}
if (success_callback) { success_callback(); }
}
}
},
);
};
Where I have to find a correct substitution for:
var entry = ?????????????????????;
Can you help me?
Ok, I see what you mean now. I thought you were talking about your code that calls the function where you can use closure to get back your variables once the callbacks get called.
Inside the 3rd callback of the download function, you can only access img_src and filePath, unfortunately the cordova api doesn't provide a pointer to an entry at this stage (supposedly because it doesn't exist until download is complete).
I don't think we should go down the onprogress path to solve that success callback not being called. First because as you can see you can't do much, and also because onprogress is not supported on other platforms (like Windows).
There is an important issue with that file plugin, it will have to be fixed at some point, I don't think it's worth hacking your way around it.
Can't you work with another version of that file transfer plugin in the meantime? either the newest source, or an older release - even if it means using an older cordova release -.
Ok, I'll try playing with other versions of the file and file-transfer plugin or wait for a new one. I will inform you if I find a solution that can be used in imgcache.js. Thank you.
@piciuriello any news on that?
No news. The project I was working is in stand-by in this moment, so I didn't investigated any further. I don't know if the file-transfer plugin was fixed.
I also have the same problem
Modifying ImgCache.cacheFile
to the following has solved the problem for me (tested on Android):
ImgCache.cacheFile = function (img_src, success_callback, error_callback, on_progress) {
if (!Private.isImgCacheLoaded() || !img_src) {
return;
}
img_src = Helpers.sanitizeURI(img_src);
var filePath = Private.getCachedFileFullPath(img_src);
var fileTransfer = new Private.FileTransferWrapper(ImgCache.attributes.filesystem);
fileTransfer.download(
img_src,
filePath + '.' + (+new Date()) + ".tmp",
function (entry) {
entry.moveTo(ImgCache.attributes.dirEntry, Private.getCachedFileName(img_src), function (entry) {
entry.getMetadata(function (metadata) {
if (metadata && ('size' in metadata)) {
ImgCache.overridables.log('Cached file size: ' + metadata.size, LOG_LEVEL_INFO);
Private.setCurrentSize(ImgCache.getCurrentSize() + parseInt(metadata.size, 10));
} else {
ImgCache.overridables.log('No metadata size property available', LOG_LEVEL_INFO);
}
});
ImgCache.overridables.log('Download complete: ' + Helpers.EntryGetPath(entry), LOG_LEVEL_INFO);
// iOS: the file should not be backed up in iCloud
// new from cordova 1.8 only
if (entry.setMetadata) {
entry.setMetadata(
function () {
/* success*/
ImgCache.overridables.log('com.apple.MobileBackup metadata set', LOG_LEVEL_INFO);
},
function () {
/* failure */
ImgCache.overridables.log('com.apple.MobileBackup metadata could not be set', LOG_LEVEL_WARNING);
},
{ 'com.apple.MobileBackup': 1 }
// 1=NO backup oddly enough..
);
}
if (success_callback) {
success_callback();
}
}, function (err) {
ImgCache.overridables.log('Failed to move file ' + JSON.stringify(err), LOG_LEVEL_ERROR);
});
},
function (error) {
if (error.source) { ImgCache.overridables.log('Download error source: ' + error.source, LOG_LEVEL_ERROR); }
if (error.target) { ImgCache.overridables.log('Download error target: ' + error.target, LOG_LEVEL_ERROR); }
ImgCache.overridables.log('Download error code: ' + error.code, LOG_LEVEL_ERROR);
if (error_callback) { error_callback(); }
},
on_progress
);
};
- download the image to a temp file
filePath + '.' + (+new Date()) + ".tmp",
(I have to add timestamp to the file name because otherwise, writing to the same .tmp file twice, may result in a part of the image to overlay another part, quite odd.. may be related to the structure of PNG files.) (Alternatively, if you don't want to use timestamp, check whether the temp file exists, if it does, delete it and then proceed) - after the download finishes, move the file to where it should be.
entry.moveTo(ImgCache.attributes.dirEntry, Private.getCachedFileName(img_src), function (entry)
Now I don't see any half-loaded corrupt images.
Hope it helps
This is a solution very similar to the one I suggested here. The problem wasn't with Android but with iOS were the success callback of fileTransfer.download wasn't fired. The Cordova issues I pointed are now set as fixed, but I didn't tried again, so I don't know if the fix they did solved the problem.
Thanks @piciuriello, fix the problem for me