X-Git-Url: https://ruderich.org/simon/gitweb/?p=nsscash%2Fnsscash.git;a=blobdiff_plain;f=file.go;h=3884bba8b6ae3c4f0e6d86f26d796fd5fcac0952;hp=02bc76715868967ed68596f8aebc5af1ccdee3bf;hb=HEAD;hpb=13b90749eb1b3547bbfbb47fe142cb96a17234b5 diff --git a/file.go b/file.go index 02bc767..3884bba 100644 --- a/file.go +++ b/file.go @@ -1,6 +1,6 @@ // Download and write files atomically to the file system -// Copyright (C) 2019 Simon Ruderich +// Copyright (C) 2019-2021 Simon Ruderich // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by @@ -19,6 +19,8 @@ package main import ( "bytes" + "crypto/sha512" + "encoding/hex" "fmt" "io/ioutil" "log" @@ -26,7 +28,9 @@ import ( "os" "path/filepath" "syscall" + "time" + "github.com/google/renameio" "github.com/pkg/errors" ) @@ -39,7 +43,7 @@ func handleFiles(cfg *Config, state *State) error { for i, f := range cfg.Files { err := fetchFile(&cfg.Files[i], state) if err != nil { - return errors.Wrapf(err, "%q (%s)", f.Url, f.Type) + return errors.Wrapf(err, "%q (%v)", f.Url, f.Type) } } @@ -51,20 +55,52 @@ func handleFiles(cfg *Config, state *State) error { err := deployFile(&cfg.Files[i]) if err != nil { - return errors.Wrapf(err, "%q (%s)", f.Url, f.Type) + return errors.Wrapf(err, "%q (%v)", f.Url, f.Type) } } return nil } +func checksumFile(file *File) (string, error) { + x, err := ioutil.ReadFile(file.Path) + if err != nil { + return "", err + } + return checksumBytes(x), nil +} + +func checksumBytes(x []byte) string { + h := sha512.New() + h.Write(x) + return hex.EncodeToString(h.Sum(nil)) +} + func fetchFile(file *File, state *State) error { t := state.LastModified[file.Url] - status, body, err := fetchIfModified(file.Url, &t) + + hash, err := checksumFile(file) + if err != nil { + // See below in deployFile() for the reason + return errors.Wrapf(err, "file.path %q must exist", file.Path) + } + if hash != state.Checksum[file.Url] { + log.Printf("%q -> %q: hash has changed", file.Url, file.Path) + var zero time.Time + t = zero // force download + } + + oldT := t + status, body, err := fetchIfModified(file.Url, + file.Username, file.Password, file.CA, &t) if err != nil { return err } if status == http.StatusNotModified { + if oldT.IsZero() { + return fmt.Errorf("status code 304 " + + "but did not send If-Modified-Since") + } log.Printf("%q -> %q: not modified", file.Url, file.Path) return nil } @@ -116,6 +152,8 @@ func fetchFile(file *File, state *State) error { } else { return fmt.Errorf("unsupported file type %v", file.Type) } + + state.Checksum[file.Url] = checksumBytes(file.body) return nil } @@ -127,28 +165,22 @@ func deployFile(file *File) error { return fmt.Errorf("refusing to write empty file") } - // Write the file in an atomic fashion by creating a temporary file - // and renaming it over the target file - - dir := filepath.Dir(file.Path) - name := filepath.Base(file.Path) - - f, err := ioutil.TempFile(dir, "tmp-"+name+"-") + f, err := renameio.TempFile(filepath.Dir(file.Path), file.Path) if err != nil { return err } - defer os.Remove(f.Name()) - defer f.Close() + defer f.Cleanup() - // Apply permissions/user/group from the target file, use Stat instead - // of Lstat as only the target's permissions are relevant + // Apply permissions/user/group from the target file but remove the + // write permissions to discourage manual modifications, use Stat + // instead of Lstat as only the target's permissions are relevant stat, err := os.Stat(file.Path) if err != nil { // We do not create the path if it doesn't exist, because we // do not know the proper permissions return errors.Wrapf(err, "file.path %q must exist", file.Path) } - err = f.Chmod(stat.Mode()) + err = f.Chmod(stat.Mode() & ^os.FileMode(0222)) // remove write perms if err != nil { return err } @@ -166,9 +198,9 @@ func deployFile(file *File) error { if err != nil { return err } - err = f.Sync() + err = f.CloseAtomicallyReplace() if err != nil { return err } - return os.Rename(f.Name(), file.Path) + return syncPath(filepath.Dir(file.Path)) }