UNPKG

cm-spyglass

Version:

A Codemirror extension that provides syntax highlighting, linting, and autocompletion for Minecraft datapacks using SpyglassMC

259 lines (230 loc) 7.3 kB
import {DummyFsWatcher} from "../../index.js"; /** * A file system based on the new File System Access API. * * @implements {import("@spyglassmc/core").ExternalFileSystem} */ export default class FSAFileSystem { /** @type {boolean|null} */ static supported = null; /** @type {FileSystemDirectoryHandle} */ root; /** * @return {Promise<boolean>} */ static async isSupported() { if (this.supported === null) { this.supported = await this.checkBrowserSupport(); } return this.supported; } /** * @return {Promise<boolean>} */ static async checkBrowserSupport() { if (typeof navigator.storage?.getDirectory !== 'function') { return false; } let dir; try { dir = await navigator.storage.getDirectory(); } catch (e) { return false; } // noinspection JSUnresolvedVariable,JSUnresolvedFunction if(typeof dir.queryPermission === 'function' && await dir.queryPermission({mode: 'readwrite'}) === 'granted') { return true; } // Some browsers support file system access, but not the method to check for permission. // In this case we try to create a file and check if it was created. let match = false; try { let name = 'test-' + Random.getString(8) + '.txt'; let file = await dir.getFileHandle(name, {create: true}); match = file.name === name && file.kind === 'file'; await dir.removeEntry(name); } catch (e) { return false; } return match; } /** * @param {string} identifier * @return {Promise<FSAFileSystem>} */ static async create(identifier) { let tempDir = await navigator.storage.getDirectory(); let rootName = `spyglass_${identifier.replace(/\//g, '_')}`; let root = await tempDir.getDirectoryHandle(rootName, {create: true}); return new this(root); } /** * @param {FileSystemDirectoryHandle} root */ constructor(root) { this.root = root; } /** * @param location * @return {string[]} */ splitPath(location) { let prefix = 'file://'; let str = location.toString(); if (!str.startsWith(prefix)) { throw new Error(`EACCES: ${str}`); } str = str.substring(prefix.length); return str.split('/').filter(part => part.length); } /** * @param location * @param resolveParent * @return {Promise<FileSystemDirectoryHandle|FileSystemFileHandle>} */ async resolve(location, resolveParent = false) { let parts = this.splitPath(location); let baseName = parts.pop(); let current = this.root; for (let part of parts) { if (!(current instanceof FileSystemDirectoryHandle)) { throw new Error(`ENOTDIR: ${location}`); } try { current = await current.getDirectoryHandle(part); } catch (e) { throw new Error(`ENOENT: ${location}: ${e.message}`); } } if (!resolveParent) { for await (let entry of current.values()) { if (entry.name === baseName) { return entry; } } throw new Error(`ENOENT: ${location}`); } return current; } /** * @inheritDoc */ async chmod(_location, _mode) { } /** * @inheritDoc */ async mkdir(location, options = {}) { let parts = this.splitPath(location); if (options.recursive || true) { let current = this.root; for (let part of parts) { if (!(current instanceof FileSystemDirectoryHandle)) { throw new Error(`ENOTDIR: ${location}`); } try { current = await current.getDirectoryHandle(part, {create: true}); } catch (e) { throw new Error(`EEXIST: ${location}`); } } return; } let parent = await this.resolve(location, true); let name = parts[parts.length - 1]; try { await parent.getDirectoryHandle(name, {create: true}); } catch (e) { throw new Error(`EEXIST: ${location}`); } } /** * @inheritDoc */ async readdir(location) { let dir = await this.resolve(location); if (!(dir instanceof FileSystemDirectoryHandle)) { throw new Error(`ENOTDIR: ${location}`); } let result = []; for await (/** @type {FileSystemDirectoryHandle|FileSystemFileHandle} */ let entry of dir.values()) { /** @type {FileSystemDirectoryHandle|FileSystemFileHandle} */ result.push({ name: entry.name, isDirectory: () => entry.kind === 'directory', isFile: () => entry.kind === 'file', isSymbolicLink: () => false }); } return result; } /** * @inheritDoc */ async readFile(location) { let file = await this.resolve(location); if (!(file instanceof FileSystemFileHandle)) { throw new Error(`EISDIR: ${location}`); } let blob = await file.getFile(); return await new Promise((resolve, reject) => { let reader = new FileReader(); reader.onload = () => resolve(new Uint8Array(reader.result)); reader.onerror = () => reject(new Error(`EPERM: ${location}: ${reader.error}`)); reader.readAsArrayBuffer(blob); }); } /** * @inheritDoc */ async showFile(_path) { throw new Error('showFile not supported on browser'); } /** * @inheritDoc */ async stat(location) { let entry = await this.resolve(location); return { isDirectory: () => entry.kind === 'directory', isFile: () => entry.kind === 'file' }; } /** * @inheritDoc */ async unlink(location) { let parent = await this.resolve(location, true); let name = this.splitPath(location).pop(); try { await parent.removeEntry(name); } catch (e) { throw new Error(`ENOENT: ${location}`); } } /** * @inheritDoc */ watch(_locations) { return new DummyFsWatcher(); } /** * @inheritDoc */ async writeFile(location, data, _options) { let parent = await this.resolve(location, true); let name = this.splitPath(location).pop(); let file; try { file = await parent.getFileHandle(name, {create: true}); } catch (e) { throw new Error(`EEXIST: ${location}: ${e.message}`); } let writable; try { writable = await file.createWritable(); } catch (e) { throw new Error(`EPERM: ${location}: ${e.message}`); } await writable.write(data); await writable.close(); } }