Author: Lu Huang
This document is a starting point for engaging the community and standards bodies in developing collaborative solutions fit for standardization. As the solutions to problems described in this document progress along the standards-track, we will retain this document as an archive and use this section to keep the community up-to-date with the most current standards venue and content location of future work and discussions.
UWP apps often make use of the ApplicationData.LocalFolder WinRT API for access to local storage. This API manages storage in a system file directory unique to the Package Family Name (PFN) of the installed app package. A UWP app distributed by the Microsoft Store may be replaced with a PWA app with the same PFN. When this update takes place on client machines, existing files in the LocalFolder directory become inaccessible to the PWA as WinRT APIs are not directly exposed to the web.
In this explainer, we propose a solution that allows Microsoft Store PWAs to read and delete files from the LocalFolder directory belonging to their PFN. This allows apps to provide a more seamless user experience after an update and to reclaim storage space.
The proposed solution works by making use of the Origin Private File System (OPFS) and the File System Access APIs. It exposes an app's LocalFolder file system directory as an entry in the app origin's OPFS root directory.
LocalFolder directory specific to its PFN.LocalFolder directory.LocalFolder directory.navigator.storage.getDirectory() is an existing API that returns a promise to a FileSystemDirectoryHandle which represents the root of directory the origin's OPFS storage space. By default, an origin's OPFS root directory has no entries.
When configured correctly, the directory handle for OPFS root will contain an additional entry that represents the app's LocalFolder directory. This entry can be retrieved as a FileSystemDirectoryHandle by calling .entries() or .getDirectoryHandle(...) on the OPFS root directory handle.
Like other OPFS handles, the LocalFolder directory handle and its contents will have readwrite permission by default. Unlike other handles with readwrite permission, a FileSystemFileHandle from LocalFolder will always throw a NoModificationAllowedError DOMException if createWritable() is used, or if getFileHandle() or getDirectoryHandle() is used with the create option being true. This allows data in LocalFolder to be read and deleted but not created or edited.
An entry for LocalFolder will only be visible in the Microsoft Store PWA context and when the related_applications field in the app's web app manifest is configured correctly.
To be in a Microsoft Store PWA context:
navigator.storage.getDirectory() must be called from:
In order to opt in to enabling LocalFolder access, the app needs to configure related_applications in its web app manifest to identify the PFN of its Windows app package. Only the web app manifest part of the configuration needed to support getInstalledRelatedApps is needed here to enable LocalFolder access.
"related_applications": [{
"platform": "windows",
"id": "PACKAGE_FAMILY_NAME!APPLICATION_ID"
}]
Note: LocalFolder can be accessed from a document in any display-mode. The loaded document does not have to be in an app window but does have to be in scope of the installed app.
The LocalFolder entry within the OPFS root directory can be found under the name microsoft_store_app_local_folder_{PFN} where {PFN} is the Package Family Name of the app.
Contents in LocalFolder can be deleted while the LocalFolder entry cannot itself be deleted - this is to mimic the effects of the WinRT ClearAsync(...) API. LocalFolder's contents can be deleted by calling FileSystemHandle:remove() or FileSystemDirectoryHandle:removeEntry().
See example usage in section below.
| Action | Result |
|---|---|
.remove() on OPFS root handle |
Will not enumerate or delete LocalFolder even if the recursive option is used. |
.removeEntry(...) on OPFS root handle using LocalFolder's correct entry name |
Will not enumerate or delete LocalFolder even if the recursive option is used. No DOMException will be thrown. This is consistent with calling .removeEntry() on names that cannot be found. |
.remove() on LocalFolder's handle |
Will throw an InvalidModificationError DOMException. Has no effect on LocalFolder directory and its contents. |
.remove() or .removeEntry(...) on any handle within LocalFolder |
Identified handles will be removed. |
As the LocalFolder directory is in a system file directory separate from Chromium's storage location for OPFS, whether an entry for LocalFolder is presented in OPFS root's entries does not affect the available storage estimate returned by navigator.storage.estimate(). As LocalFolder takes up space on disk, clearing the contents of LocalFolder can increase the estimate of available storage.
In Chromium, the underlying storage for OPFS does participate in eviction if an origin is not marked as persistent. As the LocalFolder directory is in a system file directory separate from OPFS's underlying storage, it will not be affected by origin based storage eviction.
To avoid name collisions, the LocalFolder entry under the OPFS root directory is assigned a specific name that contains the Package Family Name. If an entry with the same name already exists under the OPFS root directory, an entry representing the LocalFolder directory will not be made visible until the existing entry is renamed or otherwise removed.
// Using .getDirectoryHandle()
let opfsRoot = await navigator.storage.getDirectory();
let localFolder = await opfsRoot.getDirectoryHandle("microsoft_store_app_local_folder_APPID.37853FC22B2CE_6rarf9sa4v8jt");
Example: looking for the LocalFolder entry under the OPFS root directory.
Note: the create parameter should be {create:false} to determine if the system LocalFolder is present. If create is true, getDirectoryHandle will always return a valid handle as it creates a directory handle of the same name in actual OPFS storage. If omitted, create is false by default.
// .entries()
let opfsRoot = await navigator.storage.getDirectory();
for await (const [key, value] of opfsRoot.entries()) {
if (key === 'microsoft_store_app_local_folder_APPID.37853FC22B2CE_6rarf9sa4v8jt') {
// ...
}
}
Example: Looking for the LocalFolder entry by iterating through entries under the OPFS root directory.
let localStorage = await opfsRoot.getDirectoryHandle("microsoft_store_app_local_folder_APPID.37853FC22B2CE_6rarf9sa4v8jt");
for await (const [key, value] of localStorage.entries()) {
value.remove({ recursive: true });
}
Example: deleting contents of LocalFolder by calling .remove() on its child handles.
Note: The recursive parameter needs to be true to delete sub-directories recursively. By default, it is false. It has no effect on file handles.
There are other WinRT storage APIs similar to LocalFolder that can be exposed the same way through OPFS directory handles, but are not currently planned to be supported.
We considered creating a new API on navigator to return a FileSystemDirectoryHandle of the app's LocalFolder directory. This was rejected primarily because of the long term cost of maintaining a non-standard API.
We considered a solution that migrates the contents of the LocalFolder directory into OPFS storage. The drawbacks to this solution includes:
navigator.A malicious party could attempt to gain access to files outside of the boundary of the LocalFolder directory.
A malicious party could create executable files on a user-visible part of the file system without requiring the user to provide input through a file picker UI.
Third party scripts could read data in LocalFolder even though the app developer is not aware of this feature.
Scripts in embedded iframes could try to access data in LocalFolder.
The FileSystemHandle design does not support walking up the directory structure.
The LocalFolder directory handle and its contents will not allow the modification of existing files or creation of new files. This prevents the creation of executable files and the creation of files outside of the LocalFolder directory. Additionally, the implementation of the File System Access API in Chromium prevents creation of some executable file extensions.
The app has to opt in explictly using the related_applications field in the web app manifest. The app developer must understand the risks involved and be responsible for the actions of third party scripts used. Most apps that do not intend to access LocalFolder will not expose it unknowingly to third parties.
The LocalFolder handle entry will not be visible to cross-origin iframes as OPFS storage is partitioned by origin.
LocalFolder data in the file system can be accessed by a PWA through a Web API without prompting the user for permission. There could be personally identifiable information (PII) contained within this data.
Any script (including those from third-party origins that may not be owned by the application developer) that is loaded into a document from the same origin as the top-level frame can use the presence of a LocalFolder entry to determine if a Windows Store app associated with the origin is installed on the client machine. This information could potentially be used for fingerprinting.
display-mode is an existing method to check if a site is installed locally as an app but this method does not work when the user navigates the site in a normal browser tab or fullscreen.The user consented to the app's use of local storage through app installation from the Microsoft Store. We think it is acceptable for a newer version of this app to access the data previously created using the same PFN. The app (both the UWP and PWA implementation) has permission from the user to store and access the data even if it contains PII. The app needs to take care to protect user data.
Requiring the app to include the related_applications in its web app manifest limits the risk to apps that publicly declares a relation to a Windows app package.