@@ -11,6 +11,7 @@ import (
1111 "encoding/hex"
1212 "encoding/json"
1313 "fmt"
14+ "hash"
1415 "io"
1516 "io/ioutil"
1617 "os"
@@ -125,9 +126,17 @@ type FileBundle struct {
125126 Size * int64 `json:"size,omitempty"`
126127 FunctionMetadata * FunctionMetadata
127128
128- // Path OR Buffer should be populated
129- Path string
129+ // Path is the location of the file on disk. Uploads always stream from Path.
130+ Path string
131+
132+ // Deprecated: uploads always stream from Path; this package no longer reads Buffer. It is retained
133+ // only for backwards compatibility with external callers and may be removed in a future release.
134+ // Leave it nil to have the (also deprecated) Read/Seek/Close methods stream from Path instead.
130135 Buffer io.ReadSeeker
136+
137+ // pathReader is lazily opened from Path when Buffer is nil, so the deprecated Read/Seek/Close
138+ // methods keep working for external callers that treat a FileBundle as an io.ReadSeekCloser.
139+ pathReader * os.File
131140}
132141
133142type FunctionMetadata struct {
@@ -139,15 +148,46 @@ type toolchainSpec struct {
139148 Runtime string `json:"runtime"`
140149}
141150
151+ // Deprecated: read directly from Path (e.g. via os.Open) instead. When Buffer is set, Read reads
152+ // from it; otherwise it streams from Path. Retained for backwards compatibility with external
153+ // callers and may be removed in a future release.
142154func (f * FileBundle ) Read (p []byte ) (n int , err error ) {
143- return f .Buffer .Read (p )
155+ if f .Buffer != nil {
156+ return f .Buffer .Read (p )
157+ }
158+ if f .pathReader == nil {
159+ if f .pathReader , err = os .Open (f .Path ); err != nil {
160+ return 0 , err
161+ }
162+ }
163+ return f .pathReader .Read (p )
144164}
145165
166+ // Deprecated: read directly from Path (e.g. via os.Open) instead. When Buffer is set, Seek seeks
167+ // it; otherwise it seeks the stream opened from Path. Retained for backwards compatibility with
168+ // external callers and may be removed in a future release.
146169func (f * FileBundle ) Seek (offset int64 , whence int ) (int64 , error ) {
147- return f .Buffer .Seek (offset , whence )
170+ if f .Buffer != nil {
171+ return f .Buffer .Seek (offset , whence )
172+ }
173+ if f .pathReader == nil {
174+ var err error
175+ if f .pathReader , err = os .Open (f .Path ); err != nil {
176+ return 0 , err
177+ }
178+ }
179+ return f .pathReader .Seek (offset , whence )
148180}
149181
182+ // Deprecated: retained for backwards compatibility with external callers and may be removed in a
183+ // future release. It closes the stream lazily opened from Path by Read/Seek; it never closes a
184+ // caller-supplied Buffer.
150185func (f * FileBundle ) Close () error {
186+ if f .pathReader != nil {
187+ err := f .pathReader .Close ()
188+ f .pathReader = nil
189+ return err
190+ }
151191 return nil
152192}
153193
@@ -263,7 +303,13 @@ func (n *Netlify) DoDeploy(ctx context.Context, options *DeployOptions, deploy *
263303
264304 options .files = files
265305
266- functions , schedules , functionsConfig , err := bundle (ctx , options .FunctionsDir , options .Observer )
306+ // The temp dir is created lazily, only if a function actually needs to be zipped. Pre-bundled
307+ // .zip/.tar functions stream from their original path and never touch it, so a deploy with no
308+ // unbundled functions creates no temp dir at all.
309+ functionsTmpDir := & lazyTempDir {}
310+ defer functionsTmpDir .remove ()
311+
312+ functions , schedules , functionsConfig , err := bundle (ctx , options .FunctionsDir , functionsTmpDir , options .Observer )
267313 if err != nil {
268314 if options .Observer != nil {
269315 options .Observer .OnFailedWalk ()
@@ -456,6 +502,7 @@ func (n *Netlify) uploadFiles(ctx context.Context, d *models.Deploy, files *depl
456502 log := context .GetLogger (ctx )
457503 log .Infof ("Uploading %v files" , count )
458504
505+ var abortErr error
459506 for _ , sha := range required {
460507 if files , exist := files .Hashed [sha ]; exist {
461508 file := files [0 ]
@@ -466,7 +513,11 @@ func (n *Netlify) uploadFiles(ctx context.Context, d *models.Deploy, files *depl
466513 go n .uploadFile (ctx , d , file , observer , t , timeout , wg , sem , sharedErr , skipRetry )
467514 case <- ctx .Done ():
468515 log .Info ("Context terminated, aborting file upload" )
469- return errors .Wrap (ctx .Err (), "aborted file upload early" )
516+ abortErr = errors .Wrap (ctx .Err (), "aborted file upload early" )
517+ }
518+
519+ if abortErr != nil {
520+ break
470521 }
471522
472523 if len (files ) > 1 {
@@ -478,8 +529,16 @@ func (n *Netlify) uploadFiles(ctx context.Context, d *models.Deploy, files *depl
478529 }
479530 }
480531
532+ // Always wait for in-flight uploads to finish before returning. On the ctx.Done()
533+ // path this prevents orphaned uploadFile goroutines from racing against the caller's
534+ // deferred temp-dir cleanup (os.RemoveAll), which would otherwise open files that are
535+ // being deleted and surface spurious "no such file or directory" errors.
481536 wg .Wait ()
482537
538+ if abortErr != nil {
539+ return abortErr
540+ }
541+
483542 return sharedErr .err
484543}
485544
@@ -539,23 +598,25 @@ func (n *Netlify) uploadFile(ctx context.Context, d *models.Deploy, f *FileBundl
539598 _ , operationError = n .Operations .UploadDeployFile (params , authInfo )
540599 }
541600 case functionUpload :
542- params := operations .NewUploadDeployFunctionParams ().WithDeployID (d .ID ).WithName (f .Name ).WithFileBody (f ).WithRuntime (& f .Runtime )
601+ var body io.ReadCloser
602+ body , operationError = os .Open (f .Path )
603+ if operationError == nil {
604+ defer body .Close ()
605+ params := operations .NewUploadDeployFunctionParams ().WithDeployID (d .ID ).WithName (f .Name ).WithFileBody (body ).WithRuntime (& f .Runtime )
543606
544- if retryCount > 0 {
545- params = params .WithXNfRetryCount (& retryCount )
546- }
607+ if retryCount > 0 {
608+ params = params .WithXNfRetryCount (& retryCount )
609+ }
547610
548- if f .FunctionMetadata != nil {
549- params = params .WithInvocationMode (& f .FunctionMetadata .InvocationMode )
550- params = params .WithTimeout (& f .FunctionMetadata .Timeout )
551- }
611+ if f .FunctionMetadata != nil {
612+ params = params .WithInvocationMode (& f .FunctionMetadata .InvocationMode )
613+ params = params .WithTimeout (& f .FunctionMetadata .Timeout )
614+ }
552615
553- if timeout != 0 {
554- params .SetRequestTimeout (timeout )
555- }
556- _ , operationError = n .Operations .UploadDeployFunction (params , authInfo )
557- if operationError != nil {
558- f .Buffer .Seek (0 , 0 )
616+ if timeout != 0 {
617+ params .SetRequestTimeout (timeout )
618+ }
619+ _ , operationError = n .Operations .UploadDeployFunction (params , authInfo )
559620 }
560621 }
561622
@@ -601,6 +662,14 @@ func (n *Netlify) uploadFile(ctx context.Context, d *models.Deploy, f *FileBundl
601662}
602663
603664func createFileBundle (rel , path string ) (* FileBundle , error ) {
665+ return createFileBundleWithHasher (rel , path , sha1 .New ())
666+ }
667+
668+ func createFunctionFileBundle (rel , path string ) (* FileBundle , error ) {
669+ return createFileBundleWithHasher (rel , path , sha256 .New ())
670+ }
671+
672+ func createFileBundleWithHasher (rel , path string , s hash.Hash ) (* FileBundle , error ) {
604673 o , err := os .Open (path )
605674 if err != nil {
606675 return nil , err
@@ -612,7 +681,6 @@ func createFileBundle(rel, path string) (*FileBundle, error) {
612681 Path : path ,
613682 }
614683
615- s := sha1 .New ()
616684 if _ , err := io .Copy (s , o ); err != nil {
617685 return nil , err
618686 }
@@ -713,7 +781,30 @@ func addInternalFilesToDeploy(dir, internalPath string, files *deployFiles, obse
713781 })
714782}
715783
716- func bundle (ctx context.Context , functionDir string , observer DeployObserver ) (* deployFiles , []* models.FunctionSchedule , map [string ]models.FunctionConfig , error ) {
784+ type lazyTempDir struct {
785+ root string
786+ path string
787+ created bool
788+ }
789+
790+ func (l * lazyTempDir ) get () (string , error ) {
791+ if ! l .created {
792+ path , err := os .MkdirTemp (l .root , "netlify-deploy-functions-" )
793+ if err != nil {
794+ return "" , err
795+ }
796+ l .path , l .created = path , true
797+ }
798+ return l .path , nil
799+ }
800+
801+ func (l * lazyTempDir ) remove () {
802+ if l .created {
803+ os .RemoveAll (l .path )
804+ }
805+ }
806+
807+ func bundle (ctx context.Context , functionDir string , tmpDir * lazyTempDir , observer DeployObserver ) (* deployFiles , []* models.FunctionSchedule , map [string ]models.FunctionConfig , error ) {
717808 if functionDir == "" {
718809 return nil , nil , nil , nil
719810 }
@@ -725,7 +816,7 @@ func bundle(ctx context.Context, functionDir string, observer DeployObserver) (*
725816 if err == nil {
726817 defer manifestFile .Close ()
727818
728- return bundleFromManifest (ctx , manifestFile , observer )
819+ return bundleFromManifest (ctx , manifestFile , tmpDir , observer )
729820 }
730821
731822 functions := newDeployFiles ()
@@ -744,19 +835,19 @@ func bundle(ctx context.Context, functionDir string, observer DeployObserver) (*
744835 if err != nil {
745836 return nil , nil , nil , err
746837 }
747- file , err := newFunctionFile (filePath , i , runtime , nil , observer )
838+ file , err := newFunctionFile (filePath , i , runtime , nil , tmpDir , observer )
748839 if err != nil {
749840 return nil , nil , nil , err
750841 }
751842 functions .Add (file .Name , file )
752843 case jsFile (i ):
753- file , err := newFunctionFile (filePath , i , jsRuntime , nil , observer )
844+ file , err := newFunctionFile (filePath , i , jsRuntime , nil , tmpDir , observer )
754845 if err != nil {
755846 return nil , nil , nil , err
756847 }
757848 functions .Add (file .Name , file )
758849 case goFile (filePath , i , observer ):
759- file , err := newFunctionFile (filePath , i , amazonLinux2 , nil , observer )
850+ file , err := newFunctionFile (filePath , i , amazonLinux2 , nil , tmpDir , observer )
760851 if err != nil {
761852 return nil , nil , nil , err
762853 }
@@ -771,7 +862,7 @@ func bundle(ctx context.Context, functionDir string, observer DeployObserver) (*
771862 return functions , nil , nil , nil
772863}
773864
774- func bundleFromManifest (ctx context.Context , manifestFile * os.File , observer DeployObserver ) (* deployFiles , []* models.FunctionSchedule , map [string ]models.FunctionConfig , error ) {
865+ func bundleFromManifest (ctx context.Context , manifestFile * os.File , tmpDir * lazyTempDir , observer DeployObserver ) (* deployFiles , []* models.FunctionSchedule , map [string ]models.FunctionConfig , error ) {
775866 manifestBytes , err := ioutil .ReadAll (manifestFile )
776867 if err != nil {
777868 return nil , nil , nil , err
@@ -808,7 +899,7 @@ func bundleFromManifest(ctx context.Context, manifestFile *os.File, observer Dep
808899 InvocationMode : function .InvocationMode ,
809900 Timeout : function .Timeout ,
810901 }
811- file , err := newFunctionFile (function .Path , fileInfo , runtime , & meta , observer )
902+ file , err := newFunctionFile (function .Path , fileInfo , runtime , & meta , tmpDir , observer )
812903 if err != nil {
813904 return nil , nil , nil , err
814905 }
@@ -911,60 +1002,79 @@ func readZipRuntime(filePath string) (string, error) {
9111002 return jsRuntime , nil
9121003}
9131004
914- func newFunctionFile (filePath string , i os.FileInfo , runtime string , metadata * FunctionMetadata , observer DeployObserver ) (* FileBundle , error ) {
915- file := & FileBundle {
916- Name : strings .TrimSuffix (i .Name (), filepath .Ext (i .Name ())),
917- Runtime : runtime ,
918- }
919-
920- s := sha256 .New ()
1005+ func newFunctionFile (filePath string , i os.FileInfo , runtime string , metadata * FunctionMetadata , tmpDir * lazyTempDir , observer DeployObserver ) (* FileBundle , error ) {
1006+ var file * FileBundle
1007+ var err error
9211008
922- fileEntry , err := os .Open (filePath )
1009+ if zipFile (i ) || tarFile (i ) {
1010+ name := strings .TrimSuffix (i .Name (), filepath .Ext (i .Name ()))
1011+ file , err = createFunctionFileBundle (name , filePath )
1012+ } else {
1013+ file , err = zipFunctionFile (filePath , i , runtime , tmpDir )
1014+ }
9231015 if err != nil {
9241016 return nil , err
9251017 }
926- defer fileEntry .Close ()
9271018
928- var buf io.ReadWriter
929-
930- if zipFile (i ) || tarFile (i ) {
931- buf = fileEntry
932- } else {
933- buf = new (bytes.Buffer )
934- archive := zip .NewWriter (buf )
1019+ file .Runtime = runtime
1020+ file .FunctionMetadata = metadata
9351021
936- fileHeader , err := createHeader ( archive , i , runtime )
937- if err != nil {
1022+ if observer != nil {
1023+ if err := observer . OnSuccessfulStep ( file ); err != nil {
9381024 return nil , err
9391025 }
1026+ }
9401027
941- if _ , err = io .Copy (fileHeader , fileEntry ); err != nil {
942- return nil , err
943- }
1028+ return file , nil
1029+ }
9441030
945- if err := archive .Close (); err != nil {
946- return nil , err
947- }
1031+ func zipFunctionFile (filePath string , i os.FileInfo , runtime string , tmpDir * lazyTempDir ) (* FileBundle , error ) {
1032+ src , err := os .Open (filePath )
1033+ if err != nil {
1034+ return nil , err
9481035 }
1036+ defer src .Close ()
9491037
950- fileBuffer := new (bytes.Buffer )
951- m := io .MultiWriter (s , fileBuffer )
952-
953- if _ , err := io .Copy (m , buf ); err != nil {
1038+ dir , err := tmpDir .get ()
1039+ if err != nil {
9541040 return nil , err
9551041 }
956- file .Sum = hex .EncodeToString (s .Sum (nil ))
957- file .Buffer = bytes .NewReader (fileBuffer .Bytes ())
9581042
959- if observer != nil {
960- if err := observer .OnSuccessfulStep (file ); err != nil {
961- return nil , err
1043+ tmp , err := os .CreateTemp (dir , "function-*.zip" )
1044+ if err != nil {
1045+ return nil , err
1046+ }
1047+ defer func () {
1048+ if tmp != nil {
1049+ _ = tmp .Close ()
9621050 }
1051+ }()
1052+
1053+ s := sha256 .New ()
1054+ archive := zip .NewWriter (io .MultiWriter (tmp , s ))
1055+
1056+ fileHeader , err := createHeader (archive , i , runtime )
1057+ if err != nil {
1058+ return nil , err
1059+ }
1060+ if _ , err := io .Copy (fileHeader , src ); err != nil {
1061+ return nil , err
1062+ }
1063+ if err := archive .Close (); err != nil {
1064+ return nil , err
9631065 }
9641066
965- file .FunctionMetadata = metadata
1067+ tmpName := tmp .Name ()
1068+ if err := tmp .Close (); err != nil {
1069+ return nil , err
1070+ }
1071+ tmp = nil
9661072
967- return file , nil
1073+ return & FileBundle {
1074+ Name : strings .TrimSuffix (i .Name (), filepath .Ext (i .Name ())),
1075+ Sum : hex .EncodeToString (s .Sum (nil )),
1076+ Path : tmpName ,
1077+ }, nil
9681078}
9691079
9701080func zipFile (i os.FileInfo ) bool {
0 commit comments