-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(download): custom repository download implementation
Avoids old strip-dirs dependency. release-npm
- Loading branch information
Showing
21 changed files
with
364 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import { mkdirSync, readFileSync } from 'node:fs' | ||
import { link, mkdir, readlink, realpath, symlink, utimes, writeFile } from 'node:fs/promises' | ||
import { dirname, join } from 'node:path' | ||
// @ts-ignore | ||
import decompressTar from 'decompress-tar' | ||
// @ts-ignore | ||
import decompressTarbz2 from 'decompress-tarbz2' | ||
// @ts-ignore | ||
import decompressTargz from 'decompress-targz' | ||
// @ts-ignore | ||
import decompressUnzip from 'decompress-unzip' | ||
// @ts-ignore | ||
import stripDirs from 'strip-dirs' | ||
|
||
interface PathFile { | ||
path: string | ||
mode: number | ||
type: string | ||
mtime: Date | ||
linkname: string | ||
data: string | ||
} | ||
|
||
interface Options { | ||
strip: number | ||
filter?: () => boolean | ||
map?: (value: PathFile) => PathFile | ||
plugins: ((input: Buffer, options: Options) => string)[] | ||
} | ||
|
||
function runPlugins(input: Buffer, options: Options) { | ||
if (options.plugins.length === 0) { | ||
return Promise.resolve([]) | ||
} | ||
|
||
return Promise.all(options.plugins.map((x) => x(input, options))).then((files) => files.reduce((a, b) => a.concat(b))) | ||
} | ||
|
||
function safeMakeDir(directory: string, realOutputPath: string): Promise<string> { | ||
return realpath(directory) | ||
.catch((_) => { | ||
const parent = dirname(directory) | ||
return safeMakeDir(parent, realOutputPath) | ||
}) | ||
.then((realParentPath) => { | ||
if (realParentPath.indexOf(realOutputPath) !== 0) { | ||
throw new Error('Refusing to create a directory outside the output path.') | ||
} | ||
|
||
mkdirSync(directory, { recursive: true }) | ||
return directory | ||
}) | ||
} | ||
|
||
const preventWritingThroughSymlink = (destination: string, realOutputPath: string) => { | ||
return readlink(destination) | ||
.catch((_) => { | ||
// Either no file exists, or it's not a symlink. In either case, this is | ||
// not an escape we need to worry about in this phase. | ||
return null | ||
}) | ||
.then(() => { | ||
// No symlink exists at `destination`, so we can continue | ||
return realOutputPath | ||
}) | ||
} | ||
|
||
const extractFile = (input: Buffer, output: string, options: Options) => | ||
runPlugins(input, options).then((input) => { | ||
let files: PathFile[] = input as PathFile[] | ||
if (options.strip > 0) { | ||
files = files | ||
.map((x) => { | ||
x.path = stripDirs(x.path, options.strip) | ||
return x | ||
}) | ||
.filter((x) => x.path !== '.') | ||
} | ||
|
||
if (typeof options.filter === 'function') { | ||
files = files.filter(options.filter) | ||
} | ||
|
||
if (typeof options.map === 'function') { | ||
files = files.map(options.map) | ||
} | ||
|
||
if (!output) { | ||
return files | ||
} | ||
|
||
return Promise.all( | ||
files.map((x) => { | ||
const dest = join(output, x.path) | ||
const mode = x.mode & ~process.umask() | ||
const now = new Date() | ||
|
||
if (x.type === 'directory') { | ||
mkdir(output, { recursive: true }) | ||
|
||
return realpath(output) | ||
.then((realOutputPath) => safeMakeDir(dest, realOutputPath)) | ||
.then(() => utimes(dest, now, x.mtime)) | ||
.then(() => x) | ||
} | ||
|
||
mkdir(output, { recursive: true }) | ||
|
||
return realpath(output) | ||
.then((realOutputPath) => { | ||
// Attempt to ensure parent directory exists (failing if it's outside the output dir) | ||
return safeMakeDir(dirname(dest), realOutputPath).then(() => realOutputPath) | ||
}) | ||
.then((realOutputPath) => { | ||
if (x.type === 'file') { | ||
return preventWritingThroughSymlink(dest, realOutputPath) | ||
} | ||
|
||
return realOutputPath | ||
}) | ||
.then((realOutputPath) => { | ||
return realpath(dirname(dest)).then((realDestinationDir) => { | ||
if (realDestinationDir.indexOf(realOutputPath) !== 0) { | ||
throw new Error(`Refusing to write outside output directory: ${realDestinationDir}`) | ||
} | ||
}) | ||
}) | ||
.then(() => { | ||
if (x.type === 'link') { | ||
return link(x.linkname, dest) | ||
} | ||
|
||
if (x.type === 'symlink' && process.platform === 'win32') { | ||
return link(x.linkname, dest) | ||
} | ||
|
||
if (x.type === 'symlink') { | ||
return symlink(x.linkname, dest) | ||
} | ||
|
||
return writeFile(dest, x.data, { mode }) | ||
}) | ||
.then(() => { | ||
if (x.type === 'file') { | ||
utimes(dest, now, x.mtime) | ||
} | ||
}) | ||
}), | ||
) | ||
}) | ||
|
||
export function decompress(inputZipFile: string, destinationDirectory: string) { | ||
if (typeof inputZipFile !== 'string' && !Buffer.isBuffer(inputZipFile)) { | ||
return Promise.reject(new TypeError('Input file required')) | ||
} | ||
const options: Options = { strip: 0, plugins: [decompressTar(), decompressTarbz2(), decompressTargz(), decompressUnzip()] } | ||
return extractFile(readFileSync(inputZipFile), destinationDirectory, options) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs' | ||
import { join } from 'node:path' | ||
import axios from 'axios' | ||
import { decompress } from './decompress' | ||
import { log } from './log' | ||
|
||
async function downloadFile(url: string, extractDirectory: string) { | ||
if (!existsSync(extractDirectory)) { | ||
mkdirSync(extractDirectory, { recursive: true }) | ||
} | ||
|
||
const zipFilePath = join(process.cwd(), `temp-${Math.floor(Math.random() * (999 - 1 + 1) + 1)}.zip`) | ||
const body = await axios.get(url, { | ||
responseType: 'arraybuffer', | ||
}) | ||
|
||
writeFileSync(zipFilePath, Buffer.from(body.data)) | ||
await decompress(zipFilePath, extractDirectory) | ||
unlinkSync(zipFilePath) | ||
|
||
return false | ||
} | ||
|
||
// Custom implementation of the 'download-git-repo' package. | ||
export async function download(url: string, destination: string) { | ||
const respository = normalize(url) | ||
const fullUrl = respository.url || getUrl(respository) | ||
|
||
if (!url) { | ||
log('Repository not found', 'error') | ||
return | ||
} | ||
|
||
const error = await downloadFile(fullUrl, destination) | ||
|
||
if (error) { | ||
log(`Failed to download repository contents from ${fullUrl}`, 'error') | ||
return | ||
} | ||
|
||
return true | ||
} | ||
|
||
const normalize = (url: string) => { | ||
let regex = /^(?:(direct):([^#]+)(?:#(.+))?)$/ | ||
let match = regex.exec(url) | ||
|
||
if (match) { | ||
// TODO customizable branch. | ||
const [_, , url, directCheckout = 'main'] = match | ||
return { | ||
type: 'direct', | ||
url, | ||
checkout: directCheckout, | ||
} | ||
} | ||
|
||
regex = /^(?:(github|gitlab|bitbucket):)?(?:(.+):)?([^/]+)\/([^#]+)(?:#(.+))?$/ | ||
match = regex.exec(url) | ||
const [_, type = 'github', origin, owner, name, checkout = 'main'] = match ?? [] | ||
return { | ||
type, | ||
origin: origin ?? (type === 'github' ? 'github.com' : type === 'gitlab' ? 'gitlab.com' : 'bitbucket.org'), | ||
owner, | ||
name, | ||
checkout, | ||
} | ||
} | ||
|
||
function addProtocol(origin: string) { | ||
if (!/^(f|ht)tps?:\/\//i.test(origin)) { | ||
return `https://${origin}` | ||
} | ||
|
||
return origin | ||
} | ||
|
||
function getUrl(repository: ReturnType<typeof normalize>) { | ||
let url = '' | ||
|
||
// Get origin with protocol and add trailing slash or colon (for ssh) | ||
let origin = addProtocol(repository.origin as string) | ||
if (/^git@/i.test(origin)) { | ||
origin = `${origin}:` | ||
} else { | ||
origin = `${origin}/` | ||
} | ||
|
||
if (repository.type === 'github') { | ||
url = `${origin + repository.owner}/${repository.name}/archive/${repository.checkout}.zip` | ||
} else if (repository.type === 'gitlab') { | ||
url = `${origin + repository.owner}/${repository.name}/repository/archive.zip?ref=${repository.checkout}` | ||
} else if (repository.type === 'bitbucket') { | ||
url = `${origin + repository.owner}/${repository.name}/get/${repository.checkout}.zip` | ||
} | ||
|
||
return url | ||
} | ||
|
||
export async function downloadTemplate(url: string, cachePath: string) { | ||
const success = await download(url, cachePath) | ||
|
||
if (!success) { | ||
log(`Couldn't download repository from ${url}`, 'error') | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
Oops, something went wrong.