1 // Download and write files atomically to the file system
3 // Copyright (C) 2019 Simon Ruderich
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU Affero General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU Affero General Public License for more details.
15 // You should have received a copy of the GNU Affero General Public License
16 // along with this program. If not, see <https://www.gnu.org/licenses/>.
30 "github.com/pkg/errors"
33 func handleFiles(cfg *Config, state *State) error {
34 // Split into fetch and deploy phase to prevent updates of only some
35 // files which might lead to inconsistent state; obviously this won't
36 // work during the deploy phase, but it helps if the web server fails
37 // to deliver some files
39 for i, f := range cfg.Files {
40 err := fetchFile(&cfg.Files[i], state)
42 return errors.Wrapf(err, "%q (%s)", f.Url, f.Type)
46 for i, f := range cfg.Files {
52 err := deployFile(&cfg.Files[i])
54 return errors.Wrapf(err, "%q (%s)", f.Url, f.Type)
61 func fetchFile(file *File, state *State) error {
62 t := state.LastModified[file.Url]
63 status, body, err := fetchIfModified(file.Url, &t)
67 if status == http.StatusNotModified {
68 log.Printf("%q -> %q: not modified", file.Url, file.Path)
71 if status != http.StatusOK {
72 return fmt.Errorf("status code %v", status)
74 state.LastModified[file.Url] = t
76 if file.Type == FileTypePlain {
78 return fmt.Errorf("refusing to use empty response")
82 } else if file.Type == FileTypePasswd {
83 pws, err := ParsePasswds(bytes.NewReader(body))
87 // Safety check: having no users can be very dangerous, don't
90 return fmt.Errorf("refusing to use empty passwd file")
94 err = SerializePasswds(&x, pws)
100 } else if file.Type == FileTypeGroup {
101 grs, err := ParseGroups(bytes.NewReader(body))
106 return fmt.Errorf("refusing to use empty group file")
110 err = SerializeGroups(&x, grs)
114 file.body = x.Bytes()
117 return fmt.Errorf("unsupported file type %v", file.Type)
122 func deployFile(file *File) error {
123 log.Printf("%q -> %q: updating file", file.Url, file.Path)
126 if len(file.body) == 0 {
127 return fmt.Errorf("refusing to write empty file")
130 // Write the file in an atomic fashion by creating a temporary file
131 // and renaming it over the target file
133 dir := filepath.Dir(file.Path)
134 name := filepath.Base(file.Path)
136 f, err := ioutil.TempFile(dir, "tmp-"+name+"-")
140 defer os.Remove(f.Name())
143 // Apply permissions/user/group from the target file, use Stat instead
144 // of Lstat as only the target's permissions are relevant
145 stat, err := os.Stat(file.Path)
147 // We do not create the path if it doesn't exist, because we
148 // do not know the proper permissions
149 return errors.Wrapf(err, "file.path %q must exist", file.Path)
151 err = f.Chmod(stat.Mode())
155 // TODO: support more systems
156 sys, ok := stat.Sys().(*syscall.Stat_t)
158 return fmt.Errorf("unsupported FileInfo.Sys()")
160 err = f.Chown(int(sys.Uid), int(sys.Gid))
165 _, err = f.Write(file.body)
173 return os.Rename(f.Name(), file.Path)