@@ -25,63 +25,115 @@ import (
2525 "strings"
2626)
2727
28- // Tar tars the target file and return the content by stream.
29- func Tar (path string ) (io.Reader , error ) {
28+ // Tar creates a tar archive of the specified path (file or directory)
29+ // and returns the content as a stream. For individual files, it preserves
30+ // the directory structure relative to the working directory.
31+ func Tar (srcPath string , workDir string ) (io.Reader , error ) {
3032 pr , pw := io .Pipe ()
33+
3134 go func () {
3235 defer pw .Close ()
33- // create the tar writer.
3436 tw := tar .NewWriter (pw )
3537 defer tw .Close ()
3638
37- file , err := os .Open ( path )
39+ info , err := os .Stat ( srcPath )
3840 if err != nil {
39- pw .CloseWithError (fmt .Errorf ("failed to open file : %w" , err ))
41+ pw .CloseWithError (fmt .Errorf ("failed to stat source path : %w" , err ))
4042 return
4143 }
4244
43- defer file .Close ()
44- info , err := file .Stat ()
45- if err != nil {
46- pw .CloseWithError (fmt .Errorf ("failed to stat file: %w" , err ))
47- return
48- }
45+ // Handle directories and files differently.
46+ if info .IsDir () {
47+ // For directories, walk through and add all files/subdirs.
48+ err = filepath .Walk (srcPath , func (path string , info os.FileInfo , err error ) error {
49+ if err != nil {
50+ return err
51+ }
52+
53+ // Create a relative path for the tar file header.
54+ relPath , err := filepath .Rel (workDir , path )
55+ if err != nil {
56+ return fmt .Errorf ("failed to get relative path: %w" , err )
57+ }
58+
59+ header , err := tar .FileInfoHeader (info , "" )
60+ if err != nil {
61+ return fmt .Errorf ("failed to create tar header: %w" , err )
62+ }
63+
64+ // Set the header name to preserve directory structure.
65+ header .Name = relPath
66+ if err := tw .WriteHeader (header ); err != nil {
67+ return fmt .Errorf ("failed to write header: %w" , err )
68+ }
69+
70+ if ! info .IsDir () {
71+ file , err := os .Open (path )
72+ if err != nil {
73+ return fmt .Errorf ("failed to open file %s: %w" , path , err )
74+ }
75+ defer file .Close ()
76+
77+ if _ , err := io .Copy (tw , file ); err != nil {
78+ return fmt .Errorf ("failed to write file %s to tar: %w" , path , err )
79+ }
80+ }
81+
82+ return nil
83+ })
4984
50- header , err := tar .FileInfoHeader (info , info .Name ())
51- if err != nil {
52- pw .CloseWithError (fmt .Errorf ("failed to create tar file info header: %w" , err ))
53- return
54- }
85+ if err != nil {
86+ pw .CloseWithError (fmt .Errorf ("failed to walk directory: %w" , err ))
87+ return
88+ }
89+ } else {
90+ // For a single file, include the directory structure.
91+ file , err := os .Open (srcPath )
92+ if err != nil {
93+ pw .CloseWithError (fmt .Errorf ("failed to open file: %w" , err ))
94+ return
95+ }
96+ defer file .Close ()
5597
56- if err := tw .WriteHeader (header ); err != nil {
57- pw .CloseWithError (fmt .Errorf ("failed to write header to tar writer: %w" , err ))
58- return
59- }
98+ header , err := tar .FileInfoHeader (info , "" )
99+ if err != nil {
100+ pw .CloseWithError (fmt .Errorf ("failed to create tar header: %w" , err ))
101+ return
102+ }
60103
61- _ , err = io .Copy (tw , file )
62- if err != nil {
63- pw .CloseWithError (fmt .Errorf ("failed to copy file to tar writer: %w" , err ))
64- return
104+ // Use relative path as the header name to preserve directory structure
105+ // This keeps the directory structure as part of the file path in the tar.
106+ relPath , err := filepath .Rel (workDir , srcPath )
107+ if err != nil {
108+ pw .CloseWithError (fmt .Errorf ("failed to get relative path: %w" , err ))
109+ return
110+ }
111+
112+ // Use the relative path (including directories) as the header name.
113+ header .Name = relPath
114+ if err := tw .WriteHeader (header ); err != nil {
115+ pw .CloseWithError (fmt .Errorf ("failed to write header: %w" , err ))
116+ return
117+ }
118+
119+ if _ , err := io .Copy (tw , file ); err != nil {
120+ pw .CloseWithError (fmt .Errorf ("failed to copy file to tar: %w" , err ))
121+ return
122+ }
65123 }
66124 }()
67125
68126 return pr , nil
69127}
70128
71- // Untar untars the target stream to the destination path.
129+ // Untar extracts the contents of a tar archive from the provided reader
130+ // to the specified destination path.
72131func Untar (reader io.Reader , destPath string ) error {
73- // uncompress gzip if it is a .tar.gz file
74- // gzipReader, err := gzip.NewReader(reader)
75- // if err != nil {
76- // return err
77- // }
78- // defer gzipReader.Close()
79- // tarReader := tar.NewReader(gzipReader)
80-
81132 tarReader := tar .NewReader (reader )
82133
134+ // Ensure destination directory exists.
83135 if err := os .MkdirAll (destPath , 0755 ); err != nil {
84- return err
136+ return fmt . Errorf ( "failed to create destination directory: %w" , err )
85137 }
86138
87139 for {
@@ -90,39 +142,74 @@ func Untar(reader io.Reader, destPath string) error {
90142 break
91143 }
92144 if err != nil {
93- return err
145+ return fmt . Errorf ( "error reading tar: %w" , err )
94146 }
95147
96- // sanitize filepaths to prevent directory traversal.
148+ // Sanitize file paths to prevent directory traversal.
97149 cleanPath := filepath .Clean (header .Name )
98- if strings .Contains (cleanPath , ".." ) {
150+ if strings .Contains (cleanPath , ".." ) || strings . HasPrefix ( cleanPath , "/" ) || strings . HasPrefix ( cleanPath , ": \\ " ) {
99151 return fmt .Errorf ("tar file contains invalid path: %s" , cleanPath )
100152 }
101153
102- path := filepath .Join (destPath , cleanPath )
103- // check the file type.
154+ targetPath := filepath .Join (destPath , cleanPath )
155+
156+ // Create directories for all path components.
157+ dirPath := filepath .Dir (targetPath )
158+ if err := os .MkdirAll (dirPath , 0755 ); err != nil {
159+ return fmt .Errorf ("failed to create directory %s: %w" , dirPath , err )
160+ }
161+
104162 switch header .Typeflag {
105163 case tar .TypeDir :
106- if err := os .MkdirAll (path , 0755 ); err != nil {
107- return err
164+ if err := os .MkdirAll (targetPath , os . FileMode ( header . Mode ) ); err != nil {
165+ return fmt . Errorf ( "failed to create directory %s: %w" , targetPath , err )
108166 }
167+
109168 case tar .TypeReg :
110- file , err := os .Create (path )
169+ file , err := os .OpenFile (
170+ targetPath ,
171+ os .O_CREATE | os .O_RDWR | os .O_TRUNC ,
172+ os .FileMode (header .Mode ),
173+ )
111174 if err != nil {
112- return err
175+ return fmt . Errorf ( "failed to create file %s: %w" , targetPath , err )
113176 }
114177
115178 if _ , err := io .Copy (file , tarReader ); err != nil {
116179 file .Close ()
117- return err
180+ return fmt . Errorf ( "failed to write to file %s: %w" , targetPath , err )
118181 }
119182 file .Close ()
120183
121- if err := os .Chmod (path , os .FileMode (header .Mode )); err != nil {
122- return err
184+ case tar .TypeSymlink :
185+ if isRel (header .Linkname , destPath ) && isRel (header .Name , destPath ) {
186+ if err := os .Symlink (header .Linkname , targetPath ); err != nil {
187+ return fmt .Errorf ("failed to create symlink %s -> %s: %w" , targetPath , header .Linkname , err )
188+ }
189+ } else {
190+ return fmt .Errorf ("symlink %s -> %s points outside of destination directory" , targetPath , header .Linkname )
123191 }
192+
193+ default :
194+ // Skip other types.
195+ continue
124196 }
125197 }
126198
127199 return nil
128200}
201+
202+ // isRel checks if the candidate path is within the target directory after resolving symbolic links.
203+ func isRel (candidate , target string ) bool {
204+ if filepath .IsAbs (candidate ) {
205+ return false
206+ }
207+
208+ realpath , err := filepath .EvalSymlinks (filepath .Join (target , candidate ))
209+ if err != nil {
210+ return false
211+ }
212+
213+ relpath , err := filepath .Rel (target , realpath )
214+ return err == nil && ! strings .HasPrefix (filepath .Clean (relpath ), ".." )
215+ }
0 commit comments