/** * ## 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
star
photo_camera
Sun Feb 06 2022 18:59:43 GMT+0000 (Coordinated Universal Time) https://github.com/daemondevin/cdn/blob/main/FileBin.js
#javascript #file #choosefilesystementries #showopenfilepicker #filereader #blob