File System Access API - Something weird

455 views
Skip to first unread message

Robbi

unread,
Sep 3, 2022, 6:52:17 PM9/3/22
to Chromium Extensions
Hi developers,
I'm just doing some test with a small extension which use FSA API.
Clicking on action icon a page is opened.
In this page there are 2 buttons; one for choose a directory to save a simple text file,
and the other to read that file in that folder and show its content.
So far no problem.
Of couse, each button trigger a permission request at first time.
Furthermore, the directory handler is stored in a indexeDB.

Now if I duplicate this tab several times (sometime it's enough 2 time, sometime 4 or 5 times) when I click the button to read the file a new permission request is prompt.
The question is very simple: WHY?
Thank you in advance.

P.S. If needed I can post the code (which is not very long) or a video.

PhistucK

unread,
Sep 3, 2022, 7:05:01 PM9/3/22
to Robbi, Chromium Extensions
Does it happen using a normal website as well? If not, it might just be a bug.

PhistucK


--
You received this message because you are subscribed to the Google Groups "Chromium Extensions" group.
To unsubscribe from this group and stop receiving emails from it, send an email to chromium-extens...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/chromium-extensions/1adeb809-2701-423a-b0cf-b9e2daeed9c8n%40chromium.org.

Robbi

unread,
Sep 3, 2022, 7:40:38 PM9/3/22
to Chromium Extensions, PhistucK, Chromium Extensions, Robbi
Could be me too.
This is the code

//manifest.json - begin
{
    "manifest_version": 3,
    "name": "fsa MV3",
    "short_name": "fsa",
    "description": "fsa",
    "version": "0.0.0.2",
    "background": {
        "service_worker": "script/background.js"
    },
    "action": {
        "default_title": "fsa",
        "default_icon": "img/icon.png"
    },
    "icons": {
        "16": "img/icon.png"
    }
}

//manifest.json - end

// script/background.js - begin
chrome.action.onClicked.addListener(_ =>
    chrome.tabs.create({url: chrome.runtime.getURL('fsa.html')})

)

// script/background.js - end

//fsa.html - begin
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Directory Handle</title>
        <script src="script/myIndexedDBLib.js"></script>
        <script src="script/fsa.js"></script>
    </head>
    <body>
        <button id="directory">Choose a folder where save your secret</button>
        <pre></pre>
        <br><br>
        <button id="retrieve">Retrieve your secret</button>
    </body>
</html>

//fsa.html - end

// script/myIndexedDBLib.js - begin
function loadDBs(name, vers) {
    return new Promise((ok, ko) => {
        var db;
        var res = { 'db': null, 'upgraded': false, 'msg': null};
        var req = indexedDB.open(name, vers);
       
        req.addEventListener('error', e => {
            ko({ 'db': res.db, 'upgraded': res.upgraded, 'osExist': res.osExist, 'msg': req.errorCode})
        });
       
        req.addEventListener('success', e => {
            db = e.target.result;
            db.addEventListener('error', e => {
                ko({ 'db': res.db, 'upgraded': res.upgraded, 'msg': 'DB error: ' + e.target.error.name + ' - ' + e.target.error.message})
            });
            ok({'db': db, 'upgraded': res.upgraded, 'msg': 'OK'})
        });
       
        req.addEventListener('upgradeneeded', e => {
            res.upgraded = true;
            db = e.target.result;
            db.addEventListener('error', e => {
                ko({'db': res.db, 'upgraded': res.upgraded, 'msg': 'DB error: ' + e.target.error.name + ' - ' + e.target.error.message})     //20200828
            });
            if (!db.objectStoreNames.contains('keyval'))
                var tbl = db.createObjectStore("keyval")
        })
    })
}

function getRec(dbObj, tableName, keyP) {
    return new Promise((ok, ko) => {
        var req = dbObj.transaction(tableName, 'readonly').objectStore(tableName).get(keyP);
        req.addEventListener('error', e => { ko(e) });
        req.addEventListener('success', e => { ok(req.result) })
    });        
}

function putRecs(dbObj, tableName, ra) {
    return new Promise((ok, ko) => {
        var transObj = dbObj.transaction(tableName, "readwrite");
        transObj.addEventListener('complete', e => { ok() });
        transObj.addEventListener('error', e => { ko(e) });
        var osTmp = transObj.objectStore(tableName);
        ra.forEach(v => { osTmp.put(v.value, v.key) })
    });
}

// script/myIndexedDBLib.js - end

// script/fsa.js - begin
document.addEventListener('DOMContentLoaded', _ => {
    async function saveSecret(s) {
        var directoryHandle = await window.showDirectoryPicker();
       
        var r = await loadDBs('keyval-store', 1);
        var myDB = r.db;
        putRecs(myDB, 'keyval', [{'key': 'directory', 'value': directoryHandle}]);
       
        // In this new directory, create a file named "Sec.ret".
        const newFileHandle = await directoryHandle.getFileHandle('Sec.ret', { create: true });
        // Create a FileSystemWritableFileStream to write to.
        const writable = await newFileHandle.createWritable();
        // Write the contents of the file to the stream.
        await writable.write(s);
        // Close the file and write the contents to disk.
        await writable.close();
        document.querySelector("pre").textContent = `Stored directory handle for "${directoryHandle.name}" in indexedDB`
    }
    async function readSecret() {
        async function verifyPermission(handle, readWrite) {
            const options = {};
            if (readWrite)
                options.mode = 'readwrite';

            // Check if permission was already granted. If so, return true.
            var qrp = await handle.queryPermission(options);
            if (qrp === 'granted')
                return true;
            else {    // Request permission. If the user grants permission, return true...
                qrp = await handle.requestPermission(options);
                return (qrp === 'granted')    // otherwise return false.
            }
        }

        var r = await loadDBs('keyval-store', 1);
        var myDB = r.db;
        var directoryHandle = await getRec(myDB, 'keyval', 'directory');
       
        var secret = null;
        var perm = await verifyPermission(directoryHandle, false);
        if (!perm)
            return secret;
        try {
            const fileHandle = await directoryHandle.getFileHandle('Sec.ret');
            const file = await fileHandle.getFile();
            secret = await file.text()
        } catch(err) {
            alert('file not found.');
        } finally {
            return secret
        }
    }

    // Directory handle
    document.getElementById("directory").addEventListener("click", async _ => {
        var secret = window.prompt('Choose a secret word', 'abracadabra');
        await saveSecret(secret)
    });

    document.getElementById("retrieve").addEventListener("click", async _ => {
        var ss = await readSecret();
        alert('Your secret is:\n' + ss)
    })
})

// script/fsa.js - end

PhistucK

unread,
Sep 3, 2022, 8:02:47 PM9/3/22
to Robbi, Chromium Extensions
Again, is this an extension only issue, or can you reproduce the same issue with a normal web page?

PhistucK

Robbi

unread,
Sep 3, 2022, 8:18:43 PM9/3/22
to Chromium Extensions, PhistucK, Chromium Extensions, Robbi
If I keep the html file with 2 js files and I lanch the html file (fsa.html) in chrome I get the same results.
Some time I have to duplicate the tab many times to get this issue, but I notice that if I remove one of the cloned tab the same issue comes more easil.

Jackie Han

unread,
Sep 4, 2022, 1:00:25 AM9/4/22
to Robbi, Chromium Extensions
This is because the File System permissions are not persistent.

MDN says "However, other than through the user revoking permission, a handle retrieved from IndexedDB is also likely to return "prompt"."

Web.dev says "Since permissions currently are not persisted between sessions, you should verify ……" and "While FileSystemHandle objects can be serialized and stored in IndexedDB, the permissions currently need to be re-granted each time, which is suboptimal. Star crbug.com/1011533 to be notified of work on persisting granted permissions."

So your code is correct. The prompt behavior is current implementation.

--

Robbi

unread,
Sep 4, 2022, 5:49:57 AM9/4/22
to Chromium Extensions, Jackie Han, Chromium Extensions, Robbi
Hi @Jackie, @PhistucK
thank you for your replies.
@Jackie : I took inspiration from the second article.
"The web app can continue to save changes to the file without prompting until all tabs for its origin are closed. Once a tab is closed, the site loses all access. The next time the user uses the web app, they will be re-prompted for access to the files."
So why cloning (or closing) a same origin tab can re-prompt the permission request since a same origin tab still exists ?

However, I guess using this API in an extension popup is somewhat discouraged :-(

Jackie Han

unread,
Sep 4, 2022, 6:13:24 AM9/4/22
to Robbi, Chromium Extensions
Basically, this is similar to the concept of session. At least it ensures that the obtained permission is valid in the current page.

Robbi

unread,
Sep 5, 2022, 8:52:52 AM9/5/22
to Chromium Extensions, Jackie Han, Chromium Extensions, Robbi
A contented mind is a perpetual feast, Be happy with what you've got

Jackie Han

unread,
Sep 5, 2022, 11:06:12 AM9/5/22
to Robbi, Chromium Extensions

Jackie Han

unread,
Sep 5, 2022, 12:06:39 PM9/5/22
to Robbi, Chromium Extensions
Depending on your use case (if you just want to save data like file system, and don't access the user's real file system), Origin Private File System is better for extensions, because it does not require user permissions.

Hayat Kisa

unread,
Sep 5, 2022, 12:13:53 PM9/5/22
to PhistucK, Robbi, Chromium Extensions

4 Eyl 2022 Paz 02:04 tarihinde PhistucK <phis...@gmail.com> şunu yazdı:

Robbi

unread,
Sep 5, 2022, 2:00:55 PM9/5/22
to Chromium Extensions, kara...@gmail.com, Robbi, Chromium Extensions, PhistucK
In fact I wanted to implement a system that allows the user to upload some files to a protected and privileged file system. But I'd like the same user to be able to delete and\or add one or more files at any time.
Do I have any hope of making such a system or do I have to compromise? The alternative could be an indexDB

Jackie Han

unread,
Sep 5, 2022, 2:39:25 PM9/5/22
to Robbi, Chromium Extensions
I have replied in my last post. You can use the Origin Private File System to implement it.
Because both OPFS and indexedDB are not a real file system (users can't see them in file explorer), you need to supply a UI for users to manage(add/delete) files.

To use the origin private file system, you just use `navigator.storage.getDirectory()` instead of `window.showDirectoryPicker()` without `queryPermission()` and `requestPermission()`.

Robbi

unread,
Sep 5, 2022, 2:52:14 PM9/5/22
to Chromium Extensions, Jackie Han, Chromium Extensions, Robbi
Yes I read your previous post. I give it a try to this "origin private file system".
What I meant (perhaps between the lines) is that I would not want to get lost in a dead end. How many bug-proof APIs do you know?

Jackie Han

unread,
Sep 5, 2022, 11:23:04 PM9/5/22
to Robbi, Chromium Extensions
I don't know the bugs related to this API. But you can search it on https://bugs.chromium.org/p/chromium/issues/list , other people may report.

Reply all
Reply to author
Forward
0 new messages