]> ruderich.org/simon Gitweb - nsscash/nsscash.git/blobdiff - file.go
.github: update upstream actions to latest version
[nsscash/nsscash.git] / file.go
diff --git a/file.go b/file.go
index 33d71c14c152c144dc4e0e3cf04e758c0cee4858..3884bba8b6ae3c4f0e6d86f26d796fd5fcac0952 100644 (file)
--- 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,15 +28,22 @@ import (
        "os"
        "path/filepath"
        "syscall"
+       "time"
 
+       "github.com/google/renameio"
        "github.com/pkg/errors"
 )
 
 func handleFiles(cfg *Config, state *State) error {
+       // Split into fetch and deploy phase to prevent updates of only some
+       // files which might lead to inconsistent state; obviously this won't
+       // work during the deploy phase, but it helps if the web server fails
+       // to deliver some files
+
        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)
                }
        }
 
@@ -46,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
        }
@@ -111,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
 }
 
@@ -122,27 +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
+       // 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
        }
@@ -160,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))
 }