diff --git a/package.json b/package.json index 69922a1f..311b53c9 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dependencies": { "@msgpack/msgpack": "^3.0.0-beta2", "@obsidize/tar-browserify": "4.0.0", + "axios": "^1.6.7", "bootstrap": "^5.2.3", "browserify-fs": "^1.0.0", "buffer": "^6.0.3", diff --git a/src/App.js b/src/App.js index 91b2ed56..01db5ff1 100644 --- a/src/App.js +++ b/src/App.js @@ -25,7 +25,7 @@ export function App () { }; const [appMode, setAppMode] = useState(null); - const [fileInfo, setFileInfo] = useState(null); + const [fileSrc, setFileSrc] = useState(null); const [logEventIdx, setLogEventIdx] = useState(null); const [timestamp, setTimestamp] = useState(null); const [prettify, setPrettify] = useState(null); @@ -65,11 +65,11 @@ export function App () { const filePath = getFilePathFromWindowLocation(); if (null !== filePath) { - setFileInfo(filePath); + setFileSrc(filePath); setAppMode(APP_STATE.FILE_VIEW); } else { if (null !== config.defaultFileUrl) { - setFileInfo(config.defaultFileUrl); + setFileSrc(config.defaultFileUrl); setAppMode(APP_STATE.FILE_VIEW); } else { setAppMode(APP_STATE.FILE_PROMPT); @@ -82,7 +82,7 @@ export function App () { * @param {File} file */ const handleFileChange = (file) => { - setFileInfo(file); + setFileSrc(file); setAppMode(APP_STATE.FILE_VIEW); }; @@ -94,7 +94,7 @@ export function App () { + fileSrc={fileSrc}/> } diff --git a/src/DropFile/README.md b/src/DropFile/README.md index 2abe7cfa..6b99027e 100644 --- a/src/DropFile/README.md +++ b/src/DropFile/README.md @@ -19,16 +19,16 @@ import {DropFile} from "./DropFile/DropFile"; import "bootstrap/dist/css/bootstrap.min.css"; const App = () => { - const [fileInfo, setFileInfo] = useState(null); + const [fileSrc, setFileSrc] = useState(null); const handleFileChange = (file) => { - setFileInfo(file); + setFileSrc(file); }; return (
- +
); diff --git a/src/ThemeContext/README.md b/src/ThemeContext/README.md index 594556a9..60aba312 100644 --- a/src/ThemeContext/README.md +++ b/src/ThemeContext/README.md @@ -23,7 +23,7 @@ const App = () => { FILE_VIEW: 1, }; - const [fileInfo, setFileInfo] = useState(null); + const [fileSrc, setfileSrc] = useState(null); const [theme, setTheme] = useState(THEME_STATES.DARK); const [appMode, setAppMode] = useState(); @@ -34,7 +34,7 @@ const App = () => { }; const handleFileChange = (file) => { - setFileInfo(file); + setfileSrc(file); setAppMode(APP_STATE.FILE_VIEW); }; @@ -52,7 +52,7 @@ const App = () => { {appMode === APP_STATE.VIEWER && + fileSrc={fileSrc}/> } diff --git a/src/Viewer/Viewer.js b/src/Viewer/Viewer.js index 3f19d553..d29f4b83 100644 --- a/src/Viewer/Viewer.js +++ b/src/Viewer/Viewer.js @@ -21,7 +21,7 @@ import {getModifiedUrl, isNumeric, modifyPage} from "./services/utils"; import "./Viewer.scss"; Viewer.propTypes = { - fileInfo: oneOfType([PropTypes.object, PropTypes.string]), + fileSrc: oneOfType([PropTypes.object, PropTypes.string]), filePath: PropTypes.string, prettifyLog: PropTypes.bool, logEventNumber: PropTypes.string, @@ -31,14 +31,14 @@ Viewer.propTypes = { /** * Contains the menu, Monaco editor, and status bar. Viewer spawns its own * worker to manage the file and perform CLP operations. - * @param {File|String} fileInfo File object to read or file path to load + * @param {File|String} fileSrc File object to read or file path to load * @param {boolean} prettifyLog Whether to prettify the log file * @param {Number} logEventNumber The initial log event number * @param {Number} timestamp The initial timestamp to show. If this field is * valid, logEventNumber will be ignored. * @return {JSX.Element} */ -export function Viewer ({fileInfo, prettifyLog, logEventNumber, timestamp}) { +export function Viewer ({fileSrc, prettifyLog, logEventNumber, timestamp}) { const {theme} = useContext(ThemeContext); // Ref hook used to reference worker used for loading and decoding @@ -68,7 +68,7 @@ export function Viewer ({fileInfo, prettifyLog, logEventNumber, timestamp}) { numberOfEvents: null, verbosity: null, }); - const [fileMetadata, setFileMetadata] = useState(null); + const [fileInfo, setFileInfo] = useState(null); const [logData, setLogData] = useState(""); const [leftPanelActiveTabId, setLeftPanelActiveTabId] = useState(LEFT_PANEL_TAB_IDS.SEARCH); @@ -89,10 +89,10 @@ export function Viewer ({fileInfo, prettifyLog, logEventNumber, timestamp}) { }, []); /** - * Reload viewer on fileInfo change - * @param {File|string} fileInfo + * Reload viewer on fileSrc change + * @param {File|string} fileSrc */ - const loadFile = (fileInfo) => { + const loadFile = (fileSrc) => { if (clpWorker.current) { clpWorker.current.terminate(); } @@ -104,11 +104,11 @@ export function Viewer ({fileInfo, prettifyLog, logEventNumber, timestamp}) { // Create new worker and pass args to worker to load file clpWorker.current = new Worker(new URL("./services/clpWorker.js", import.meta.url)); // If file was loaded using file dialog or drag/drop, reset logEventIdx - const logEvent = (typeof fileInfo === "string") ? logFileState.logEventIdx : null; + const logEvent = (typeof fileSrc === "string") ? logFileState.logEventIdx : null; const initialTimestamp = isNumeric(timestamp) ? Number(timestamp) : null; clpWorker.current.postMessage({ code: CLP_WORKER_PROTOCOL.LOAD_FILE, - fileInfo: fileInfo, + fileSrc: fileSrc, prettify: logFileState.prettify, logEventIdx: logEvent, initialTimestamp: initialTimestamp, @@ -118,8 +118,8 @@ export function Viewer ({fileInfo, prettifyLog, logEventNumber, timestamp}) { // Load file if file info changes (this could happen from drag and drop) useEffect(() => { - loadFile(fileInfo); - }, [fileInfo]); + loadFile(fileSrc); + }, [fileSrc]); // Save statusMessages to the msg logger for debugging useEffect(() => { @@ -269,7 +269,7 @@ export function Viewer ({fileInfo, prettifyLog, logEventNumber, timestamp}) { setStatusMessage(""); break; case CLP_WORKER_PROTOCOL.UPDATE_FILE_INFO: - setFileMetadata(event.data.fileState); + setFileInfo(event.data.fileInfo); break; case CLP_WORKER_PROTOCOL.UPDATE_SEARCH_RESULTS: setSearchResults((prevArray) => [...prevArray, { @@ -287,14 +287,14 @@ export function Viewer ({fileInfo, prettifyLog, logEventNumber, timestamp}) { }, [logFileState, logData, searchQuery, shouldReloadSearch]); useEffect(() => { - if (null !== fileMetadata) { - const searchParams = {filePath: fileMetadata.filePath}; + if (null !== fileInfo) { + const searchParams = {filePath: fileInfo.filePath}; const hashParams = {logEventIdx: logFileState.logEventIdx}; const newUrl = getModifiedUrl(searchParams, hashParams); window.history.pushState({}, null, newUrl); } - }, [fileMetadata]); + }, [fileInfo]); /** * Unsets the cached page size in case it causes a client OOM. If it @@ -380,7 +380,7 @@ export function Viewer ({fileInfo, prettifyLog, logEventNumber, timestamp}) { }}> diff --git a/src/Viewer/components/LeftPanel/LeftPanel.js b/src/Viewer/components/LeftPanel/LeftPanel.js index cc77563c..4f3d3685 100644 --- a/src/Viewer/components/LeftPanel/LeftPanel.js +++ b/src/Viewer/components/LeftPanel/LeftPanel.js @@ -8,10 +8,10 @@ import {THEME_STATES} from "../../../ThemeContext/THEME_STATES"; import {ThemeContext} from "../../../ThemeContext/ThemeContext"; import STATE_CHANGE_TYPE from "../../services/STATE_CHANGE_TYPE"; import {ResizeHandle} from "../ResizeHandle/ResizeHandle"; +import DOWNLOAD_WORKER_ACTION from "./DOWNLOAD_WORKER_ACTION"; +import {BlobAppender, downloadBlob, downloadCompressedFile} from "./DownloadHelper"; import "./LeftPanel.scss"; -import {BlobAppender, downloadBlob, downloadCompressedFile} from "./DownloadHelper"; -import DOWNLOAD_WORKER_ACTION from "./DOWNLOAD_WORKER_ACTION"; const LEFT_PANEL_WIDTH_LIMIT_FACTOR = 0.8; const LEFT_PANEL_SNAP_WIDTH = 108; @@ -19,6 +19,7 @@ const LEFT_PANEL_DEFAULT_WIDTH_FACTOR = 0.2; LeftPanel.propTypes = { logFileState: PropTypes.object, + fileInfo: PropTypes.object, panelWidth: PropTypes.number, setPanelWidth: PropTypes.func, activeTabId: PropTypes.number, @@ -57,7 +58,7 @@ LeftPanel.propTypes = { /** * The left panel component * @param {object} logFileState Current state of the log file - * @param {object} fileMetaData Object containing file metadata + * @param {object} fileInfo Object containing file metadata * @param {number} panelWidth * @param {SetPanelWidth} setPanelWidth * @param {number} activeTabId @@ -69,7 +70,7 @@ LeftPanel.propTypes = { */ export function LeftPanel ({ logFileState, - fileMetaData, + fileInfo, panelWidth, setPanelWidth, activeTabId, @@ -110,7 +111,7 @@ export function LeftPanel ({ <> 0 ? activeTabId : -1} togglePanel={togglePanel} loadFileCallback={loadFileCallback} @@ -131,7 +132,7 @@ export function LeftPanel ({ LeftPanelTabs.propTypes = { logFileState: PropTypes.object, - fileMetaData: PropTypes.object, + fileInfo: PropTypes.object, activeTabId: PropTypes.number, togglePanel: PropTypes.func, loadFileCallback: PropTypes.func, @@ -170,7 +171,7 @@ LeftPanelTabs.propTypes = { */ function LeftPanelTabs ({ logFileState, - fileMetaData, + fileInfo, activeTabId, togglePanel, loadFileCallback, @@ -254,7 +255,7 @@ function LeftPanelTabs ({ worker.postMessage({ code: DOWNLOAD_WORKER_ACTION.initialize, - name: fileMetaData.name, + name: fileInfo.name, count: logFileState.downloadPageChunks, }); @@ -278,7 +279,7 @@ function LeftPanelTabs ({ code: DOWNLOAD_WORKER_ACTION.clearDatabase, }); worker.terminate(); - downloadBlob(blob.getBlob(), fileMetaData.name); + downloadBlob(blob.getBlob(), fileInfo.name); } break; case DOWNLOAD_WORKER_ACTION.progress: @@ -363,10 +364,10 @@ function LeftPanelTabs ({ @@ -404,17 +405,17 @@ function LeftPanelTabs ({ } - + {!isDownloading && } {isDownloading && @@ -433,7 +434,7 @@ function LeftPanelTabs ({ + className="p-0 border-0 rounded-0"/> } diff --git a/src/Viewer/components/MenuBar/MenuBar.js b/src/Viewer/components/MenuBar/MenuBar.js index 999f9f41..1ac08a54 100644 --- a/src/Viewer/components/MenuBar/MenuBar.js +++ b/src/Viewer/components/MenuBar/MenuBar.js @@ -24,7 +24,7 @@ import "./MenuBar.scss"; MenuBar.propTypes = { logFileState: PropTypes.object, - fileMetaData: PropTypes.object, + fileInfo: PropTypes.object, loadingLogs: PropTypes.bool, changeStateCallback: PropTypes.func, loadFileCallback: PropTypes.func, @@ -40,14 +40,14 @@ MenuBar.propTypes = { /** * Menu bar used to navigate the log file. * @param {object} logFileState Current state of the log file - * @param {object} fileMetaData Object containing file metadata + * @param {object} fileInfo Object containing file name & path * @param {boolean} loadingLogs Indicates if logs are being decoded and * loaded by worker. * @param {ChangeStateCallback} changeStateCallback * @return {JSX.Element} */ export function MenuBar ({ - logFileState, fileMetaData, loadingLogs, changeStateCallback, loadFileCallback, + logFileState, fileInfo, loadingLogs, changeStateCallback, loadFileCallback, }) { const {theme, switchTheme} = useContext(ThemeContext); @@ -159,7 +159,7 @@ export function MenuBar ({ :
; }; - const fileName = fileMetaData.name.split("?")[0]; + const fileName = fileInfo.name.split("?")[0]; // TODO make file icon a button to open modal with file info // TODO Move modals into their own component diff --git a/src/Viewer/services/ActionHandler.js b/src/Viewer/services/ActionHandler.js index 7fa84089..d78da60d 100644 --- a/src/Viewer/services/ActionHandler.js +++ b/src/Viewer/services/ActionHandler.js @@ -15,17 +15,22 @@ import {isBoolean, isNumeric} from "./decoder/utils"; class ActionHandler { /** * Creates a new FileManager object and initiates the download. - * @param {String|File} fileInfo + * @param {String|File} fileSrc * @param {boolean} prettify * @param {Number} logEventIdx * @param {Number} initialTimestamp * @param {Number} pageSize */ - constructor (fileInfo, prettify, logEventIdx, initialTimestamp, pageSize) { - this._logFile = new FileManager(fileInfo, prettify, logEventIdx, initialTimestamp, pageSize, + constructor (fileSrc, prettify, logEventIdx, initialTimestamp, pageSize) { + this._logFile = new FileManager(fileSrc, prettify, logEventIdx, initialTimestamp, pageSize, this._loadingMessageCallback, this._updateStateCallback, this._updateLogsCallback, this._updateFileInfoCallback, this._updateSearchResultsCallback); - this._logFile.loadLogFile(); + this._logFile.loadLogFile().then(()=> { + console.log(fileSrc, "File loaded successfully"); + }).catch((e) => { + this._loadingMessageCallback(e, true); + console.error("Error processing log file:", e); + }); } /** @@ -218,10 +223,10 @@ class ActionHandler { * Send the file information. * @param {string} fileState */ - _updateFileInfoCallback = (fileState) => { + _updateFileInfoCallback = (fileInfo) => { postMessage({ code: CLP_WORKER_PROTOCOL.UPDATE_FILE_INFO, - fileState: fileState, + fileInfo: fileInfo, }); }; diff --git a/src/Viewer/services/GetFile.js b/src/Viewer/services/GetFile.js index 49575948..5ceced60 100644 --- a/src/Viewer/services/GetFile.js +++ b/src/Viewer/services/GetFile.js @@ -1,9 +1,22 @@ +import axios from "axios"; + /** - * Error class for HTTP requests + * Custom error class for representing HTTP request errors. + * + * @class HTTPRequestError + * @extends {Error} */ class HTTPRequestError extends Error { + /** + * Constructs and initializes instance of HTTPRequestError + * + * @param {string} url of the HTTP request that resulted in an error + * @param {number} status code of the response + * @param {string} statusText of the response + */ constructor (url, status, statusText) { super(`${url} returned ${status} ${statusText}`); + this.name = "HTTPRequestError"; this.url = url; this.status = status; @@ -18,130 +31,91 @@ class HTTPRequestError extends Error { * @param {number} numBytesDownloaded Bytes downloaded * @param {number} fileSizeBytes Size of file in Bytes */ - /** - * Creates a promise that downloads a file with the given URL or gets - * the data from input file. The given callback is called whenever - * the download makes progress. + * Downloads and reads a file with a given URL. * - * @param {string|object} fileInfo A File object or a file path to download + * @param {string} fileUrl * @param {ProgressCallback} progressCallback Callback to update progress - * @return {Promise} A promise that resolves with the file's content + * @return {Uint8Array} File content */ -function readFile (fileInfo, progressCallback) { - return new Promise(async (resolve, reject) => { - if (fileInfo instanceof File) { - readFileInputPromise(fileInfo, progressCallback).then((data) => { - resolve({ - name: fileInfo.name, - filePath: null, - data: data, - }); - }).catch((reason) => { - reject(reason); - }); - } else if (typeof fileInfo == "string") { - const name = fileInfo.split("/").pop(); - getFetchFilePromise(fileInfo, progressCallback).then((data) => { - resolve({ - name: name, - filePath: fileInfo, - data: data, - }); - }).catch((reason) => { - reject(reason); - }); - } else { - reject(new Error("Invalid file")); - } - }); -} +const downloadAndReadFile = async (fileUrl, progressCallback) => { + try { + const {data} = await axios.get(fileUrl, { + responseType: "arraybuffer", + onDownloadProgress: (progressEvent) => { + progressCallback(progressEvent.loaded, progressEvent.total); + }, + headers: { + "Cache-Control": "no-cache", + "Pragma": "no-cache", + "Expires": "0", + }, + }); + + return new Uint8Array(data); + } catch (e) { + throw new HTTPRequestError(fileUrl, e.response.status, e.response.data); + } +}; /** - * Creates a promise that downloads a file with the given URL. The given - * callback is called whenever the download makes progress. + * Reads a File object using FileReader. * - * @param {string} fileUrl + * @param {File} file File object to read data from. * @param {ProgressCallback} progressCallback Callback to update progress - * @return {Promise} A promise that resolves with the file's content + * @return {Uint8Array} File content */ -function getFetchFilePromise (fileUrl, progressCallback) { - return new Promise(async (resolve, reject) => { - fetch(fileUrl, {cache: "no-cache"}).then(async (response) => { - if (false === response.ok) { - throw new HTTPRequestError(fileUrl, response.status, response.statusText); - } - const reader = response.body.getReader(); - const totalBytes = +response.headers.get("Content-Length"); +const readFileObject = (file, progressCallback) => new Promise((resolve, reject) => { + const reader = new FileReader(); - let receivedBytes = 0; - const chunks = []; - while (true) { - const {done, value} = await reader.read(); - if (done) { - break; - } - chunks.push(value); - receivedBytes += value.length; - progressCallback(receivedBytes, totalBytes); - console.debug(`Received ${receivedBytes}B of ${totalBytes}B`); - } + reader.onload = (event) => { + progressCallback(file.size, file.size); + resolve(new Uint8Array(reader.result)); + }; + reader.onerror = () => { + reject(reader.error); + }; - const concatenatedChunks = new Uint8Array(receivedBytes); - let pos = 0; - for (const chunk of chunks) { - concatenatedChunks.set(chunk, pos); - pos += chunk.length; - } - resolve(concatenatedChunks); - }).catch((reason) => { - reject(reason); - }); - }); -} + reader.readAsArrayBuffer(file); +}); /** - * Get the size of a file. + * Input File Information * - * @param {string} url - * @param {function} updateSizeCallback + * @typedef {Object} FileInfo + * @property {string} name File name + * @property {string|null} [filePath] File URL when the file is downloaded + * @property {Uint8Array} data File content */ -function getFileSize (url, updateSizeCallback) { - fetch(url, {method: "HEAD"}) - .then(function (response) { - if (response.ok) { - updateSizeCallback(parseInt(response.headers.get("Content-Length"))); - return parseInt(response.headers.get("Content-Length")); - } else { - console.error(`Failed to get file size for ${url} -`, - `${response.status} ${response.statusText}`); - } - }) - .catch(function (reason) { - console.error(`Failed to get file size for ${url} - ${reason}`); - }); -} - /** - * Reads a file object using FileReader, resolves with the data from the file. + * Gets content from an input file. If given `fileSrc` is a string, treat it as + * a URL and download before getting data. * - * @param {File} file File object to read data from. + * @param {string|File} fileSrc A File object or a file URL to download * @param {ProgressCallback} progressCallback Callback to update progress - * @return {Promise} A promise that resolves with the file's content + * @return {FileInfo} Input File Information which contains the file content */ -function readFileInputPromise (file, progressCallback) { - return new Promise(async (resolve, reject) => { - const reader = new FileReader(); - reader.onload = (event) => { - progressCallback(file.size, file.size); - resolve(new Uint8Array(event.target.result)); +const readFile = async (fileSrc, progressCallback) => { + let fileInfo = null; + + if (fileSrc instanceof File) { + const data = await readFileObject(fileSrc, progressCallback); + fileInfo = { + name: fileSrc.name, + filePath: null, + data: data, }; - // TODO Revisit errors when trying to read the file. - reader.onerror = () => { - reject(reader.error); + } else if ("string" === typeof fileSrc) { + const name = fileSrc.split("/").pop(); + const data = await downloadAndReadFile(fileSrc, progressCallback); + fileInfo = { + name: name, + filePath: fileSrc, + data: data, }; - reader.readAsArrayBuffer(file); - }); -} + } + + return fileInfo; +}; export {readFile}; diff --git a/src/Viewer/services/clpWorker.js b/src/Viewer/services/clpWorker.js index 3cc84b08..9a9e2eb1 100644 --- a/src/Viewer/services/clpWorker.js +++ b/src/Viewer/services/clpWorker.js @@ -18,12 +18,12 @@ onmessage = function (e) { switch (e.data.code) { case CLP_WORKER_PROTOCOL.LOAD_FILE: try { - const fileInfo = e.data.fileInfo; + const fileSrc = e.data.fileSrc; const prettify = e.data.prettify; const logEventIdx = e.data.logEventIdx; const pageSize = e.data.pageSize; const initialTimestamp = e.data.initialTimestamp; - handler = new ActionHandler(fileInfo, prettify, logEventIdx, initialTimestamp, + handler = new ActionHandler(fileSrc, prettify, logEventIdx, initialTimestamp, pageSize); } catch (e) { sendError(e); diff --git a/src/Viewer/services/decoder/FileManager.js b/src/Viewer/services/decoder/FileManager.js index f86a942f..0943cd32 100644 --- a/src/Viewer/services/decoder/FileManager.js +++ b/src/Viewer/services/decoder/FileManager.js @@ -21,10 +21,21 @@ const FILE_MANAGER_LOG_SEARCH_CHUNK_SIZE = 10000; * File manager to manage and track state of each file that is loaded. */ class FileManager { + static #decompressZstd = async (data) => { + const zstd = await new Promise((resolve) => { + ZstdCodec.run((zstd) => { + resolve(zstd); + }); + }); + const zstdCtx = new zstd.Streaming(); + + return zstdCtx.decompress(data).buffer; + }; + /** * Initializes the class and sets the default states. * - * @param {string} fileInfo + * @param {string} fileSrc * @param {boolean} prettify * @param {number} logEventIdx * @param {number} initialTimestamp @@ -33,12 +44,13 @@ class FileManager { * @param {function} updateStateCallback * @param {function} updateLogsCallback * @param {function} updateFileInfoCallback + * @param {function} updateSearchResultsCallback */ - constructor (fileInfo, prettify, logEventIdx, initialTimestamp, pageSize, + constructor (fileSrc, prettify, logEventIdx, initialTimestamp, pageSize, loadingMessageCallback, updateStateCallback, updateLogsCallback, updateFileInfoCallback, updateSearchResultsCallback) { - this._fileInfo = fileInfo; + this._fileSrc = fileSrc; this._prettify = prettify; this._initialTimestamp = initialTimestamp; this._logEventOffsets = []; @@ -52,7 +64,7 @@ class FileManager { this._IRStreamHeader = null; this._timestampSorted = false; - this._fileState = { + this._fileInfo = { name: null, path: null, }; @@ -67,8 +79,8 @@ class FileManager { columnNumber: null, numberOfEvents: null, verbosity: null, - compressedSize: null, - decompressedSize: null, + compressedHumanSize: null, + decompressedHumanSize: null, downloadChunkSize: 10000, }; @@ -204,27 +216,26 @@ class FileManager { }; /** - * Append token to the end of fileState name + * Append token to the end of this._fileInfo.name * @param {string} token to append * @private */ - _appendToFileStateName (token) { - this._fileState.name += token; - this._updateFileInfoCallback(this._fileState); + _appendToFileInfoName (token) { + this._fileInfo.name += token; + this._updateFileInfoCallback(this._fileInfo); } /** * Update input file and status - * @param {map} file to use as input - * @return {map} file to use as input + * @param {object} file to use as input * @private */ - _updateInputFileAndStatus (file) { - this._fileState = file; - this._updateFileInfoCallback(this._fileState); + _updateInputFileInfo (file) { + this._fileInfo = file; + this._updateFileInfoCallback(this._fileInfo); - this.state.compressedSize = formatSizeInBytes(file.data.byteLength, false); - this._loadingMessageCallback(`Decompressing ${this.state.compressedSize}.`); + this.state.compressedHumanSize = formatSizeInBytes(file.data.byteLength, false); + this._loadingMessageCallback(`Decompressing ${this.state.compressedHumanSize}.`); return file; } @@ -237,19 +248,24 @@ class FileManager { */ _decodePlainTextLogAndUpdate (decompressedLogFile) { // Update decompression status - this.state.decompressedSize = formatSizeInBytes(decompressedLogFile.byteLength, false); - this._loadingMessageCallback(`Decompressed ${this.state.decompressedSize}.`); + this.state.decompressedHumanSize = formatSizeInBytes(decompressedLogFile.byteLength, false); + this._loadingMessageCallback(`Decompressed ${this.state.decompressedHumanSize}.`); - this._logs = this._textDecoder.decode(decompressedLogFile); - this._updateLogsCallback(this._logs); + this._logsArray = this._textDecoder.decode(decompressedLogFile).split("\n"); - // Update state to re-render a single page on the editor + // Update state this.state.verbosity = -1; this.state.lineNumber = 1; this.state.columnNumber = 1; - this.state.page = 1; - this.state.pages = 1; + this.state.numberOfEvents = this._logsArray.length; + this.computePageNumFromLogEventIdx(); + this.createPages(); this._updateStateCallback(CLP_WORKER_PROTOCOL.UPDATE_STATE, this.state); + + this.decodePage(); + + // FIXME: dirty hack to get search working + this._logEventOffsetsFiltered.length = this._logsArray.length; } /** @@ -263,7 +279,7 @@ class FileManager { this._arrayBuffer = decompressedIRStreamFile; // Approximated decompressed file size - this.state.decompressedSize = formatSizeInBytes(this._arrayBuffer.byteLength, false); + this.state.decompressedHumanSize = formatSizeInBytes(this._arrayBuffer.byteLength, false); this._buildIndex(); this.filterLogEvents(-1); @@ -289,116 +305,110 @@ class FileManager { this._updateStateCallback(CLP_WORKER_PROTOCOL.UPDATE_STATE, this.state); } - /** - * Load plain-text file, update state to viewer - * @private - */ - _loadPlainTextFile () { - readFile(this._fileInfo, this._updateFileLoadProgress, this._updateFileSize) - .then((file) => this._updateInputFileAndStatus(file)) - .then((file) => this._decodePlainTextLogAndUpdate(file.data)) - .catch((error) => { - this._loadingMessageCallback(error.message, true); - console.error("Error processing log file:", error); - }); - } /** - * Decompress and load gzip file, update state to viewer + * Get the file name associated with this.fileSrc, + * whether it's a File object or a URL. + * + * @return {string} * @private */ - _loadGzipFile () { - readFile(this._fileInfo, this._updateFileLoadProgress, this._updateFileSize) - .then((file) => this._updateInputFileAndStatus(file)) - .then((file) => pako.inflate(file.data, {to: "Uint8Array"})) - .then((decompressedLogFile) => this._decodePlainTextLogAndUpdate(decompressedLogFile)) - .catch((error) => { - this._loadingMessageCallback(error.message, true); - console.error("Error processing log file:", error); - }); + _getFileName () { + return this._fileSrc instanceof File ? + this._fileSrc.name : + new URL(this._fileSrc).pathname; } /** - * Decompress and load first file in tar.gz archive, update state to viewer + * Extracts and retrieves content of the first file within a ZIP archive. + * + * @return {Uint8Array} content of the first file as a Uint8Array. + * @throws {Error} if there is an issue loading or extracting the archive * @private */ - _loadTarGzipArchive () { - readFile(this._fileInfo, this._updateFileLoadProgress, this._updateFileSize) - .then((file) => this._updateInputFileAndStatus(file)) - .then((file) => pako.inflate(file.data, {to: "Uint8Array"})) - .then((tarArchive) => { - // Extract the first file in the tar archive - const [entry] = Tarball.extract(tarArchive).filter((entry) => entry.isFile()); - this._appendToFileStateName("/" + entry.fileName); - this._decodePlainTextLogAndUpdate(entry.content); - }) - .catch((error) => { - this._loadingMessageCallback(error.message, true); - console.error("Error processing log file:", error); - }); - } + async _getZipFirstFileContent () { + const zipArchive = await new JSZip().loadAsync(this._fileInfo.data); + const [filePathToDecompress] = Object.keys(zipArchive.files); + this._appendToFileInfoName("/" + filePathToDecompress); - /** - * Decompress and load first file in ZIP archive, update state to viewer - * @private - */ - _loadZipArchive () { - readFile(this._fileInfo, this._updateFileLoadProgress, this._updateFileSize) - .then((file) => this._updateInputFileAndStatus(file)) - .then((file) => (new JSZip()).loadAsync(file.data)) - .then((zipArchive) => { - // Extract the first file in the zip archive - const [filePathToDecompress] = Object.keys(zipArchive.files); - this._appendToFileStateName("/" + filePathToDecompress); - return zipArchive.files[filePathToDecompress].async("uint8array"); - }) - .then((decompressedLogFile) => this._decodePlainTextLogAndUpdate(decompressedLogFile)) - .catch((error) => { - this._loadingMessageCallback(error.message, true); - console.error("Error processing log file:", error); - }); + return zipArchive.files[filePathToDecompress].async("uint8array"); } /** - * Decompress and load zst file, update state to viewer + * Decompresses and retrieves the content of the first file within a + * TAR GZIP archive. + * + * @return {Uint8Array} content of the first file as a Uint8Array. + * @throws {Error} if there is an issue loading or extracting the archive * @private */ - _loadZstFile () { - readFile(this._fileInfo, this._updateFileLoadProgress, this._updateFileSize) - .then((file) => this._updateInputFileAndStatus(file)) - .then((file) => - Promise.all([new Promise((resolve) => - ZstdCodec.run((zstd) => resolve(new zstd.Streaming()))), file])) - .then(([zstdCtx, file]) => zstdCtx.decompress(file.data).buffer) - .then((decompressedLogFile) => this._decodePlainTextLogAndUpdate(decompressedLogFile)) - .catch((error) => { - this._loadingMessageCallback(error.message, true); - console.error("Error processing log file:", error); - }); + _getTarGzipFirstFileContent () { + const tarArchive = pako.inflate(this._fileInfo.data, {to: "Uint8Array"}); + const [entry] = Tarball.extract(tarArchive).filter((entry) => entry.isFile()); + this._appendToFileInfoName("/" + entry.fileName); + + return entry.content; } /** - * Decompress and load CLP IRStream file, update state to viewer - * @private + * Load log file into editor */ - _loadClpIRStreamFile () { - readFile(this._fileInfo, this._updateFileLoadProgress, this._updateFileSize) - .then((file) => this._updateInputFileAndStatus(file)) - .then((file) => - Promise.all([new Promise((resolve) => - ZstdCodec.run((zstd) => resolve(new zstd.Streaming()))), file])) - .then(([zstdCtx, file]) => zstdCtx.decompress(file.data).buffer) - .then((decompressedIRStreamFile) => - this._decodeIRStreamLogAndUpdate(decompressedIRStreamFile)) - .catch((error) => { - if (error instanceof DataInputStreamEOFError) { - // If the file is truncated, send back a user-friendly error - this._loadingMessageCallback("IRStream truncated", true); - } else { - this._loadingMessageCallback(error.message, true); + async loadLogFile () { + const fileName = this._getFileName(); + const fileInfo = await readFile(this._fileSrc, this._updateFileLoadProgress); + this._updateInputFileInfo(fileInfo); + + const fileExtensionHandlers = { + ".clp": async () => { + console.log("Opening CLP compressed archive"); + return this._decodeClpArchiveLogAndUpdate(fileInfo.data); + }, + ".clp.zst": async () => { + console.log("Opening CLP IRStream compressed file: " + fileName); + return FileManager.#decompressZstd(fileInfo.data); + }, + ".zst": async () => { + console.log("Opening zst compressed file: " + fileName); + return FileManager.#decompressZstd(fileInfo.data); + }, + ".zip": async () => { + console.log("Opening zip compressed archive: " + fileName); + return this._getZipFirstFileContent(); + }, + ".tar.gz": async () => { + console.log("Opening tar.gz compressed archive: " + fileName); + return this._getTarGzipFirstFileContent(); + }, + ".gz": async () => { + console.log("Opening gzip compressed file: " + fileName); + return pako.inflate(fileInfo.data, {to: "Uint8Array"}); + }, + ".gzip": async () => { + console.log("Opening gzip compressed file: " + fileName); + return pako.inflate(fileInfo.data, {to: "Uint8Array"}); + }, + }; + + let isPlainTextFile = true; + for (const extension in fileExtensionHandlers) { + if (fileName.endsWith(extension)) { + isPlainTextFile = false; + + const decompressedLogFile = await fileExtensionHandlers[extension](); + if (".clp.zst" === extension) { + this._decodeIRStreamLogAndUpdate(decompressedLogFile); + } else if (".clp" !== extension) { + this._decodePlainTextLogAndUpdate(decompressedLogFile); } - console.error(error); - }); + + break; + } + } + + if (isPlainTextFile) { + console.log("Opening plain-text file: " + fileName); + this._decodePlainTextLogAndUpdate(fileInfo.data); + } } /** @@ -409,8 +419,8 @@ class FileManager { */ async _decodeClpArchiveLogAndUpdate (decompressedLogFile) { // Update decompression status - this.state.decompressedSize = formatSizeInBytes(decompressedLogFile.byteLength, false); - this._loadingMessageCallback(`Decompressed ${this.state.decompressedSize}.`); + this.state.decompressedHumanSize = formatSizeInBytes(decompressedLogFile.byteLength, false); + this._loadingMessageCallback(`Decompressed ${this.state.decompressedHumanSize}.`); const clpArchiveDecoder = new ClpArchiveDecoder(decompressedLogFile); this._logsArray = await clpArchiveDecoder.decode(); @@ -430,56 +440,6 @@ class FileManager { this._logEventOffsetsFiltered.length = this._logsArray.length; } - /** - * Decompress and load CLP Archive file, update state to viewer - * @private - */ - _loaClpArchiveFile () { - readFile(this._fileInfo, this._updateFileLoadProgress, this._updateFileSize) - .then((file) => this._updateInputFileAndStatus(file)) - .then((file) => this._decodeClpArchiveLogAndUpdate(file.data)) - .catch((error) => { - this._loadingMessageCallback(error.message, true); - console.error("Error processing log file:", error); - }); - } - - /** - * Load log file into editor - */ - loadLogFile () { - let filePath; - if (this._fileInfo instanceof File) { - filePath = this._fileInfo.name; - } else { - const url = new URL(this._fileInfo); - filePath = url.pathname; - } - - if (filePath.endsWith(".clp.zst")) { - console.log("Opening CLP IRStream compressed file: " + filePath); - this._loadClpIRStreamFile(); - } else if (filePath.endsWith(".clp")) { - console.log("Opening CLP compressed archive"); - this._loaClpArchiveFile(); - } else if (filePath.endsWith(".zst")) { - console.log("Opening zst compressed file: " + filePath); - this._loadZstFile(); - } else if (filePath.endsWith(".zip")) { - console.log("Opening zip compressed archive: " + filePath); - this._loadZipArchive(); - } else if (filePath.endsWith(".tar.gz")) { - console.log("Opening tar.gz compressed archive: " + filePath); - this._loadTarGzipArchive(); - } else if (filePath.endsWith(".gz") || filePath.endsWith(".gzip")) { - console.log("Opening gzip compressed file: " + filePath); - this._loadGzipFile(); - } else { - console.log("Opening plain-text file: " + filePath); - this._loadPlainTextFile(); - } - } - /** * @param {number} timestamp The timestamp to search for as milliseconds * since the UNIX epoch. @@ -579,7 +539,7 @@ class FileManager { const logEvents = this._logEventOffsets.slice(targetEvent, targetEvent + numberOfEvents ); this._workerPool.assignTask({ - fileName: this._fileState.name, + fileName: this._fileInfo.name, page: page, logEvents: logEvents, inputStream: inputStream, @@ -593,11 +553,17 @@ class FileManager { if (null !== this._logsArray) { // for Single-file CLP Archive only const startingEventIdx = this.state.pageSize * (this.state.page - 1); - const endingEventIdx = this.state.pageSize * (this.state.page) - 1; + const endingEventIdx = Math.min( + this._logsArray.length, + this.state.pageSize * (this.state.page)); let offset = 0; this._logsLineOffsetsArray.length = 0; this._logs = ""; + console.log(this._logsLineOffsetsArray); + console.log(this._logs); + console.log(startingEventIdx); + console.log(endingEventIdx); for (let i = startingEventIdx; i< endingEventIdx; i++) { this._logs += this._logsArray[i] + "\n"; this._logsLineOffsetsArray.push(offset);