Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File Download and Access Issues on iOS 17.5.1 with Cordova #628

Closed
andreafrancioni opened this issue Jul 11, 2024 · 12 comments
Closed

File Download and Access Issues on iOS 17.5.1 with Cordova #628

andreafrancioni opened this issue Jul 11, 2024 · 12 comments
Labels

Comments

@andreafrancioni
Copy link

andreafrancioni commented Jul 11, 2024

Versions:

•	iOS Version: 17.5.1
•	Cordova Version: 12.0.0 ([email protected])
•	Cordova Plugin File Version: 8.1.0

Description:

I am experiencing an issue with downloading and accessing media files on iOS 17.5.1 using Cordova. The code works perfectly on Android devices, but on iOS, the files are downloaded but cannot be accessed or played.

Steps to Reproduce:

1.	Prompt the user to download media files.
2.	Download the files and save them in the cache directory using cordova.file.dataDirectory.
3.	Attempt to access and play the downloaded files.

Current Behavior:

On iOS, the files are downloaded successfully, but when attempting to access or play these files, the app fails to locate the files, and they appear to be missing or inaccessible.

Expected Behavior:

The downloaded files should be accessible and playable from the cache directory, similar to the behavior observed on Android devices.

Relevant Code:

setDialog(
"E' necessario scaricare i contenuti audio/video, vuoi continuare?",
"yes-no",
(answer) => {
if (answer === "yes") {
this.downloadMediaFiles();
}
}
);

getCacheDirectoryPath() {
// Ottieni il percorso della directory di cache
window.resolveLocalFileSystemURL(cordova.file.dataDirectory, (dirEntry) => {
this.cacheDirectory = dirEntry.toURL();
console.log("Cache directory: " + this.cacheDirectory);
}, (error) => {
console.error("Errore nel risolvere il file system: " + error.toString());
});
}

async downloadMediaFiles() {
this.downloadProgress = 0;
this.downloadMessage = Scaricamento di 0/${this.mediaFiles.length} file;

for (let i = 0; i < this.mediaFiles.length; i++) {
await this.downloadFile(this.server_base_path + this.mediaFiles[i]);
this.downloadProgress = ((i + 1) / this.mediaFiles.length) * 100;
this.downloadMessage = Scaricamento di ${i + 1}/${this.mediaFiles.length} file;
}

this.downloadMessage = "Download completato!";
localStorage.setItem('downloadedMediaFiles', JSON.stringify(this.mediaFiles));

setTimeout(() => {
this.showDownloadDialog = false;
}, 300);
}

async downloadFile(url) {
console.log("Download URL:", url);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const blob = await response.blob();
const fileName = url.split('/').pop();
console.log("File name:", fileName);

window.resolveLocalFileSystemURL(this.cacheDirectory, (dirEntry) => {
  dirEntry.getFile(fileName, { create: true, exclusive: false }, (fileEntry) => {
    fileEntry.createWriter((fileWriter) => {
      fileWriter.onwriteend = () => {
        console.log("Download completato: " + fileEntry.toURL());
      };
      fileWriter.onerror = (error) => {
        console.error("Errore nel download del file: " + error.toString());
      };
      console.log(blob);
      fileWriter.write(blob);
    }, (error) => {
      console.error("Errore nella creazione del file: " + error.toString());
    });
  }, (error) => {
    console.error("Errore nel recuperare il file: " + error.toString());
  });
}, (error) => {
  console.error("Errore nel risolvere il file system: " + error.toString());
});

} catch (error) {
console.error("Errore nel download del file: " + error.toString());
}
}

video source are build like this:
<video preload="auto" playinline="" autoplay="" controls="" controlslist="nofullscreen"><source src="file:///var/mobile/Containers/Data/Application/(APP_ID)/Library/NoCloud/(FILENAME).mp4" type="video/mp4"></video>

Observations:

•	The issue appears to be specific to iOS.
•	The file download and access process works correctly on Android devices.
•	When attempting to download a file through Safari’s debugger, it can be accessed correctly if saved manually.

Possible Causes:

