mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 12:57:47 +00:00
[build-utils] Preserve symlinks for FileRef and FileBlob types in download() (#7534)
Co-authored-by: Nathan Rajlich <n@n8.io>
This commit is contained in:
@@ -48,6 +48,10 @@ export default class FileBlob implements FileBase {
|
|||||||
return new FileBlob({ mode, contentType, data });
|
return new FileBlob({ mode, contentType, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async toStreamAsync(): Promise<NodeJS.ReadableStream> {
|
||||||
|
return this.toStream();
|
||||||
|
}
|
||||||
|
|
||||||
toStream(): NodeJS.ReadableStream {
|
toStream(): NodeJS.ReadableStream {
|
||||||
return intoStream(this.data);
|
return intoStream(this.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import debug from '../debug';
|
|||||||
import FileFsRef from '../file-fs-ref';
|
import FileFsRef from '../file-fs-ref';
|
||||||
import { File, Files, Meta } from '../types';
|
import { File, Files, Meta } from '../types';
|
||||||
import { remove, mkdirp, readlink, symlink } from 'fs-extra';
|
import { remove, mkdirp, readlink, symlink } from 'fs-extra';
|
||||||
|
import streamToBuffer from './stream-to-buffer';
|
||||||
|
|
||||||
export interface DownloadedFiles {
|
export interface DownloadedFiles {
|
||||||
[filePath: string]: FileFsRef;
|
[filePath: string]: FileFsRef;
|
||||||
@@ -15,19 +16,44 @@ export function isSymbolicLink(mode: number): boolean {
|
|||||||
return (mode & S_IFMT) === S_IFLNK;
|
return (mode & S_IFMT) === S_IFLNK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function prepareSymlinkTarget(
|
||||||
|
file: File,
|
||||||
|
fsPath: string
|
||||||
|
): Promise<string> {
|
||||||
|
const mkdirPromise = mkdirp(path.dirname(fsPath));
|
||||||
|
if (file.type === 'FileFsRef') {
|
||||||
|
const [target] = await Promise.all([readlink(file.fsPath), mkdirPromise]);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type === 'FileRef' || file.type === 'FileBlob') {
|
||||||
|
const targetPathBufferPromise = await streamToBuffer(
|
||||||
|
await file.toStreamAsync()
|
||||||
|
);
|
||||||
|
const [targetPathBuffer] = await Promise.all([
|
||||||
|
targetPathBufferPromise,
|
||||||
|
mkdirPromise,
|
||||||
|
]);
|
||||||
|
return targetPathBuffer.toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`file.type "${(file as any).type}" not supported for symlink`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadFile(file: File, fsPath: string): Promise<FileFsRef> {
|
async function downloadFile(file: File, fsPath: string): Promise<FileFsRef> {
|
||||||
const { mode } = file;
|
const { mode } = file;
|
||||||
if (mode && isSymbolicLink(mode) && file.type === 'FileFsRef') {
|
|
||||||
const [target] = await Promise.all([
|
if (isSymbolicLink(mode)) {
|
||||||
readlink(file.fsPath),
|
const target = await prepareSymlinkTarget(file, fsPath);
|
||||||
mkdirp(path.dirname(fsPath)),
|
|
||||||
]);
|
|
||||||
await symlink(target, fsPath);
|
await symlink(target, fsPath);
|
||||||
return FileFsRef.fromFsPath({ mode, fsPath });
|
return FileFsRef.fromFsPath({ mode, fsPath });
|
||||||
} else {
|
|
||||||
const stream = file.toStream();
|
|
||||||
return FileFsRef.fromStream({ mode, stream, fsPath });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stream = file.toStream();
|
||||||
|
return FileFsRef.fromStream({ mode, stream, fsPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeFile(basePath: string, fileMatched: string) {
|
async function removeFile(basePath: string, fileMatched: string) {
|
||||||
|
|||||||
73
packages/build-utils/test/unit.test.ts
vendored
73
packages/build-utils/test/unit.test.ts
vendored
@@ -1,7 +1,7 @@
|
|||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs, { readlink } from 'fs-extra';
|
||||||
import { strict as assert } from 'assert';
|
import { strict as assert, strictEqual } from 'assert';
|
||||||
import { createZip } from '../src/lambda';
|
import { createZip } from '../src/lambda';
|
||||||
import { getSupportedNodeVersion } from '../src/fs/node-version';
|
import { getSupportedNodeVersion } from '../src/fs/node-version';
|
||||||
import download from '../src/fs/download';
|
import download from '../src/fs/download';
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
runNpmInstall,
|
runNpmInstall,
|
||||||
runPackageJsonScript,
|
runPackageJsonScript,
|
||||||
scanParentDirs,
|
scanParentDirs,
|
||||||
|
FileBlob,
|
||||||
} from '../src';
|
} from '../src';
|
||||||
|
|
||||||
async function expectBuilderError(promise: Promise<any>, pattern: string) {
|
async function expectBuilderError(promise: Promise<any>, pattern: string) {
|
||||||
@@ -47,7 +48,7 @@ afterEach(() => {
|
|||||||
console.warn = originalConsoleWarn;
|
console.warn = originalConsoleWarn;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should re-create symlinks properly', async () => {
|
it('should re-create FileFsRef symlinks properly', async () => {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
console.log('Skipping test on windows');
|
console.log('Skipping test on windows');
|
||||||
return;
|
return;
|
||||||
@@ -69,6 +70,72 @@ it('should re-create symlinks properly', async () => {
|
|||||||
assert(linkStat.isSymbolicLink());
|
assert(linkStat.isSymbolicLink());
|
||||||
assert(linkDirStat.isSymbolicLink());
|
assert(linkDirStat.isSymbolicLink());
|
||||||
assert(aStat.isFile());
|
assert(aStat.isFile());
|
||||||
|
|
||||||
|
const [linkDirContents, linkTextContents] = await Promise.all([
|
||||||
|
readlink(path.join(outDir, 'link-dir')),
|
||||||
|
readlink(path.join(outDir, 'link.txt')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
strictEqual(linkDirContents, 'dir');
|
||||||
|
strictEqual(linkTextContents, './a.txt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should re-create FileBlob symlinks properly', async () => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
console.log('Skipping test on windows');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = {
|
||||||
|
'a.txt': new FileBlob({
|
||||||
|
mode: 33188,
|
||||||
|
contentType: undefined,
|
||||||
|
data: 'a text',
|
||||||
|
}),
|
||||||
|
'dir/b.txt': new FileBlob({
|
||||||
|
mode: 33188,
|
||||||
|
contentType: undefined,
|
||||||
|
data: 'b text',
|
||||||
|
}),
|
||||||
|
'link-dir': new FileBlob({
|
||||||
|
mode: 41453,
|
||||||
|
contentType: undefined,
|
||||||
|
data: 'dir',
|
||||||
|
}),
|
||||||
|
'link.txt': new FileBlob({
|
||||||
|
mode: 41453,
|
||||||
|
contentType: undefined,
|
||||||
|
data: 'a.txt',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
strictEqual(Object.keys(files).length, 4);
|
||||||
|
|
||||||
|
const outDir = path.join(__dirname, 'symlinks-out');
|
||||||
|
await fs.remove(outDir);
|
||||||
|
|
||||||
|
const files2 = await download(files, outDir);
|
||||||
|
strictEqual(Object.keys(files2).length, 4);
|
||||||
|
|
||||||
|
const [linkStat, linkDirStat, aStat, dirStat] = await Promise.all([
|
||||||
|
fs.lstat(path.join(outDir, 'link.txt')),
|
||||||
|
fs.lstat(path.join(outDir, 'link-dir')),
|
||||||
|
fs.lstat(path.join(outDir, 'a.txt')),
|
||||||
|
fs.lstat(path.join(outDir, 'dir')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert(linkStat.isSymbolicLink());
|
||||||
|
assert(linkDirStat.isSymbolicLink());
|
||||||
|
assert(aStat.isFile());
|
||||||
|
assert(dirStat.isDirectory());
|
||||||
|
|
||||||
|
const [linkDirContents, linkTextContents] = await Promise.all([
|
||||||
|
readlink(path.join(outDir, 'link-dir')),
|
||||||
|
readlink(path.join(outDir, 'link.txt')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
strictEqual(linkDirContents, 'dir');
|
||||||
|
strictEqual(linkTextContents, 'a.txt');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create zip files with symlinks properly', async () => {
|
it('should create zip files with symlinks properly', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user