import brotli from "brotli" ; import fs from "fs-extra" ; import yaml from "js-yaml" ; import path from "path" ; import { gunzipSync , gzipSync } from "zlib" ; import { CompressionAlgorithm , Headers , PersistedBuffer , PersistedTapeRecord , TapeRecord , } from "./tape" ; /**
 * Persistence layer to save tapes to disk and read them from disk.
 */ export class Persistence { constructor ( private readonly tapeDir , private readonly redactHeaders ) { } /**
     * Saves the tape to disk.
     */ saveTapeToDisk ( tapeName , tapeRecords ) { const persistedTapeRecords = tapeRecords . map ( this . redact , this ) . map ( persistTape ) ; const tapePath = this . getTapePath ( tapeName ) ; fs . ensureDirSync ( path . dirname ( tapePath ) ) ; fs . writeFileSync ( tapePath , yaml . safeDump ( { http_interactions : persistedTapeRecords , } ) , "utf8" ) ; } /**
     * Redacts the request headers of the given record, depending on the redactHeaders array
     */ private redact ( record ) { redactRequestHeaders ( record , this . redactHeaders ) ; return record ; } /**
     * Loads the tape from disk.
     */ loadTapeFromDisk ( tapeName ) { const tapePath = this . getTapePath ( tapeName ) ; if ( ! fs . existsSync ( tapePath ) ) { throw new Error ( `No tape found with name ${ tapeName }` ) ; } const persistedTapeRecords = yaml . safeLoad ( fs . readFileSync ( tapePath , "utf8" ) ) . http_interactions as PersistedTapeRecord [ ] ; return persistedTapeRecords . map ( reviveTape ) ; } isTapeNameValid ( tapeName ) { return ! path . relative ( this . tapeDir , path . join ( this . tapeDir , tapeName ) ) . startsWith ( "../" ) ; } /**
     * Returns the tape's path on disk.
     */ private getTapePath ( tapeName ) { return path . join ( this . tapeDir , `${ tapeName }.yml` ) ; } } /**
 * Redacts the headers of the given record based on the provided array of headers to redact
 */ export function redactRequestHeaders ( record , redactHeaders ) { redactHeaders . forEach ( ( header ) => { if ( record . request . headers [ header ] ) { record . request . headers [ header ] = "XXXX" ; } } ) ; } export function persistTape ( record ) { return { request : { method : record . request . method , path : record . request . path , headers : record . request . headers , body : serialiseBuffer ( record . request . body , record . request . headers ) , } , response : { status : record . response . status , headers : record . response . headers , body : serialiseBuffer ( record . response . body , record . response . headers ) , } , } ; } export function reviveTape ( persistedRecord ) { return { request : { method : persistedRecord . request . method , path : persistedRecord . request . path , headers : persistedRecord . request . headers , body : unserialiseBuffer ( persistedRecord . request . body ) , } , response : { status : persistedRecord . response . status , headers : persistedRecord . response . headers , body : unserialiseBuffer ( persistedRecord . response . body ) , } , } ; } export function serialiseBuffer ( buffer , headers ) { const header = headers [ "content-encoding" ] ; const contentEncoding = typeof header === "string" ? header : undefined ; const originalBuffer = buffer ; let compression = "none" ; if ( contentEncoding === "br" ) { buffer = Buffer . from ( brotli . decompress ( buffer ) ) ; compression = "br" ; } if ( contentEncoding === "gzip" ) { buffer = gunzipSync ( buffer ) ; compression = "gzip" ; } const utf8Representation = buffer . toString ( "utf8" ) ; try { const recreatedBuffer = Buffer . from ( yaml . safeLoad ( yaml . safeDump ( utf8Representation ) ) , "utf8" ) ; if ( Buffer . compare ( buffer , recreatedBuffer ) === 0 ) { return { encoding : "utf8" , data : utf8Representation , compression , } ; } } catch { } return { encoding : "base64" , data : originalBuffer . toString ( "base64" ) , } ; } function unserialiseBuffer ( persisted ) { let buffer ; switch ( persisted . encoding ) { case "base64" : buffer = Buffer . from ( persisted . data , "base64" ) ; break ; case "utf8" : buffer = Buffer . from ( persisted . data , "utf8" ) ; if ( persisted . compression === "br" ) { const compressed = brotli . compress ( buffer ) ; if ( compressed ) { buffer = Buffer . from ( compressed ) ; } else { throw new Error ( `Brotli compression failed!` ) ; } } if ( persisted . compression === "gzip" ) { buffer = gzipSync ( buffer ) ; } break ; default : throw new Error ( `Unsupported encoding!` ) ; } return buffer ; }