•	Differences in file system access permissions between iOS and Android.
•	Possible bug in cordova-plugin-file or Cordova itself on iOS 17.5.1.

Any insights or suggestions on how to resolve this issue would be greatly appreciated. Specifically, if there are known issues with cordova-plugin-file on iOS 17.5.1 or if additional configurations are required, please let me know.

@phynae
Copy link

phynae commented Aug 1, 2024

Same here! Working when reading and writing using the FileSystem-API but not when passing the file-url to for example a HTML-ImageTag. Furthermore my attempt was to convert it using the window.WkWebView.convertFilePath.

@phynae
Copy link

phynae commented Aug 2, 2024

The following scenarios were tested, but the issue persisted:

  • Older Cordova-Lib version ([email protected])
  • Older ios-platform version ([email protected])
  • Older cordova-plugin-file version (7.0.0)
  • Deploying older apps from XCode on an iPhone
  • Older iOS Version (iOS 12)

Workaround (not recommended though):

  • Converting the file into a Data-URL and then putting this url in the src-Tag of an image

Weird thing is, that when downloaded the already released version of the same app (release was about half a year ago), the issue is gone

It almost seems like it has to do with X-Code. Unfortunately it was not possible for me to check that.

@breautek
Copy link
Contributor

breautek commented Aug 9, 2024

If you're app is configured to use schemes (e.g. your html page is loaded over http(s)://... on android or app://... (the scheme is customizable on iOS) then using file:// paths in your DOM will likely get blocked by CORs. If you're app is loaded over the file:// protocol, then using file:// paths in the DOM should work as expected but is considered less secure.

v7 (if I recall correctly) of the file plugin introduce changes to produce a DOM-usable url when using the FileEntry.toURL() method.

The pasted code is not well formatted and is hard to read but here are some things to check:

  1. Make sure .toURL() is being used.
  2. The ionic webview may not parse the URL produced by .toURL() properly, so if the ionic webview is being used, attempt to test using the default cordova webview.

If it still doesn't work, please let us know what version of cordova-ios you're using and whether you're using schemes or loading directly the file:// protocol.

@phynae
Copy link

phynae commented Aug 12, 2024

Hi, we are using [email protected]. I've already tried using the .toURL function without success. If your assumption that the change was introduced in v7 is true, then i wonder why it didn't work when i build using an older version.

Furthermore, as mentioned above, we faced the same error when re-deploying old apps (that previously had no such issues) that use way older versions of cordova-ios onto iPhones running a pretty modern iOS-Version.

@dele1907
Copy link

I also have the problem which @phynae mentioned above.
Passing the file URL to an image tag is not working for me, but when I access files for reading or writing using the filesystem-API everything works fine.

I am operating on [email protected] with [email protected].
Additionally I have tried to get rid of the issue using older versions of [email protected], [email protected] and [email protected] but this will not change anything.
Moreover the .toURL function also did not help me to solve the problem.

@breautek breautek added bug and removed info-needed labels Aug 27, 2024
@breautek
Copy link
Contributor

I was able to take a deeper look this morning and .toURL() on file entries is suppose to produce a DOM-usable url, but it won't on ios when using app schemes.

It's missing an implementation override the scheme task and to map a scheme URL to the file on disk to return an asset over the scheme.

Workarounds could include reading the asset as a blob and using blob urls, or if the asset is small enough, read as a data uri. Don't forget that blob urls needs to be released when it's not being used anymore.

If you're using a large asset like a video file, then there is no "good" workaround available afaik.

I can't really give any timeline on when a proper solution will be made available but it is on our radar now.

@andreafrancioni
Copy link
Author

Hi breautek, its been long time since last time i tried using cached file on ios, there's been new implementation or have you implemented new functionality about this problem?

Thank you for your patience

@breautek
Copy link
Contributor

breautek commented Oct 29, 2024

This would be specific to iOS but I've been told that since the addition of schemes on iOS there is a WkWebView.convertFilePath API available in JS.

It's not ideal since you would have to do a platform check but you could try it as a work around.

I'm not familiar with the API, but I believe it would work something like:

