@@ -3,6 +3,11 @@ import fs from './wrapped-fs';
33import { Pickle } from './pickle' ;
44import { Filesystem , FilesystemFileEntry } from './filesystem' ;
55import { CrawledFileType } from './crawlfs' ;
6+ import { Stats } from 'fs' ;
7+ import { promisify } from 'util' ;
8+ import * as stream from 'stream' ;
9+
10+ const pipeline = promisify ( stream . pipeline ) ;
611
712let filesystemCache : Record < string , Filesystem | undefined > = Object . create ( null ) ;
813
@@ -19,12 +24,10 @@ async function copyFile(dest: string, src: string, filename: string) {
1924}
2025
2126async function streamTransformedFile (
22- originalFilename : string ,
27+ stream : NodeJS . ReadableStream ,
2328 outStream : NodeJS . WritableStream ,
24- transformed : CrawledFileType [ 'transformed' ] ,
2529) {
2630 return new Promise < void > ( ( resolve , reject ) => {
27- const stream = fs . createReadStream ( transformed ? transformed . path : originalFilename ) ;
2831 stream . pipe ( outStream , { end : false } ) ;
2932 stream . on ( 'error' , reject ) ;
3033 stream . on ( 'end' , ( ) => resolve ( ) ) ;
@@ -35,15 +38,29 @@ export type InputMetadata = {
3538 [ property : string ] : CrawledFileType ;
3639} ;
3740
38- export type BasicFilesArray = { filename : string ; unpack : boolean } [ ] ;
39-
40- export type FilesystemFilesAndLinks = { files : BasicFilesArray ; links : BasicFilesArray } ;
41+ export type BasicFilesArray = {
42+ filename : string ;
43+ unpack : boolean ;
44+ } [ ] ;
45+
46+ export type BasicStreamArray = {
47+ filename : string ;
48+ streamGenerator : ( ) => NodeJS . ReadableStream ; // this is called multiple times per file
49+ mode : Stats [ 'mode' ] ;
50+ unpack : boolean ;
51+ link : string | undefined ; // only for symlinks, should refactor as part of larger project refactor in follow-up PR
52+ } [ ] ;
53+
54+ export type FilesystemFilesAndLinks < T extends BasicFilesArray | BasicStreamArray > = {
55+ files : T ;
56+ links : T ;
57+ } ;
4158
4259const writeFileListToStream = async function (
4360 dest : string ,
4461 filesystem : Filesystem ,
4562 out : NodeJS . WritableStream ,
46- lists : FilesystemFilesAndLinks ,
63+ lists : FilesystemFilesAndLinks < BasicFilesArray > ,
4764 metadata : InputMetadata ,
4865) {
4966 const { files, links } = lists ;
@@ -53,50 +70,55 @@ const writeFileListToStream = async function (
5370 const filename = path . relative ( filesystem . getRootPath ( ) , file . filename ) ;
5471 await copyFile ( `${ dest } .unpacked` , filesystem . getRootPath ( ) , filename ) ;
5572 } else {
56- await streamTransformedFile ( file . filename , out , metadata [ file . filename ] . transformed ) ;
73+ const transformed = metadata [ file . filename ] . transformed ;
74+ const stream = fs . createReadStream ( transformed ? transformed . path : file . filename ) ;
75+ await streamTransformedFile ( stream , out ) ;
5776 }
5877 }
59- const unpackedSymlinks = links . filter ( ( f ) => f . unpack ) ;
60- for ( const file of unpackedSymlinks ) {
78+ for ( const file of links . filter ( ( f ) => f . unpack ) ) {
6179 // the symlink needs to be recreated outside in .unpacked
6280 const filename = path . relative ( filesystem . getRootPath ( ) , file . filename ) ;
6381 const link = await fs . readlink ( file . filename ) ;
64- // if symlink is within subdirectories, then we need to recreate dir structure
65- await fs . mkdirp ( path . join ( `${ dest } .unpacked` , path . dirname ( filename ) ) ) ;
66- // create symlink within unpacked dir
67- await fs . symlink ( link , path . join ( `${ dest } .unpacked` , filename ) ) . catch ( async ( error ) => {
68- if ( error . code === 'EPERM' && error . syscall === 'symlink' ) {
69- throw new Error (
70- 'Could not create symlinks for unpacked assets. On Windows, consider activating Developer Mode to allow non-admin users to create symlinks by following the instructions at https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development.' ,
71- ) ;
72- }
73- throw error ;
74- } ) ;
82+ await createSymlink ( dest , filename , link ) ;
7583 }
7684 return out . end ( ) ;
7785} ;
7886
7987export async function writeFilesystem (
8088 dest : string ,
8189 filesystem : Filesystem ,
82- lists : FilesystemFilesAndLinks ,
90+ lists : FilesystemFilesAndLinks < BasicFilesArray > ,
8391 metadata : InputMetadata ,
8492) {
85- const headerPickle = Pickle . createEmpty ( ) ;
86- headerPickle . writeString ( JSON . stringify ( filesystem . getHeader ( ) ) ) ;
87- const headerBuf = headerPickle . toBuffer ( ) ;
93+ const out = await createFilesystemWriteStream ( filesystem , dest ) ;
94+ return writeFileListToStream ( dest , filesystem , out , lists , metadata ) ;
95+ }
8896
89- const sizePickle = Pickle . createEmpty ( ) ;
90- sizePickle . writeUInt32 ( headerBuf . length ) ;
91- const sizeBuf = sizePickle . toBuffer ( ) ;
97+ export async function streamFilesystem (
98+ dest : string ,
99+ filesystem : Filesystem ,
100+ lists : FilesystemFilesAndLinks < BasicStreamArray > ,
101+ ) {
102+ const out = await createFilesystemWriteStream ( filesystem , dest ) ;
92103
93- const out = fs . createWriteStream ( dest ) ;
94- await new Promise < void > ( ( resolve , reject ) => {
95- out . on ( 'error' , reject ) ;
96- out . write ( sizeBuf ) ;
97- return out . write ( headerBuf , ( ) => resolve ( ) ) ;
98- } ) ;
99- return writeFileListToStream ( dest , filesystem , out , lists , metadata ) ;
104+ const { files, links } = lists ;
105+ for await ( const file of files ) {
106+ // the file should not be packed into archive
107+ if ( file . unpack ) {
108+ const targetFile = path . join ( `${ dest } .unpacked` , file . filename ) ;
109+ await fs . mkdirp ( path . dirname ( targetFile ) ) ;
110+ const writeStream = fs . createWriteStream ( targetFile , { mode : file . mode } ) ;
111+ await pipeline ( file . streamGenerator ( ) , writeStream ) ;
112+ } else {
113+ await streamTransformedFile ( file . streamGenerator ( ) , out ) ;
114+ }
115+ }
116+
117+ for ( const file of links . filter ( ( f ) => f . unpack && f . link ) ) {
118+ // the symlink needs to be recreated outside in .unpacked
119+ await createSymlink ( dest , file . filename , file . link ! ) ;
120+ }
121+ return out . end ( ) ;
100122}
101123
102124export interface FileRecord extends FilesystemFileEntry {
@@ -187,3 +209,35 @@ export function readFileSync(filesystem: Filesystem, filename: string, info: Fil
187209 }
188210 return buffer ;
189211}
212+
213+ async function createFilesystemWriteStream ( filesystem : Filesystem , dest : string ) {
214+ const headerPickle = Pickle . createEmpty ( ) ;
215+ headerPickle . writeString ( JSON . stringify ( filesystem . getHeader ( ) ) ) ;
216+ const headerBuf = headerPickle . toBuffer ( ) ;
217+
218+ const sizePickle = Pickle . createEmpty ( ) ;
219+ sizePickle . writeUInt32 ( headerBuf . length ) ;
220+ const sizeBuf = sizePickle . toBuffer ( ) ;
221+
222+ const out = fs . createWriteStream ( dest ) ;
223+ await new Promise < void > ( ( resolve , reject ) => {
224+ out . on ( 'error' , reject ) ;
225+ out . write ( sizeBuf ) ;
226+ return out . write ( headerBuf , ( ) => resolve ( ) ) ;
227+ } ) ;
228+ return out ;
229+ }
230+
231+ async function createSymlink ( dest : string , filepath : string , link : string ) {
232+ // if symlink is within subdirectories, then we need to recreate dir structure
233+ await fs . mkdirp ( path . join ( `${ dest } .unpacked` , path . dirname ( filepath ) ) ) ;
234+ // create symlink within unpacked dir
235+ await fs . symlink ( link , path . join ( `${ dest } .unpacked` , filepath ) ) . catch ( async ( error ) => {
236+ if ( error . code === 'EPERM' && error . syscall === 'symlink' ) {
237+ throw new Error (
238+ 'Could not create symlinks for unpacked assets. On Windows, consider activating Developer Mode to allow non-admin users to create symlinks by following the instructions at https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development.' ,
239+ ) ;
240+ }
241+ throw error ;
242+ } ) ;
243+ }
0 commit comments