/* * Copyright (C) 2017 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ WI.FileUtilities = class FileUtilities { static screenshotString() { let date = new Date; let values = [ date.getFullYear(), Number.zeroPad(date.getMonth() + 1, 2), Number.zeroPad(date.getDate(), 2), Number.zeroPad(date.getHours(), 2), Number.zeroPad(date.getMinutes(), 2), Number.zeroPad(date.getSeconds(), 2), ]; return WI.UIString("Screen Shot %s-%s-%s at %s.%s.%s").format(...values); } static sanitizeFilename(filename) { return filename.replace(/:+/g, "-"); } static inspectorURLForFilename(filename) { return "web-inspector:///" + encodeURIComponent(FileUtilities.sanitizeFilename(filename)); } static canSave(saveMode) { console.assert(Object.values(WI.FileUtilities.SaveMode).includes(saveMode), saveMode); return InspectorFrontendHost.canSave(saveMode); } static async save(saveMode, fileVariants, forceSaveAs) { console.assert(WI.FileUtilities.canSave(saveMode), saveMode); console.assert(fileVariants); if (!fileVariants) { InspectorFrontendHost.beep(); return; } let isFileVariantsMode = saveMode === WI.FileUtilities.SaveMode.FileVariants; if (isFileVariantsMode) forceSaveAs = true; if (typeof fileVariants.customSaveHandler === "function") { fileVariants.customSaveHandler(forceSaveAs); return; } if (!isFileVariantsMode && !Array.isArray(fileVariants)) fileVariants = [fileVariants]; console.assert(Array.isArray(fileVariants), fileVariants); if (!Array.isArray(fileVariants)) { InspectorFrontendHost.beep(); return; } let promises = fileVariants.map((fileVariant) => { let content = fileVariant.content; console.assert(content, fileVariant); if (!content) return null; let displayType = fileVariant.displayType || ""; console.assert(!isFileVariantsMode || fileVariant.displayType, fileVariant); if (!fileVariant.displayType && isFileVariantsMode) return null; let suggestedName = fileVariant.suggestedName; if (!suggestedName) { let url = fileVariant.url || ""; suggestedName = parseURL(url).lastPathComponent; if (!suggestedName) { suggestedName = WI.UIString("Untitled"); let dataURLTypeMatch = /^data:([^;]+)/.exec(url); if (dataURLTypeMatch) { let fileExtension = WI.fileExtensionForMIMEType(dataURLTypeMatch[1]); if (fileExtension) suggestedName += "." + fileExtension; } } } let url = WI.FileUtilities.inspectorURLForFilename(suggestedName); if (typeof content === "string") { return Promise.resolve({ displayType, url, content, base64Encoded: !!fileVariant.base64Encoded, }); } let wrappedPromise = new WI.WrappedPromise; let fileReader = new FileReader; fileReader.addEventListener("loadend", () => { wrappedPromise.resolve({ displayType, url, content: parseDataURL(fileReader.result).data, base64Encoded: true, }); }); fileReader.readAsDataURL(content); return wrappedPromise.promise; }); if (promises.includes(null)) { InspectorFrontendHost.beep(); return; } let saveDatas = await Promise.all(promises); console.assert(isFileVariantsMode || saveDatas.length === 1, saveDatas); console.assert(!isFileVariantsMode || new Set(saveDatas.map((saveData) => saveData.displayType)).size === saveDatas.length, saveDatas); console.assert(!isFileVariantsMode || new Set(saveDatas.map((saveData) => WI.urlWithoutExtension(saveData.url))).size === 1, saveDatas); InspectorFrontendHost.save(saveDatas, !!forceSaveAs); } static import(callback, {multiple} = {}) { let inputElement = document.createElement("input"); inputElement.type = "file"; inputElement.value = null; inputElement.multiple = !!multiple; inputElement.addEventListener("change", (event) => { callback(inputElement.files); }); inputElement.click(); // Cache the last used import element so that it doesn't get GCd while the native file // picker is shown, which would prevent the "change" event listener from firing. FileUtilities.importInputElement = inputElement; } static importText(callback, options = {}) { FileUtilities.import((files) => { FileUtilities.readText(files, callback); }, options); } static importJSON(callback, options = {}) { FileUtilities.import((files) => { FileUtilities.readJSON(files, callback); }, options); } static importData(callback, options = {}) { FileUtilities.import((files) => { FileUtilities.readData(files, callback); }, options); } static async readText(fileOrList, callback) { await FileUtilities._read(fileOrList, async (file, result) => { await new Promise((resolve, reject) => { let reader = new FileReader; reader.addEventListener("loadend", (event) => { result.text = reader.result; resolve(event); }); reader.addEventListener("error", reject); reader.readAsText(file); }); }, callback); } static async readJSON(fileOrList, callback) { await WI.FileUtilities.readText(fileOrList, async (result) => { if (result.text && !result.error) { try { result.json = JSON.parse(result.text); } catch (e) { result.error = e; } } await callback(result); }); } static async readData(fileOrList, callback) { await FileUtilities._read(fileOrList, async (file, result) => { await new Promise((resolve, reject) => { let reader = new FileReader; reader.addEventListener("loadend", (event) => { let {mimeType, base64, data} = parseDataURL(reader.result); // In case no mime type was determined, try to derive one from the file extension. if (!mimeType || mimeType === "text/plain") { let extension = WI.fileExtensionForFilename(result.filename); if (extension) mimeType = WI.mimeTypeForFileExtension(extension); } result.mimeType = mimeType; result.base64Encoded = base64; result.content = data; resolve(event); }); reader.addEventListener("error", reject); reader.readAsDataURL(file); }); }, callback); } // Private static async _read(fileOrList, operation, callback) { console.assert(fileOrList instanceof File || fileOrList instanceof FileList); let files = []; if (fileOrList instanceof File) files.push(fileOrList); else if (fileOrList instanceof FileList) files = Array.from(fileOrList); for (let file of files) { let result = { filename: file.name, }; try { await operation(file, result); } catch (e) { result.error = e; } await callback(result); } } }; // Keep in sync with `InspectorFrontendClient::SaveMode` and `InspectorFrontendHost::SaveMode`. WI.FileUtilities.SaveMode = { SingleFile: "single-file", FileVariants: "file-variants", };