let myImage = document.getElementById('myImage');
myImage.src = WkWebView.convertFilePath(theFilePath);

@andreafrancioni
Copy link
Author

Hi breautek, today i tested the app on IOS Simulator and it works fine, but when i build the app and install it on real devices (iPhone 13 Pro Max 18.2.1) i got the problem, i cant figure out whats its going on.
I think its a problem with access permission to mobile folder, cause on Simulator the path of cached files are on:
file:///Users/andrea.francioni/Library/Developer/CoreSimulator/Devices/F8023E24-5204-496C-B442-0CB634870DB9/data/Containers/Data/Application/D75CC093-800E-4846-874A-F8022C4AD5B2/Library/NoCloud/indicazioni_piano_terra_def.mp4

And on this path i can write/read file without any problem and they load everywhere on DOM.

While the path on real device is:
file:///var/mobile/Containers/Data/Application/EC749CD6-F9FE-405B-8D52-D4C6127677D0/Library/NoCloud/indicazioni_intro_def.mp4

And on this path i can write without problem but i didnt see it when i load it on dom.

I also tryed add a reader object:

  loadCachedVideo(v) {
      if (!v) {
        console.error("Il parametro 'v' è vuoto.");
        return null;
      }
      const videoUrl = this.cacheDirectory + this.getFileName(v);
      console.log('video url: ' + videoUrl);
      window.resolveLocalFileSystemURL(videoUrl, (fileEntry) => {
        fileEntry.file((file) => {
          const reader = new FileReader();
          reader.onloadend = () => {
            console.log('File letto con successo:', reader.result);
          };
          reader.readAsArrayBuffer(file);
        }, (error) => {
          console.error("Errore nella lettura del file:", error);
        });
      }, (error) => {
        console.error("Errore nel recuperare il file:", error);
      });
      return videoUrl;
    }

and the result of console.log of reader.onloadend is:
"File letto con successo: -ArrayBuffer (byteLenght: 15310703, resizable: false, maxByteLenght: 15310703...

Also tryed using the workaround u gave me with WkWebView.convertFilePath(theFilePath); but i didnt understand how to use it.

@breautek
Copy link
Contributor

breautek commented Jan 14, 2025

and the result of console.log of reader.onloadend is:
"File letto con successo: -ArrayBuffer (byteLenght: 15310703, resizable: false, maxByteLenght: 15310703...

This looks like it worked as intended, it has an ArrayBuffer filled with bytes. The array buffer alone isn't usable but you can wrap it around a Blob (new Blob([arrayBuffer])) and then use URL.createObjectURL(blob). HOWEVER this is very inefficient, transferring large amounts of data over the cordova bridge is very slow and you'll need to use URL.revokeObjectURL after you don't need it anymore. Forgetting to revoke it will force the video file to be retained in memory which will be quite a serious memory leak considering how large videos can be.

Also tryed using the workaround u gave me with WkWebView.convertFilePath(theFilePath); but i didnt understand how to use it.

Using WkWebView.convertFilePath(theFilePath); is definitely a better path. The output of convertFilePath is a source string that can be used directly in the DOM. WkWebView.convertFilePath is a iOS-specific API however, so instead of using it directly, you should use the FileEntry.toURL() API, which allows you to be platform independent.

Suppose you had a <video id="myVideo" /> tag somewheres in your HTML, here is an example:

loadCachedVideo(v) {
      if (!v) {
        console.error("Il parametro 'v' è vuoto.");
        return null;
      }
   
      const videoUrl = this.cacheDirectory + this.getFileName(v);
      console.log('video url: ' + videoUrl);
      window.resolveLocalFileSystemURL(videoUrl, (fileEntry) => {
          const videoElement = document.getElementByID("myVideo");
          videoElement.src = fileEntry.toURL();
          videoElement.play();
      });
});

