1
0
Fork 0
mirror of synced 2024-06-26 02:31:09 -04:00
ultimate-vim/sources_non_forked/coc.nvim/src/model/download.ts
2022-07-20 13:20:15 +08:00

145 lines
4.6 KiB
TypeScript

'use strict'
import contentDisposition from 'content-disposition'
import { http, https } from 'follow-redirects'
import fs, { Stats } from 'fs-extra'
import { IncomingMessage } from 'http'
import path from 'path'
import tar from 'tar'
import unzip from 'unzip-stream'
import { v1 as uuidv1 } from 'uuid'
import { CancellationToken } from 'vscode-languageserver-protocol'
import { resolveRequestOptions, FetchOptions } from './fetch'
const logger = require('../util/logger')('model-download')
export interface DownloadOptions extends Omit<FetchOptions, 'buffer'> {
/**
* Folder that contains downloaded file or extracted files by untar or unzip
*/
dest: string
/**
* Remove the specified number of leading path elements for *untar* only, default to `1`.
*/
strip?: number
/**
* If true, use untar for `.tar.gz` filename
*/
extract?: boolean | 'untar' | 'unzip'
onProgress?: (percent: string) => void
}
/**
* Download file from url, with optional untar/unzip support.
*
* @param {string} url
* @param {DownloadOptions} options contains dest folder and optional onProgress callback
*/
export default function download(url: string, options: DownloadOptions, token?: CancellationToken): Promise<string> {
let { dest, onProgress, extract } = options
if (!dest || !path.isAbsolute(dest)) {
throw new Error(`Expect absolute file path for dest option.`)
}
let stat: Stats
try {
stat = fs.statSync(dest)
} catch (_e) {
fs.mkdirpSync(dest)
}
if (stat && !stat.isDirectory()) {
throw new Error(`${dest} exists, but not directory!`)
}
let mod = url.startsWith('https') ? https : http
let opts = resolveRequestOptions(url, options)
let extname = path.extname(url)
return new Promise<string>((resolve, reject) => {
if (token) {
let disposable = token.onCancellationRequested(() => {
disposable.dispose()
req.destroy(new Error('request aborted'))
})
}
let timer: NodeJS.Timer
const req = mod.request(opts, (res: IncomingMessage) => {
if ((res.statusCode >= 200 && res.statusCode < 300) || res.statusCode === 1223) {
let headers = res.headers || {}
let dispositionHeader = headers['content-disposition']
if (!extname && dispositionHeader) {
let disposition = contentDisposition.parse(dispositionHeader)
if (disposition.parameters?.filename) {
extname = path.extname(disposition.parameters.filename)
}
}
if (extract === true) {
if (extname === '.zip' || headers['content-type'] == 'application/zip') {
extract = 'unzip'
} else if (extname == '.tgz') {
extract = 'untar'
} else {
reject(new Error(`Unable to extract for ${url}`))
return
}
}
let total = Number(headers['content-length'])
let cur = 0
if (!isNaN(total)) {
res.on('data', chunk => {
cur += chunk.length
let percent = (cur / total * 100).toFixed(1)
if (onProgress) {
onProgress(percent)
} else {
logger.info(`Download ${url} progress ${percent}%`)
}
})
}
res.on('error', err => {
reject(new Error(`Unable to connect ${url}: ${err.message}`))
})
res.on('data', () => {
if (timer) {
clearTimeout(timer)
timer = undefined
}
})
res.on('end', () => {
logger.info('Download completed:', url)
})
let stream: any
if (extract === 'untar') {
stream = res.pipe(tar.x({ strip: options.strip ?? 1, C: dest }))
} else if (extract === 'unzip') {
stream = res.pipe(unzip.Extract({ path: dest }))
} else {
dest = path.join(dest, `${uuidv1()}${extname}`)
stream = res.pipe(fs.createWriteStream(dest))
}
stream.on('finish', () => {
logger.info(`Downloaded ${url} => ${dest}`)
setTimeout(() => {
resolve(dest)
}, 100)
})
stream.on('error', reject)
} else {
reject(new Error(`Invalid response from ${url}: ${res.statusCode}`))
}
})
req.on('error', e => {
// Possible succeed proxy request with ECONNRESET error on node > 14
if (opts.agent && e.code == 'ECONNRESET') {
timer = setTimeout(() => {
reject(e)
}, 500)
} else {
reject(e)
}
})
req.on('timeout', () => {
req.destroy(new Error(`request timeout after ${options.timeout}ms`))
})
if (options.timeout) {
req.setTimeout(options.timeout)
}
req.end()
})
}