@@ -21,8 +21,9 @@ export async function calculateFilesHash(
2121 src : string ,
2222 dest : string ,
2323 contextPath : string ,
24- ignorePatterns ?: string [ ] ,
25- stackTrace ?: string
24+ ignorePatterns : string [ ] ,
25+ resolveSymlinks : boolean ,
26+ stackTrace : string | undefined
2627) : Promise < string > {
2728 const { glob } = await dynamicGlob ( )
2829 const srcPath = path . join ( contextPath , src )
@@ -44,13 +45,47 @@ export async function calculateFilesHash(
4445 throw error
4546 }
4647
48+ // Hash stats
49+ const hashStats = ( stats : fs . Stats ) => {
50+ hash . update ( stats . mode . toString ( ) )
51+ hash . update ( stats . uid . toString ( ) )
52+ hash . update ( stats . gid . toString ( ) )
53+ hash . update ( stats . size . toString ( ) )
54+ hash . update ( stats . mtimeMs . toString ( ) )
55+ }
56+
4757 for ( const file of files ) {
48- if ( ! file . isFile ( ) ) {
49- continue
58+ // Add a relative path to hash calculation
59+ const relativePath = path . relative ( contextPath , file . fullpath ( ) )
60+ hash . update ( relativePath )
61+
62+ // Add stat information to hash calculation
63+ if ( file . isSymbolicLink ( ) ) {
64+ // If the symlink is broken, it will return undefined, otherwise it will return a stats object of the target
65+ const stats = fs . statSync ( file . fullpath ( ) , { throwIfNoEntry : false } )
66+ const shouldFollow =
67+ resolveSymlinks && ( stats ?. isFile ( ) || stats ?. isDirectory ( ) )
68+
69+ if ( ! shouldFollow ) {
70+ const stats = fs . lstatSync ( file . fullpath ( ) )
71+
72+ hashStats ( stats )
73+
74+ const content = fs . readlinkSync ( file . fullpath ( ) )
75+ hash . update ( content )
76+
77+ continue
78+ }
5079 }
5180
52- const content = fs . readFileSync ( file . fullpath ( ) )
53- hash . update ( new Uint8Array ( content ) )
81+ const stats = fs . statSync ( file . fullpath ( ) )
82+
83+ hashStats ( stats )
84+
85+ if ( stats . isFile ( ) ) {
86+ const content = fs . readFileSync ( file . fullpath ( ) )
87+ hash . update ( new Uint8Array ( content ) )
88+ }
5489 }
5590
5691 return hash . digest ( 'hex' )
@@ -105,34 +140,48 @@ export function padOctal(mode: number): string {
105140 return mode . toString ( 8 ) . padStart ( 4 , '0' )
106141}
107142
108- export async function tarFileStream ( fileName : string , fileContextPath : string ) {
143+ export async function tarFileStream (
144+ fileName : string ,
145+ fileContextPath : string ,
146+ resolveSymlinks : boolean
147+ ) {
109148 const { globSync } = await dynamicGlob ( )
110149 const { create } = await dynamicTar ( )
111- const files = globSync ( fileName , { cwd : fileContextPath , nodir : false } )
150+ const files = globSync ( fileName , { cwd : fileContextPath } )
112151
113152 return create (
114153 {
115154 gzip : true ,
116155 cwd : fileContextPath ,
156+ follow : resolveSymlinks ,
117157 } ,
118158 files
119159 )
120160}
121161
122162export async function tarFileStreamUpload (
123163 fileName : string ,
124- fileContextPath : string
164+ fileContextPath : string ,
165+ resolveSymlinks : boolean
125166) {
126167 // First pass: calculate the compressed size without buffering
127- const sizeCalculationStream = await tarFileStream ( fileName , fileContextPath )
168+ const sizeCalculationStream = await tarFileStream (
169+ fileName ,
170+ fileContextPath ,
171+ resolveSymlinks
172+ )
128173 let contentLength = 0
129174 for await ( const chunk of sizeCalculationStream as unknown as AsyncIterable < Buffer > ) {
130175 contentLength += chunk . length
131176 }
132177
133178 return {
134179 contentLength,
135- uploadStream : await tarFileStream ( fileName , fileContextPath ) ,
180+ uploadStream : await tarFileStream (
181+ fileName ,
182+ fileContextPath ,
183+ resolveSymlinks
184+ ) ,
136185 }
137186}
138187
0 commit comments