1// Copyright 2018 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package main
16
17import (
18	"archive/zip"
19	"flag"
20	"fmt"
21	"io"
22	"io/ioutil"
23	"log"
24	"os"
25	"path/filepath"
26	"strings"
27)
28
29var (
30	outputDir  = flag.String("d", "", "output dir")
31	outputFile = flag.String("l", "", "output list file")
32	filter     = flag.String("f", "", "optional filter pattern")
33	zipPrefix  = flag.String("zip-prefix", "", "optional prefix within the zip file to extract, stripping the prefix")
34)
35
36func must(err error) {
37	if err != nil {
38		log.Fatal(err)
39	}
40}
41
42func writeFile(filename string, in io.Reader, perm os.FileMode) error {
43	out, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
44	if err != nil {
45		return err
46	}
47	_, err = io.Copy(out, in)
48	if err != nil {
49		out.Close()
50		return err
51	}
52
53	return out.Close()
54}
55
56func writeSymlink(filename string, in io.Reader) error {
57	b, err := ioutil.ReadAll(in)
58	if err != nil {
59		return err
60	}
61	dest := string(b)
62	err = os.Symlink(dest, filename)
63	return err
64}
65
66func main() {
67	flag.Usage = func() {
68		fmt.Fprintln(os.Stderr, "usage: zipsync -d <output dir> [-l <output file>] [-f <pattern>] [zip]...")
69		flag.PrintDefaults()
70	}
71
72	flag.Parse()
73
74	if *outputDir == "" {
75		flag.Usage()
76		os.Exit(1)
77	}
78
79	inputs := flag.Args()
80
81	// For now, just wipe the output directory and replace its contents with the zip files
82	// Eventually this could only modify the directory contents as necessary to bring it up
83	// to date with the zip files.
84	must(os.RemoveAll(*outputDir))
85
86	must(os.MkdirAll(*outputDir, 0777))
87
88	var files []string
89	seen := make(map[string]string)
90
91	if *zipPrefix != "" {
92		*zipPrefix = filepath.Clean(*zipPrefix) + "/"
93	}
94
95	for _, input := range inputs {
96		reader, err := zip.OpenReader(input)
97		if err != nil {
98			log.Fatal(err)
99		}
100		defer reader.Close()
101
102		for _, f := range reader.File {
103			name := f.Name
104			if *zipPrefix != "" {
105				if !strings.HasPrefix(name, *zipPrefix) {
106					continue
107				}
108				name = strings.TrimPrefix(name, *zipPrefix)
109			}
110			if *filter != "" {
111				if match, err := filepath.Match(*filter, filepath.Base(name)); err != nil {
112					log.Fatal(err)
113				} else if !match {
114					continue
115				}
116			}
117			if filepath.IsAbs(name) {
118				log.Fatalf("%q in %q is an absolute path", name, input)
119			}
120
121			if prev, exists := seen[name]; exists {
122				log.Fatalf("%q found in both %q and %q", name, prev, input)
123			}
124			seen[name] = input
125
126			filename := filepath.Join(*outputDir, name)
127			if f.FileInfo().IsDir() {
128				must(os.MkdirAll(filename, 0777))
129			} else {
130				must(os.MkdirAll(filepath.Dir(filename), 0777))
131				in, err := f.Open()
132				if err != nil {
133					log.Fatal(err)
134				}
135				if f.FileInfo().Mode()&os.ModeSymlink != 0 {
136					must(writeSymlink(filename, in))
137				} else {
138					must(writeFile(filename, in, f.FileInfo().Mode()))
139				}
140				in.Close()
141				files = append(files, filename)
142			}
143		}
144	}
145
146	if *outputFile != "" {
147		data := strings.Join(files, "\n")
148		if len(files) > 0 {
149			data += "\n"
150		}
151		must(ioutil.WriteFile(*outputFile, []byte(data), 0666))
152	}
153}
154