import { createWriteStream , ensureDir , pathExists , rename , writeFile } from "fs-extra" ; import fetch , { Response } from "node-fetch" ; import pDefer from "p-defer" ; import path from "path" ; import tar from "tar" ; import { URL } from "url" ; import { sha256hex } from "./crypto" ; import { InternalError } from "./internal_error" ; import { xdgCacheDir } from "./xdg" ; export interface FetchToCacheCommon { /** Short human-friendly name for this download. Used in local path. */ name : string ; /** URL to fetch file from */ url : string ; /**
     * A human-friendly string describing the version of the file, used in local
     * path.
     */ version ? : string ; } export interface FetchToCacheFileOptions extends FetchToCacheCommon { defaultValue file ? : string ; /** Mode (permissions) for the local file. */ mode ? : number ; } export interface FetchToCacheTarOptions extends FetchToCacheCommon { tarOptions ? : tar . ExtractOptions ; fileList ? : string [ ] ; untar : true ; } type FetchToCacheOptions = FetchToCacheFileOptions | FetchToCacheTarOptions ; function isFileOptions ( opt ) { return ( opt as FetchToCacheTarOptions ) . untar !== true ; } export interface CachedFile { file : string ; dir : string ; } export interface CachedDir { dir : string ; } type CachePromise = pDefer . DeferredPromise < CachedFile | CachedDir > ; const cacheMap = new Map < string , CachePromise > ( ) ; interface CacheKey { name : string ; url : string ; version ? : string ; } const cacheKey = ( { name , url , version } ) => `${ name }-${ version }-${ url }` ; remarks returns beta export async function fetchToCache ( options ) ; export async function fetchToCache ( options ) ; export async function fetchToCache ( options ) { const key = cacheKey ( options ) ; const { name , url , version } = options ; const entry = cacheMap . get ( key ) ; if ( entry ) return entry . promise ; const deferred = pDefer < CachedFile | CachedDir > ( ) ; cacheMap . set ( key , deferred ) ; try { const toolsCacheBase = path . join ( xdgCacheDir ( ) , "tools" ) ; const sha = sha256hex ( url ) . slice ( 0 , 5 ) ; const tag = version ? `${ version }-${ sha }` : sha ; const dir = path . join ( toolsCacheBase , name , tag ) ; const complete = path . join ( dir , ".complete" ) ; let file ; if ( isFileOptions ( options ) ) { const urlObj = new URL ( url ) ; const baseName = options . file || urlObj . pathname . split ( "/" ) . slice ( - 1 ) [ 0 ] ; if ( ! baseName ) { throw new Error ( `Cannot determine a local file name to use for fetching ${ name }. ` + `Use the 'file' option.` ) ; } file = path . join ( dir , baseName ) ; } if ( ! ( await pathExists ( complete ) ) ) { await ensureDir ( dir , 0o700 ) ; const response = await fetch ( url ) ; if ( response . status !== 200 ) throw new Error ( `Could not get ${ name } from ${ url }: ${ response . statusText }` ) ; if ( isFileOptions ( options ) ) { if ( ! file ) throw new InternalError ( `file cannot be null` ) ; await downloadFile ( file , response , options ) ; } else { await downloadTar ( dir , response , options ) ; } await writeFile ( complete , "" ) ; } deferred . resolve ( { dir , file } ) ; } catch ( err ) { deferred . reject ( err ) ; } return deferred . promise ; } async function downloadFile ( file , response , options ) { const mode = options . mode != null ? options . mode : 0o500 ; const fileStream = createWriteStream ( `${ file }.download` , { mode , flags : "wx" } ) ; await waitForStreams ( response . body , fileStream , options ) ; await rename ( `${ file }.download` , file ) ; } async function downloadTar ( dir , response , options ) { const tarOptions = { ... ( options . tarOptions || { } ) , cwd : dir , } ; const tarStream = tar . extract ( tarOptions || { } , options . fileList ) ; await waitForStreams ( response . body , tarStream , options ) ; } async function waitForStreams ( from , to , { name , url } ) { return new Promise ( ( res , rej ) => { let err ; to . on ( "close" , res ) ; to . on ( "error" , ( e ) => { if ( ! err ) { rej ( e ) ; err = e ; } else { console . error ( `Unhandled error while writing ${ name } from ${ url }:` , e ) ; } } ) ; from . on ( "error" , ( e ) => { if ( ! err ) { rej ( e ) ; err = e ; } else { console . error ( `Unhandled error downloading ${ name } from ${ url }:` , e ) ; } } ) ; from . pipe ( to ) ; } ) ; }