Browse code

Merge pull request #3682 from alexlarsson/implement-tar

Implement tar in Go

Michael Crosby authored on 2014/01/23 04:23:47
Showing 7 changed files
... ...
@@ -23,10 +23,7 @@ type Compression int
23 23
 
24 24
 type TarOptions struct {
25 25
 	Includes    []string
26
-	Excludes    []string
27
-	Recursive   bool
28 26
 	Compression Compression
29
-	CreateFiles []string
30 27
 }
31 28
 
32 29
 const (
... ...
@@ -66,7 +63,7 @@ func DetectCompression(source []byte) Compression {
66 66
 func xzDecompress(archive io.Reader) (io.Reader, error) {
67 67
 	args := []string{"xz", "-d", "-c", "-q"}
68 68
 
69
-	return CmdStream(exec.Command(args[0], args[1:]...), archive, nil)
69
+	return CmdStream(exec.Command(args[0], args[1:]...), archive)
70 70
 }
71 71
 
72 72
 func DecompressStream(archive io.Reader) (io.Reader, error) {
... ...
@@ -100,16 +97,20 @@ func DecompressStream(archive io.Reader) (io.Reader, error) {
100 100
 	}
101 101
 }
102 102
 
103
-func (compression *Compression) Flag() string {
104
-	switch *compression {
105
-	case Bzip2:
106
-		return "j"
103
+func CompressStream(dest io.WriteCloser, compression Compression) (io.WriteCloser, error) {
104
+
105
+	switch compression {
106
+	case Uncompressed:
107
+		return dest, nil
107 108
 	case Gzip:
108
-		return "z"
109
-	case Xz:
110
-		return "J"
109
+		return gzip.NewWriter(dest), nil
110
+	case Bzip2, Xz:
111
+		// archive/bzip2 does not support writing, and there is no xz support at all
112
+		// However, this is not a problem as docker only currently generates gzipped tars
113
+		return nil, fmt.Errorf("Unsupported compression format %s", (&compression).Extension())
114
+	default:
115
+		return nil, fmt.Errorf("Unsupported compression format %s", (&compression).Extension())
111 116
 	}
112
-	return ""
113 117
 }
114 118
 
115 119
 func (compression *Compression) Extension() string {
... ...
@@ -126,6 +127,59 @@ func (compression *Compression) Extension() string {
126 126
 	return ""
127 127
 }
128 128
 
129
+func addTarFile(path, name string, tw *tar.Writer) error {
130
+	fi, err := os.Lstat(path)
131
+	if err != nil {
132
+		return err
133
+	}
134
+
135
+	link := ""
136
+	if fi.Mode()&os.ModeSymlink != 0 {
137
+		if link, err = os.Readlink(path); err != nil {
138
+			return err
139
+		}
140
+	}
141
+
142
+	hdr, err := tar.FileInfoHeader(fi, link)
143
+	if err != nil {
144
+		return err
145
+	}
146
+
147
+	if fi.IsDir() && !strings.HasSuffix(name, "/") {
148
+		name = name + "/"
149
+	}
150
+
151
+	hdr.Name = name
152
+
153
+	stat, ok := fi.Sys().(*syscall.Stat_t)
154
+	if ok {
155
+		// Currently go does not fill in the major/minors
156
+		if stat.Mode&syscall.S_IFBLK == syscall.S_IFBLK ||
157
+			stat.Mode&syscall.S_IFCHR == syscall.S_IFCHR {
158
+			hdr.Devmajor = int64(major(uint64(stat.Rdev)))
159
+			hdr.Devminor = int64(minor(uint64(stat.Rdev)))
160
+		}
161
+	}
162
+
163
+	if err := tw.WriteHeader(hdr); err != nil {
164
+		return err
165
+	}
166
+
167
+	if hdr.Typeflag == tar.TypeReg {
168
+		if file, err := os.Open(path); err != nil {
169
+			return err
170
+		} else {
171
+			_, err := io.Copy(tw, file)
172
+			if err != nil {
173
+				return err
174
+			}
175
+			file.Close()
176
+		}
177
+	}
178
+
179
+	return nil
180
+}
181
+
129 182
 func createTarFile(path, extractDir string, hdr *tar.Header, reader *tar.Reader) error {
130 183
 	switch hdr.Typeflag {
131 184
 	case tar.TypeDir:
... ...
@@ -207,7 +261,7 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader *tar.Reader)
207 207
 // Tar creates an archive from the directory at `path`, and returns it as a
208 208
 // stream of bytes.
209 209
 func Tar(path string, compression Compression) (io.Reader, error) {
210
-	return TarFilter(path, &TarOptions{Recursive: true, Compression: compression})
210
+	return TarFilter(path, &TarOptions{Compression: compression})
211 211
 }
212 212
 
213 213
 func escapeName(name string) string {
... ...
@@ -228,57 +282,55 @@ func escapeName(name string) string {
228 228
 
229 229
 // Tar creates an archive from the directory at `path`, only including files whose relative
230 230
 // paths are included in `filter`. If `filter` is nil, then all files are included.
231
-func TarFilter(path string, options *TarOptions) (io.Reader, error) {
232
-	args := []string{"tar", "--numeric-owner", "-f", "-", "-C", path, "-T", "-"}
233
-	if options.Includes == nil {
234
-		options.Includes = []string{"."}
235
-	}
236
-	args = append(args, "-c"+options.Compression.Flag())
231
+func TarFilter(srcPath string, options *TarOptions) (io.Reader, error) {
232
+	pipeReader, pipeWriter := io.Pipe()
237 233
 
238
-	for _, exclude := range options.Excludes {
239
-		args = append(args, fmt.Sprintf("--exclude=%s", exclude))
234
+	compressWriter, err := CompressStream(pipeWriter, options.Compression)
235
+	if err != nil {
236
+		return nil, err
240 237
 	}
241 238
 
242
-	if !options.Recursive {
243
-		args = append(args, "--no-recursion")
244
-	}
239
+	tw := tar.NewWriter(compressWriter)
245 240
 
246
-	files := ""
247
-	for _, f := range options.Includes {
248
-		files = files + escapeName(f) + "\n"
249
-	}
250
-
251
-	tmpDir := ""
241
+	go func() {
242
+		// In general we log errors here but ignore them because
243
+		// during e.g. a diff operation the container can continue
244
+		// mutating the filesystem and we can see transient errors
245
+		// from this
252 246
 
253
-	if options.CreateFiles != nil {
254
-		var err error // Can't use := here or we override the outer tmpDir
255
-		tmpDir, err = ioutil.TempDir("", "docker-tar")
256
-		if err != nil {
257
-			return nil, err
247
+		if options.Includes == nil {
248
+			options.Includes = []string{"."}
258 249
 		}
259 250
 
260
-		files = files + "-C" + tmpDir + "\n"
261
-		for _, f := range options.CreateFiles {
262
-			path := filepath.Join(tmpDir, f)
263
-			err := os.MkdirAll(filepath.Dir(path), 0600)
264
-			if err != nil {
265
-				return nil, err
266
-			}
251
+		for _, include := range options.Includes {
252
+			filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
253
+				if err != nil {
254
+					utils.Debugf("Tar: Can't stat file %s to tar: %s\n", srcPath, err)
255
+					return nil
256
+				}
267 257
 
268
-			if file, err := os.OpenFile(path, os.O_CREATE, 0600); err != nil {
269
-				return nil, err
270
-			} else {
271
-				file.Close()
272
-			}
273
-			files = files + escapeName(f) + "\n"
258
+				relFilePath, err := filepath.Rel(srcPath, filePath)
259
+				if err != nil {
260
+					return nil
261
+				}
262
+
263
+				if err := addTarFile(filePath, relFilePath, tw); err != nil {
264
+					utils.Debugf("Can't add file %s to tar: %s\n", srcPath, err)
265
+				}
266
+				return nil
267
+			})
274 268
 		}
275
-	}
276 269
 
277
-	return CmdStream(exec.Command(args[0], args[1:]...), bytes.NewBufferString(files), func() {
278
-		if tmpDir != "" {
279
-			_ = os.RemoveAll(tmpDir)
270
+		// Make sure to check the error on Close.
271
+		if err := tw.Close(); err != nil {
272
+			utils.Debugf("Can't close tar writer: %s\n", err)
280 273
 		}
281
-	})
274
+		if err := compressWriter.Close(); err != nil {
275
+			utils.Debugf("Can't close compress writer: %s\n", err)
276
+		}
277
+	}()
278
+
279
+	return pipeReader, nil
282 280
 }
283 281
 
284 282
 // Untar reads a stream of bytes from `archive`, parses it as a tar archive,
... ...
@@ -311,19 +363,6 @@ func Untar(archive io.Reader, dest string, options *TarOptions) error {
311 311
 			return err
312 312
 		}
313 313
 
314
-		if options != nil {
315
-			excludeFile := false
316
-			for _, exclude := range options.Excludes {
317
-				if strings.HasPrefix(hdr.Name, exclude) {
318
-					excludeFile = true
319
-					break
320
-				}
321
-			}
322
-			if excludeFile {
323
-				continue
324
-			}
325
-		}
326
-
327 314
 		// Normalize name, for safety and for a simple is-root check
328 315
 		hdr.Name = filepath.Clean(hdr.Name)
329 316
 
... ...
@@ -378,9 +417,9 @@ func Untar(archive io.Reader, dest string, options *TarOptions) error {
378 378
 // TarUntar is a convenience function which calls Tar and Untar, with
379 379
 // the output of one piped into the other. If either Tar or Untar fails,
380 380
 // TarUntar aborts and returns the error.
381
-func TarUntar(src string, filter []string, dst string) error {
382
-	utils.Debugf("TarUntar(%s %s %s)", src, filter, dst)
383
-	archive, err := TarFilter(src, &TarOptions{Compression: Uncompressed, Includes: filter, Recursive: true})
381
+func TarUntar(src string, dst string) error {
382
+	utils.Debugf("TarUntar(%s %s)", src, dst)
383
+	archive, err := TarFilter(src, &TarOptions{Compression: Uncompressed})
384 384
 	if err != nil {
385 385
 		return err
386 386
 	}
... ...
@@ -417,7 +456,7 @@ func CopyWithTar(src, dst string) error {
417 417
 		return err
418 418
 	}
419 419
 	utils.Debugf("Calling TarUntar(%s, %s)", src, dst)
420
-	return TarUntar(src, nil, dst)
420
+	return TarUntar(src, dst)
421 421
 }
422 422
 
423 423
 // CopyFileWithTar emulates the behavior of the 'cp' command-line
... ...
@@ -480,13 +519,10 @@ func CopyFileWithTar(src, dst string) (err error) {
480 480
 // CmdStream executes a command, and returns its stdout as a stream.
481 481
 // If the command fails to run or doesn't complete successfully, an error
482 482
 // will be returned, including anything written on stderr.
483
-func CmdStream(cmd *exec.Cmd, input io.Reader, atEnd func()) (io.Reader, error) {
483
+func CmdStream(cmd *exec.Cmd, input io.Reader) (io.Reader, error) {
484 484
 	if input != nil {
485 485
 		stdin, err := cmd.StdinPipe()
486 486
 		if err != nil {
487
-			if atEnd != nil {
488
-				atEnd()
489
-			}
490 487
 			return nil, err
491 488
 		}
492 489
 		// Write stdin if any
... ...
@@ -497,16 +533,10 @@ func CmdStream(cmd *exec.Cmd, input io.Reader, atEnd func()) (io.Reader, error)
497 497
 	}
498 498
 	stdout, err := cmd.StdoutPipe()
499 499
 	if err != nil {
500
-		if atEnd != nil {
501
-			atEnd()
502
-		}
503 500
 		return nil, err
504 501
 	}
505 502
 	stderr, err := cmd.StderrPipe()
506 503
 	if err != nil {
507
-		if atEnd != nil {
508
-			atEnd()
509
-		}
510 504
 		return nil, err
511 505
 	}
512 506
 	pipeR, pipeW := io.Pipe()
... ...
@@ -531,9 +561,6 @@ func CmdStream(cmd *exec.Cmd, input io.Reader, atEnd func()) (io.Reader, error)
531 531
 		} else {
532 532
 			pipeW.Close()
533 533
 		}
534
-		if atEnd != nil {
535
-			atEnd()
536
-		}
537 534
 	}()
538 535
 	// Run the command and return the pipe
539 536
 	if err := cmd.Start(); err != nil {
... ...
@@ -14,7 +14,7 @@ import (
14 14
 
15 15
 func TestCmdStreamLargeStderr(t *testing.T) {
16 16
 	cmd := exec.Command("/bin/sh", "-c", "dd if=/dev/zero bs=1k count=1000 of=/dev/stderr; echo hello")
17
-	out, err := CmdStream(cmd, nil, nil)
17
+	out, err := CmdStream(cmd, nil)
18 18
 	if err != nil {
19 19
 		t.Fatalf("Failed to start command: %s", err)
20 20
 	}
... ...
@@ -35,7 +35,7 @@ func TestCmdStreamLargeStderr(t *testing.T) {
35 35
 
36 36
 func TestCmdStreamBad(t *testing.T) {
37 37
 	badCmd := exec.Command("/bin/sh", "-c", "echo hello; echo >&2 error couldn\\'t reverse the phase pulser; exit 1")
38
-	out, err := CmdStream(badCmd, nil, nil)
38
+	out, err := CmdStream(badCmd, nil)
39 39
 	if err != nil {
40 40
 		t.Fatalf("Failed to start command: %s", err)
41 41
 	}
... ...
@@ -50,7 +50,7 @@ func TestCmdStreamBad(t *testing.T) {
50 50
 
51 51
 func TestCmdStreamGood(t *testing.T) {
52 52
 	cmd := exec.Command("/bin/sh", "-c", "echo hello; exit 0")
53
-	out, err := CmdStream(cmd, nil, nil)
53
+	out, err := CmdStream(cmd, nil)
54 54
 	if err != nil {
55 55
 		t.Fatal(err)
56 56
 	}
... ...
@@ -89,6 +89,16 @@ func tarUntar(t *testing.T, origin string, compression Compression) error {
89 89
 	if _, err := os.Stat(tmp); err != nil {
90 90
 		return err
91 91
 	}
92
+
93
+	changes, err := ChangesDirs(origin, tmp)
94
+	if err != nil {
95
+		return err
96
+	}
97
+
98
+	if len(changes) != 0 {
99
+		t.Fatalf("Unexpected differences after tarUntar: %v", changes)
100
+	}
101
+
92 102
 	return nil
93 103
 }
94 104
 
... ...
@@ -108,8 +118,6 @@ func TestTarUntar(t *testing.T) {
108 108
 	for _, c := range []Compression{
109 109
 		Uncompressed,
110 110
 		Gzip,
111
-		Bzip2,
112
-		Xz,
113 111
 	} {
114 112
 		if err := tarUntar(t, origin, c); err != nil {
115 113
 			t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err)
... ...
@@ -347,70 +347,12 @@ func ExportChanges(dir string, changes []Change) (Archive, error) {
347 347
 				}
348 348
 			} else {
349 349
 				path := filepath.Join(dir, change.Path)
350
-
351
-				var stat syscall.Stat_t
352
-				if err := syscall.Lstat(path, &stat); err != nil {
353
-					utils.Debugf("Can't stat source file: %s\n", err)
354
-					continue
355
-				}
356
-
357
-				mtim := getLastModification(&stat)
358
-				atim := getLastAccess(&stat)
359
-				hdr := &tar.Header{
360
-					Name:       change.Path[1:],
361
-					Mode:       int64(stat.Mode & 07777),
362
-					Uid:        int(stat.Uid),
363
-					Gid:        int(stat.Gid),
364
-					ModTime:    time.Unix(int64(mtim.Sec), int64(mtim.Nsec)),
365
-					AccessTime: time.Unix(int64(atim.Sec), int64(atim.Nsec)),
366
-				}
367
-
368
-				if stat.Mode&syscall.S_IFDIR == syscall.S_IFDIR {
369
-					hdr.Typeflag = tar.TypeDir
370
-				} else if stat.Mode&syscall.S_IFLNK == syscall.S_IFLNK {
371
-					hdr.Typeflag = tar.TypeSymlink
372
-					if link, err := os.Readlink(path); err != nil {
373
-						utils.Debugf("Can't readlink source file: %s\n", err)
374
-						continue
375
-					} else {
376
-						hdr.Linkname = link
377
-					}
378
-				} else if stat.Mode&syscall.S_IFBLK == syscall.S_IFBLK ||
379
-					stat.Mode&syscall.S_IFCHR == syscall.S_IFCHR {
380
-					if stat.Mode&syscall.S_IFBLK == syscall.S_IFBLK {
381
-						hdr.Typeflag = tar.TypeBlock
382
-					} else {
383
-						hdr.Typeflag = tar.TypeChar
384
-					}
385
-					hdr.Devmajor = int64(major(uint64(stat.Rdev)))
386
-					hdr.Devminor = int64(minor(uint64(stat.Rdev)))
387
-				} else if stat.Mode&syscall.S_IFIFO == syscall.S_IFIFO ||
388
-					stat.Mode&syscall.S_IFSOCK == syscall.S_IFSOCK {
389
-					hdr.Typeflag = tar.TypeFifo
390
-				} else if stat.Mode&syscall.S_IFREG == syscall.S_IFREG {
391
-					hdr.Typeflag = tar.TypeReg
392
-					hdr.Size = stat.Size
393
-				} else {
394
-					utils.Debugf("Unknown file type: %s\n", path)
395
-					continue
396
-				}
397
-
398
-				if err := tw.WriteHeader(hdr); err != nil {
399
-					utils.Debugf("Can't write tar header: %s\n", err)
400
-				}
401
-				if hdr.Typeflag == tar.TypeReg {
402
-					if file, err := os.Open(path); err != nil {
403
-						utils.Debugf("Can't open file: %s\n", err)
404
-					} else {
405
-						_, err := io.Copy(tw, file)
406
-						if err != nil {
407
-							utils.Debugf("Can't copy file: %s\n", err)
408
-						}
409
-						file.Close()
410
-					}
350
+				if err := addTarFile(path, change.Path[1:], tw); err != nil {
351
+					utils.Debugf("Can't add file %s to tar: %s\n", path, err)
411 352
 				}
412 353
 			}
413 354
 		}
355
+
414 356
 		// Make sure to check the error on Close.
415 357
 		if err := tw.Close(); err != nil {
416 358
 			utils.Debugf("Can't close layer: %s\n", err)
... ...
@@ -1473,7 +1473,6 @@ func (container *Container) Copy(resource string) (archive.Archive, error) {
1473 1473
 	return archive.TarFilter(basePath, &archive.TarOptions{
1474 1474
 		Compression: archive.Uncompressed,
1475 1475
 		Includes:    filter,
1476
-		Recursive:   true,
1477 1476
 	})
1478 1477
 }
1479 1478
 
... ...
@@ -225,7 +225,6 @@ func (a *Driver) Get(id string) (string, error) {
225 225
 // Returns an archive of the contents for the id
226 226
 func (a *Driver) Diff(id string) (archive.Archive, error) {
227 227
 	return archive.TarFilter(path.Join(a.rootPath(), "diff", id), &archive.TarOptions{
228
-		Recursive:   true,
229 228
 		Compression: archive.Uncompressed,
230 229
 	})
231 230
 }
... ...
@@ -120,7 +120,6 @@ The test suite will also download a small test container, so you will need inter
120 120
 
121 121
 To run properly, docker needs the following software to be installed at runtime:
122 122
 
123
-* GNU Tar version 1.26 or later
124 123
 * iproute2 version 3.5 or later (build after 2012-05-21), and specifically the "ip" utility
125 124
 * iptables version 1.4 or later
126 125
 * The LXC utility scripts (http://lxc.sourceforge.net) version 0.8 or later
... ...
@@ -374,7 +374,7 @@ func TestGetContainersExport(t *testing.T) {
374 374
 			}
375 375
 			t.Fatal(err)
376 376
 		}
377
-		if h.Name == "./test" {
377
+		if h.Name == "test" {
378 378
 			found = true
379 379
 			break
380 380
 		}