UNPKG

@bluwy/giget-core

Version:

Download template from a git repo or tarball URL

199 lines (170 loc) 5.28 kB
import fs from 'node:fs/promises' import fss from 'node:fs' import os from 'node:os' import path from 'node:path' import { pipeline } from 'node:stream/promises' import { createGunzip } from 'node:zlib' import { unpackTar } from 'modern-tar/fs' import { DownloadFailedError, SubdirNotFoundError } from './errors.js' import { providers as builtinProviders } from './providers.js' /** @import { ProviderName, TemplateProvider } from './index.d.ts' */ const sourceProtoRe = /^([\w-.]+):/ /** * * @param {string} input * @param {ProviderName | undefined} providerName * @param {Record<ProviderName, TemplateProvider> | undefined} providers * @returns {{ source: string, providerName: ProviderName, provider: TemplateProvider | undefined }} */ export function getProvider(input, providerName, providers) { providerName ||= 'github' let source = input const sourceProviderMatch = input.match(sourceProtoRe) if (sourceProviderMatch) { providerName = sourceProviderMatch[1] if (providerName !== 'http' && providerName !== 'https') { source = input.slice(sourceProviderMatch[0].length) } } const provider = providers?.[providerName] || builtinProviders[providerName] return { source, providerName, provider, } } /** * @param {string} url * @param {string} filePath * @param {RequestInit} [options] * @returns */ export async function download(url, filePath, options = {}) { const infoPath = filePath + '.json' /** @type {{ etag?: string }} */ const info = JSON.parse(await fs.readFile(infoPath, 'utf8').catch(() => '{}')) const headResponse = await sendFetch(url, { ...options, method: 'HEAD', }).catch(() => undefined) const etag = headResponse?.headers.get('etag') if (info.etag === etag && fss.existsSync(filePath)) { // Already downloaded return } if (typeof etag === 'string') { info.etag = etag } const response = await sendFetch(url, { headers: options.headers }) if (response.status >= 400) { throw new DownloadFailedError( `Failed to download ${url}: ${response.status} ${response.statusText}`, ) } if (response.body == null) { throw new DownloadFailedError( `Failed to download ${url}: empty response body`, ) } await fs.mkdir(path.dirname(filePath), { recursive: true }) const stream = fss.createWriteStream(filePath) await pipeline(response.body, stream) await fs.writeFile(infoPath, JSON.stringify(info), 'utf8') } const inputRegex = /^(?<repo>[\w\-.]+\/[\w\-.]+)(?<subdir>[^#]+)?(?<ref>#[\w\-./@]+)?/ /** @type {import('./index.d.ts').parseGitURI} */ export function parseGitURI(input) { const m = input.match(inputRegex)?.groups || {} return { repo: m.repo, subdir: m.subdir || '/', ref: m.ref?.slice(1), } } /** * @param {...unknown} args */ export function debug(...args) { if (process.env.DEBUG) { console.debug('[giget]', ...args) } } /** * @param {string} url * @param {RequestInit & { validateStatus?: boolean }} [options] */ export async function sendFetch(url, options = {}) { // https://github.com/nodejs/undici/issues/1305 if (options.headers?.['sec-fetch-mode']) { options.mode = options.headers['sec-fetch-mode'] } const res = await fetch(url, options).catch((error) => { throw new Error(`Failed to fetch ${url}`, { cause: error }) }) if (options.validateStatus && res.status >= 400) { throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`) } return res } export function cacheDirectory() { return process.env.XDG_CACHE_HOME ? path.resolve(process.env.XDG_CACHE_HOME, 'bluwy-giget') : path.resolve(os.homedir(), '.cache/bluwy-giget') } /** * @param {string} tarPath * @param {string} extractPath * @param {string} [subdir] */ export async function extract(tarPath, extractPath, subdir) { // subdir `/` is the same as root, so ignore as undefined if (subdir === '/') { subdir = undefined } // If `subdir` is defined, ensure it has no leading slash, and has a trailing slash if (subdir) { if (subdir.startsWith('/')) { subdir = subdir.slice(1) } if (!subdir.endsWith('/')) { subdir += '/' } } let subdirFound = false // Create an empty directory here to make tar happy await fs.mkdir(extractPath, { recursive: true }) const readStream = fss.createReadStream(tarPath) const unpackStream = unpackTar(extractPath, { filter(entry) { const path = entry.name.split('/').slice(1).join('/') // Skip the root directory if (path === '') { return false } if (!subdir) { return true } if (path.startsWith(subdir)) { subdirFound = true return true } else { return false } }, map(entry) { let path = entry.name.split('/').slice(1).join('/') if (subdir) { path = path.slice(subdir.length) } entry.name = path return entry }, }) await pipeline(readStream, createGunzip(), unpackStream) if (subdir && !subdirFound) { // Clean up as it should be empty await fs.rm(extractPath, { recursive: true, force: true }) throw new SubdirNotFoundError(`Subdirectory not found in tar: ${subdir}`) } }