FileBin

PHOTO EMBED

Sun Feb 06 2022 18:59:43 GMT+0000 (Coordinated Universal Time)

Saved by @daemondevin #javascript #file #choosefilesystementries #showopenfilepicker #filereader #blob

/**
 * ## FileBin
 * A little library for dealing with file objects
 *
 * Best to use the methods directoryOpen(), fileOpen(), and fileSave()
 * as these methods ensure the use of the File System Access API is used
 * either the latest version or the legacy one.
 *
 * If the File System Access API isn't supported by the client's browser
 * it will then fallback to using the old-school hacks for opening and
 * saving files and directories.
 */
const handler = new function FileBin () {
    let self = this;

    /**
     * Returns whether the File System Access API is supported and usable in the
     * current context (for example cross-origin iframes).
     * @returns {boolean} Returns `true` if the File System Access API is supported and usable, else returns `false`.
     */
    const supported = (() => {
        if ('top' in self && self !== top) {
            try {
                // This will succeed on same-origin iframes,
                // but fail on cross-origin iframes.
                top.location + '';
            } catch {
                return false;
            }
        } else if ('chooseFileSystemEntries' in self) {
            return 'chooseFileSystemEntries';
        } else if ('showOpenFilePicker' in self) {
            return 'showOpenFilePicker';
        }
        return false;
    })();

    FileBin.prototype.getFiles = async (dirHandle, recursive, path = dirHandle.name) => {
        const dirs = [];
        const files = [];
        for await (const entry of dirHandle.values()) {
            const nestedPath = `${path}/${entry.name}`;
            if (entry.kind === 'file') {
                files.push(
                    entry.getFile().then((file) => {
                        file.directoryHandle = dirHandle;
                        return Object.defineProperty(file, 'webkitRelativePath', {
                            configurable: true,
                            enumerable: true,
                            get: () => nestedPath,
                        });
                    })
                );
            } else if (entry.kind === 'directory' && recursive) {
                dirs.push(self.getFiles(entry, recursive, nestedPath));
            }
        }
        return [...(await Promise.all(dirs)).flat(), ...(await Promise.all(files))];
    };

    FileBin.prototype.getFilesLegacy = async (dirHandle, recursive, path = dirHandle.name) => {
        const dirs = [];
        const files = [];
        for await (const entry of dirHandle.getEntries()) {
            const nestedPath = `${path}/${entry.name}`;
            if (entry.isFile) {
                files.push(
                    entry.getFile().then((file) => {
                        file.directoryHandle = dirHandle;
                        return Object.defineProperty(file, 'webkitRelativePath', {
                            configurable: true,
                            enumerable: true,
                            get: () => nestedPath,
                        });
                    })
                );
            } else if (entry.isDirectory && recursive) {
                dirs.push(self.getFilesLegacy(entry, recursive, nestedPath));
            }
        }
        return [...(await Promise.all(dirs)).flat(), ...(await Promise.all(files))];
    };

    /**
     * Opens a directory from disk using the (legacy) File System Access API.
     */
    FileBin.prototype.openDirLegacy = async (options = {}) => {
        options.recursive = options.recursive || false;
        const handle = await window.chooseFileSystemEntries({
            type: 'open-directory',
        });
        return self.getFilesLegacy(handle, options.recursive);
    };

    /**
     * Opens a directory from disk using the File System Access API.
     */
    FileBin.prototype.openDir = async (options = {}) => {
        options.recursive = options.recursive || false;
        const handle = await window.showDirectoryPicker();
        return self.getFiles(handle, options.recursive);
    };

    FileBin.prototype.getFileWithHandle = async (handle) => {
        const file = await handle.getFile();
        file.handle = handle;
        return file;
    };

    /**
     * Opens a file from disk using the (legacy) File System Access API.
     */
    FileBin.prototype.openFileLegacy = async (options = {}) => {
        const handleOrHandles = await window.chooseFileSystemEntries({
            accepts: [
                {
                    description: options.description || '',
                    mimeTypes: options.mimeTypes || ['*/*'],
                    extensions: options.extensions || [''],
                },
            ],
            multiple: options.multiple || false,
        });
        if (options.multiple) {
            return Promise.all(handleOrHandles.map(self.getFileWithHandle));
        }
        return self.getFileWithHandle(handleOrHandles);
    };

    /**
     * Opens a file from disk using the File System Access API.
     */
    FileBin.prototype.openFile = async (options = {}) => {
        const accept = {};
        if (options.mimeTypes) {
            options.mimeTypes.map((mimeType) => {
                accept[mimeType] = options.extensions || [];
            });
        } else {
            accept['*/*'] = options.extensions || [];
        }
        const handleOrHandles = await window.showOpenFilePicker({
            types: [
                {
                    description: options.description || '',
                    accept: accept,
                },
            ],
            multiple: options.multiple || false,
        });
        const files = await Promise.all(handleOrHandles.map(self.getFileWithHandle));
        if (options.multiple) {
            return files;
        }
        return files[0];
    };

    /**
     * Saves a file to disk using the (legacy) File System Access API.
     */
    FileBin.prototype.saveFileLegacy = async (blob, options = {}, handle = null) => {
        options.fileName = options.fileName || 'Untitled';
        handle =
            handle ||
            (await window.chooseFileSystemEntries({
                type: 'save-file',
                accepts: [
                    {
                        description: options.description || '',
                        mimeTypes: [blob.type],
                        extensions: options.extensions || [''],
                    },
                ],
            }));
        const writable = await handle.createWritable();
        await writable.write(blob);
        await writable.close();
        return handle;
    };

    /**
     * Saves a file to disk using the File System Access API.
     */
    FileBin.prototype.saveFile =  async (
        blob,
        options = {},
        existingHandle = null,
        throwIfExistingHandleNotGood = false
    ) => {
        options.fileName = options.fileName || 'Untitled';
        const accept = {};
        if (options.mimeTypes) {
            options.mimeTypes.push(blob.type);
            options.mimeTypes.map((mimeType) => {
                accept[mimeType] = options.extensions || [];
            });
        } else {
            accept[blob.type] = options.extensions || [];
        }
        if (existingHandle) {
            try {
                // Check if the file still exists.
                await existingHandle.getFile();
            } catch (err) {
                existingHandle = null;
                if (throwIfExistingHandleNotGood) {
                    throw err;
                }
            }
        }
        const handle =
            existingHandle ||
            (await window.showSaveFilePicker({
                suggestedName: options.fileName,
                types: [
                    {
                        description: options.description || '',
                        accept: accept,
                    },
                ],
            }));
        const writable = await handle.createWritable();
        await writable.write(blob);
        await writable.close();
        return handle;
    };

    /**
     * Saves a file to disk using the legacy hidden anchor method.
     */
    FileBin.prototype.legacySave = async (blob, options = {}) => {
        const a = document.createElement('a');
        a.download = options.fileName || 'Untitled';
        a.href = URL.createObjectURL(blob);
        a.addEventListener('click', () => {
            // `setTimeout()` due to
            // https://github.com/LLK/scratch-gui/issues/1783#issuecomment-426286393
            setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
        });
        a.click();
    };

    /**
     * Opens a file from disk using the legacy input click method.
     */
    FileBin.prototype.legacyOpen = async (options = {}) => {
        return new Promise((resolve, reject) => {
            const input = document.createElement('input');
            input.type = 'file';
            const accept = [
                ...(options.mimeTypes ? options.mimeTypes : []),
                options.extensions ? options.extensions : [],
            ].join();
            input.multiple = options.multiple || false;
            // Empty string allows everything.
            input.accept = accept || '';

            let cleanupListenersAndMaybeReject;
            const rejectionHandler = () => cleanupListenersAndMaybeReject(reject);
            if (options.setupLegacyCleanupAndRejection) {
                cleanupListenersAndMaybeReject =
                    options.setupLegacyCleanupAndRejection(rejectionHandler);
            } else {
                // Default rejection behavior; works in most cases, but not in Chrome in non-secure contexts.
                cleanupListenersAndMaybeReject = (reject) => {
                    window.removeEventListener('pointermove', rejectionHandler);
                    window.removeEventListener('pointerdown', rejectionHandler);
                    window.removeEventListener('keydown', rejectionHandler);
                    if (reject) {
                        reject(new DOMException('The user aborted a request.', 'AbortError'));
                    }
                };

                window.addEventListener('pointermove', rejectionHandler);
                window.addEventListener('pointerdown', rejectionHandler);
                window.addEventListener('keydown', rejectionHandler);
            }

            input.addEventListener('change', () => {
                cleanupListenersAndMaybeReject();
                resolve(input.multiple ? Array.from(input.files) : input.files[0]);
            });

            input.click();
        });
    };

    /**
     * Reads the text content from a file. If reader.text()
     * is not available then uses FileReader as a fallback.
     */
    FileBin.prototype.readFile = (file) => {
        if (file.text) {
            return file.text();
        }
        return self.readFileLegacy(file);
    }

    /**
     * Reads the text content from a file.
     */
    FileBin.prototype.readFileLegacy = (file) => {
        return new Promise((resolve) => {
            const reader = new FileReader();
            reader.addEventListener('loadend', (e) => {
                const text = e.target.result;
                resolve(text);
            });
            reader.readAsText(file);
        });
    }

    /**
     * Opens a directory from disk using the legacy input click method.
     */
    FileBin.prototype.legacyOpenDir = async (options = {}) => {
        options.recursive = options.recursive || false;
        return new Promise((resolve, reject) => {
            const input = document.createElement('input');
            input.type = 'file';
            input.webkitdirectory = true;

            let cleanupListenersAndMaybeReject;
            const rejectionHandler = () => cleanupListenersAndMaybeReject(reject);
            if (options.setupLegacyCleanupAndRejection) {
                cleanupListenersAndMaybeReject =
                    options.setupLegacyCleanupAndRejection(rejectionHandler);
            } else {
                // Default rejection behavior; works in most cases, but not in Chrome in non-secure contexts.
                cleanupListenersAndMaybeReject = (reject) => {
                    window.removeEventListener('pointermove', rejectionHandler);
                    window.removeEventListener('pointerdown', rejectionHandler);
                    window.removeEventListener('keydown', rejectionHandler);
                    if (reject) {
                        reject(new DOMException('The user aborted a request.', 'AbortError'));
                    }
                };

                window.addEventListener('pointermove', rejectionHandler);
                window.addEventListener('pointerdown', rejectionHandler);
                window.addEventListener('keydown', rejectionHandler);
            }

            input.addEventListener('change', () => {
                cleanupListenersAndMaybeReject();
                let files = Array.from(input.files);
                if (!options.recursive) {
                    files = files.filter((file) => {
                        return file.webkitRelativePath.split('/').length === 2;
                    });
                }
                resolve(files);
            });

            input.click();
        });
    };


    FileBin.prototype.fileRead = async (file) => {
        return await self.readFile(file);
    }

    const openDirMethod = !supported ? self.legacyOpenDir
        : supported === 'chooseFileSystemEntries'
            ? self.openDirLegacy
            : self.openDir;

    FileBin.prototype.directoryOpen = async (...args) => {
        return await openDirMethod(...args);
    }

    const openFileMethod = !supported
        ? self.legacyOpen
        : supported === 'chooseFileSystemEntries'
            ? self.openFileLegacy
            : self.openFile;

    FileBin.prototype.fileOpen = async (...args) => {
        return await openFileMethod(...args);
    }

    const saveFileMethod = !supported
        ? self.legacySave
        : supported === 'chooseFileSystemEntries'
            ? self.saveFileLegacy
            : self.saveFile;

    FileBin.prototype.fileSave = async (...args) => {
        return await saveFileMethod(...args);
    }

}();
 Save
content_copyCOPY

https://github.com/daemondevin/cdn/blob/main/FileBin.js