Files
leporello-js/src/filesystem.js
2023-07-14 03:32:32 +03:00

175 lines
4.9 KiB
JavaScript

// code is borrowed from
// https://googlechrome.github.io/samples/service-worker/post-message/
const send_message = (message) => {
return new Promise(function(resolve) {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function(event) {
resolve(event.data)
};
if(navigator.serviceWorker.controller == null) {
// Service worker will be available after reload
window.location.reload()
}
navigator.serviceWorker.controller.postMessage(message,
[messageChannel.port2]);
});
}
export const close_dir = () => {
send_message({type: 'SET_DIR_HANDLE', data: null})
clearInterval(keepalive_interval_id)
keepalive_interval_id = null
}
let dir_handle
let keepalive_interval_id
/*
Service worker is killed by the browser after 40 seconds of inactivity see
https://github.com/mswjs/msw/issues/367
There is hard 5 minute limit on service worker lifetime See
https://chromium.googlesource.com/chromium/src/+/master/docs/security/service-worker-security-faq.md#do-service-workers-live-forever
Keep reviving serivce worker, so when user reloads page, dir_handle is picked
up from service worker
*/
const keep_service_worker_alive = () => {
if(keepalive_interval_id != null) {
return
}
keepalive_interval_id = setInterval(() => {
send_message({type: 'SET_DIR_HANDLE', data: dir_handle})
}, 10_000)
}
const request_directory_handle = async () => {
dir_handle = await globalThis.showDirectoryPicker()
await send_message({type: 'SET_DIR_HANDLE', data: dir_handle})
return dir_handle
}
export const init_window_service_worker = window => {
window.navigator.serviceWorker.ready.then(() => {
window.navigator.serviceWorker.addEventListener('message', e => {
if(e.data.type == 'GET_DIR_HANDLE') {
e.ports[0].postMessage(dir_handle)
}
})
})
}
const load_persisted_directory_handle = () => {
return navigator.serviceWorker.register('service_worker.js')
.then(() => navigator.serviceWorker.ready)
/*
Main window also provides dir_handle to service worker, together with
app_window. app_window provides dir_handle to service worker when it
issues fetch event. If clientId is '' then service worker will try to get
dir_handle from main window
*/
.then(() => init_window_service_worker(globalThis))
.then(() => send_message({type: 'GET_DIR_HANDLE'}))
.then(async h => {
if(h == null || (await h.queryPermission()) != 'granted') {
return null
}
// test if directory handle is valid
try {
await h.entries().next()
} catch(e) {
return null
}
dir_handle = h
return dir_handle
})
}
const file_handle = async (dir_handle, filename, is_directory = false, options) => {
if(typeof(filename) == 'string') {
filename = filename.split('/')
}
const [first, ...rest] = filename
if(rest.length == 0) {
return is_directory
? await dir_handle.getDirectoryHandle(first, options)
: await dir_handle.getFileHandle(first, options)
} else {
const nested_dir_handle = await dir_handle.getDirectoryHandle(first)
return file_handle(nested_dir_handle, rest, is_directory, options)
}
}
export const write_file = async (name, contents) => {
const f_hanlde = await file_handle(dir_handle, name)
// Create a FileSystemWritableFileStream to write to.
const writable = await f_hanlde.createWritable()
// Write the contents of the file to the stream.
await writable.write(contents)
// Close the file and write the contents to disk.
await writable.close()
}
// Blacklist hidden dirs and node_modules
const is_blacklisted = h => h.name == 'node_modules' || h.name.startsWith('.')
const read_file = async handle => {
const file_data = await handle.getFile()
return await file_data.text()
}
const do_open_dir = async (handle, path) => {
if(handle.kind == 'directory') {
const children = []
for await (let [name, h] of handle) {
if(!is_blacklisted(h)) {
children.push(h)
}
}
return {
name: handle.name,
path,
kind: 'directory',
children: (await Promise.all(
children.map(c =>
do_open_dir(c, path == null ? c.name : path + '/' + c.name)
)
)).sort((a, b) => a.name.localeCompare(b.name))
}
} else if(handle.kind == 'file') {
return {
name: handle.name,
path,
kind: 'file',
contents: await read_file(handle)
}
} else {
throw new Error('unknown kind')
}
}
export const create_file = (path, is_dir) => {
return file_handle(
dir_handle,
path,
is_dir,
{create: true}
)
}
export const open_dir = async (should_request_access) => {
let handle
if(should_request_access) {
handle = await request_directory_handle()
} else {
handle = await load_persisted_directory_handle()
}
if(handle == null) {
return null
} else {
keep_service_worker_alive()
}
return do_open_dir(handle, null)
}