]> ruderich.org/simon Gitweb - nsscash/nsscash.git/blob - file.go
README: minor updates and fixes
[nsscash/nsscash.git] / file.go
1 // Download and write files atomically to the file system
2
3 // Copyright (C) 2019  Simon Ruderich
4 //
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.
9 //
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.
14 //
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/>.
17
18 package main
19
20 import (
21         "bytes"
22         "fmt"
23         "io/ioutil"
24         "log"
25         "net/http"
26         "os"
27         "path/filepath"
28         "syscall"
29
30         "github.com/pkg/errors"
31 )
32
33 func handleFiles(cfg *Config, state *State) error {
34         for i, f := range cfg.Files {
35                 err := fetchFile(&cfg.Files[i], state)
36                 if err != nil {
37                         return errors.Wrapf(err, "%q (%s)", f.Url, f.Type)
38                 }
39         }
40
41         for i, f := range cfg.Files {
42                 // No update required
43                 if f.body == nil {
44                         continue
45                 }
46
47                 err := deployFile(&cfg.Files[i])
48                 if err != nil {
49                         return errors.Wrapf(err, "%q (%s)", f.Url, f.Type)
50                 }
51         }
52
53         return nil
54 }
55
56 func fetchFile(file *File, state *State) error {
57         t := state.LastModified[file.Url]
58         status, body, err := fetchIfModified(file.Url, &t)
59         if err != nil {
60                 return err
61         }
62         if status == http.StatusNotModified {
63                 log.Printf("%q -> %q: not modified", file.Url, file.Path)
64                 return nil
65         }
66         if status != http.StatusOK {
67                 return fmt.Errorf("status code %v", status)
68         }
69         state.LastModified[file.Url] = t
70
71         if file.Type == FileTypePlain {
72                 if len(body) == 0 {
73                         return fmt.Errorf("refusing to use empty response")
74                 }
75                 file.body = body
76
77         } else if file.Type == FileTypePasswd {
78                 pws, err := ParsePasswds(bytes.NewReader(body))
79                 if err != nil {
80                         return err
81                 }
82                 // Safety check: having no users can be very dangerous, don't
83                 // permit it
84                 if len(pws) == 0 {
85                         return fmt.Errorf("refusing to use empty passwd file")
86                 }
87
88                 var x bytes.Buffer
89                 err = SerializePasswds(&x, pws)
90                 if err != nil {
91                         return err
92                 }
93                 file.body = x.Bytes()
94
95         } else if file.Type == FileTypeGroup {
96                 grs, err := ParseGroups(bytes.NewReader(body))
97                 if err != nil {
98                         return err
99                 }
100                 if len(grs) == 0 {
101                         return fmt.Errorf("refusing to use empty group file")
102                 }
103
104                 var x bytes.Buffer
105                 err = SerializeGroups(&x, grs)
106                 if err != nil {
107                         return err
108                 }
109                 file.body = x.Bytes()
110
111         } else {
112                 return fmt.Errorf("unsupported file type %v", file.Type)
113         }
114         return nil
115 }
116
117 func deployFile(file *File) error {
118         log.Printf("%q -> %q: updating file", file.Url, file.Path)
119
120         // Safety check
121         if len(file.body) == 0 {
122                 return fmt.Errorf("refusing to write empty file")
123         }
124
125         // Write the file in an atomic fashion by creating a temporary file
126         // and renaming it over the target file
127
128         dir := filepath.Dir(file.Path)
129         name := filepath.Base(file.Path)
130
131         f, err := ioutil.TempFile(dir, "tmp-"+name+"-")
132         if err != nil {
133                 return err
134         }
135         defer os.Remove(f.Name())
136         defer f.Close()
137
138         // Apply permissions/user/group from the target file
139         stat, err := os.Stat(file.Path)
140         if err != nil {
141                 // We do not create the path if it doesn't exist, because we
142                 // do not know the proper permissions
143                 return errors.Wrapf(err, "file.path %q must exist", file.Path)
144         }
145         err = f.Chmod(stat.Mode())
146         if err != nil {
147                 return err
148         }
149         // TODO: support more systems
150         sys, ok := stat.Sys().(*syscall.Stat_t)
151         if !ok {
152                 return fmt.Errorf("unsupported FileInfo.Sys()")
153         }
154         err = f.Chown(int(sys.Uid), int(sys.Gid))
155         if err != nil {
156                 return err
157         }
158
159         _, err = f.Write(file.body)
160         if err != nil {
161                 return err
162         }
163         err = f.Sync()
164         if err != nil {
165                 return err
166         }
167         return os.Rename(f.Name(), file.Path)
168 }