Note that I'd recommend using file plugin 8.1.3 as it contains fixes surrounding .toURL() usage on iOS. Earlier versions will not work if you're also using schemes (e.g. app is hosted on app:// or something other than file://)

This issue should be fixed by This issue should be fixed by #642 if you can confirm (just missed it when authoring the PR) but would be great if you can confirm.

@andreafrancioni
Copy link
Author

andreafrancioni commented Jan 14, 2025

Note that I'd recommend using file plugin 8.1.3 as it contains fixes surrounding .toURL() usage on iOS. Earlier versions will not work if you're also using schemes (e.g. app is hosted on app:// or something other than file://)

This issue should be fixed by This issue should be fixed by #642 if you can confirm (just missed it when authoring the PR) but would be great if you can confirm.

Hi I'have checked the fix u linked to me, and i looked that you store cache file on tmp folder, so i tryed using tempDirectory on cordova.file instead of dataDirectory and it worked as should without changing anything.

I assume that folder Library/NoCache have no enaugh permission on IOS to load on DOM.

Could you please try saving and using file as u did on #642 but using Library/NoCache folder?

Cause the problem of using tmp folder is that is not persistent, so when i close the app i lose all my cached data, and should be downloaded each time users would use this app

ps. im not currently using scheme, im using the default scheme which return me using .toURL() the default file://
pps. file plugin that im using is 8.1.3 as u recommend

@breautek
Copy link
Contributor

ps. im not currently using scheme, im using the default scheme which return me using .toURL() the default file://

There are known issues surrounding using file:// hosted applications, which was noted on the blog post.

I assume that folder Library/NoCache have no enaugh permission on IOS to load on DOM.

Could you please try saving and using file as u did on #642 but using Library/NoCache folder?

WKWebKit by default (and for security reasons) will only load content within the read-only application content. That is content that is packaged shipped with the ios binary. Everything outside of this directly is considered outside of the "sandbox", which includes the app's writable data directories.

The .toURL() returns a file:// path with not using schemes to maintain legacy behaviour, which only valid for the legacy UIWebView. This method does not work on the newer WKWebView as explained above.

If you can't switch to schemes, then using the ArrayBuffer / Blob url method is the only other way to load videos into the DOM, since you're bypassing WkWebkit restrictions at this point and it's the safest way to do that. You'll need to maintain state to determine when a particular video blob url is no longer used and can be freed to avoid memory leaks.

Using Schemes

It would be recommended to switch to schemes if possible, it has several other advantages besides enabling DOM urls. This can be done by adding the scheme and hostname preferences in your config.xml:

<widget ...>
    ...
    <platform name="ios">
        <preference name="scheme" value="app" />
        <preference name="hostname" value="localhost" />
        ...
    </platform>
</widget>

The main things to know when switching is doing this will cause your document.origin to change from null to app://localhost (or whatever what you choose for the scheme). This means web storage associated with the origin will not be accessible, as you'll get a new storage container. So if you use cookies, local storage, IndexedDB, etc... these containers will effectively be reset. Technically the old data will still exists in the app's data folders but they will not be accessible by the webview.

If you don't use the web storage containers then it should be pretty safe for you to switch to schemes.

You're free to choose whatever scheme and hostname you like, but there is a limitation and you cannot choose any scheme that the WKWebView already handles. This means common protocols like http, or https cannot be used for the scheme value. If you choose an unavailable scheme, cordova will silently error and fallback to app.

Cause the problem of using tmp folder is that is not persistent, so when i close the app i lose all my cached data, and should be downloaded each time users would use this app

The temp folder should only be cleared for 3 reasons:

  • The application clears the files manually
  • The user requests the cache to be cleared on an application
  • The disk storage space is so dangerously low that the OS needs to clear space for to function properly, in which case it will generally start clearing caches of apps that hasn't been used first.

Otherwise the temp directory is persistent.

Apache plugins that does data writes tends to choose the cache/temp directory because the plugin makes no assumptions and leaves it to the app to decide what it should do with the file. So if the file should be "more" persistent, then the application should move the file to the data directory using the file plugin's APIs. Do note that user's can still request app data to be cleared as well.

I'm closing this issue because I believe I have a good enough understanding of your environment now and I don't believe there is a bug that Cordova can take action on. If you have further issues using .toURL() with schemes enabled, then a new issue can be created within that context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants