480 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			480 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| //3456789112345676892123456789312345678941234567895123456789612345678971234567898
 | |
| 
 | |
| package main
 | |
| 
 | |
| import "crypto/sha256"
 | |
| import "fmt"
 | |
| import "github.com/jessevdk/go-flags"
 | |
| import "github.com/mr-tron/base58"
 | |
| import "github.com/multiformats/go-multihash"
 | |
| import "github.com/pkg/xattr"
 | |
| import "github.com/sirupsen/logrus"
 | |
| 
 | |
| import "os"
 | |
| import "io"
 | |
| import "flag"
 | |
| import "time"
 | |
| import "strconv"
 | |
| 
 | |
| var Version string
 | |
| var Buildtime string
 | |
| var Builduser string
 | |
| var Buildarch string
 | |
| var log *logrus.Logger
 | |
| 
 | |
| const namespacePrefix = "berlin.sneak.xsum"
 | |
| 
 | |
| //FIXME(sneak) make this parallelize to NUM_CPUS when processing multiple
 | |
| //args
 | |
| 
 | |
| //FIXME(sneak) add a -r (recursive) flag for directories
 | |
| 
 | |
| //FIXME(sneak) make checking support reading hash algo type from multihash
 | |
| //instead of assumming sha256
 | |
| 
 | |
| func main() {
 | |
| 	os.Exit(xsum())
 | |
| }
 | |
| 
 | |
| func xsum() int {
 | |
| 	log = logrus.New()
 | |
| 	log.SetLevel(logrus.ErrorLevel)
 | |
| 	log.SetReportCaller(false)
 | |
| 
 | |
| 	var opts struct {
 | |
| 		// Slice of bool will append 'true' each time the option
 | |
| 		// is encountered (can be set multiple times, like -vvv)
 | |
| 		Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"`
 | |
| 	}
 | |
| 
 | |
| 	args, err := flags.Parse(&opts)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		usage()
 | |
| 		return -1
 | |
| 	}
 | |
| 
 | |
| 	if opts.Verbose == true {
 | |
| 		log.SetReportCaller(true)
 | |
| 		log.SetLevel(logrus.DebugLevel)
 | |
| 	}
 | |
| 
 | |
| 	log.Debugf(
 | |
| 		"xsum version %s (%s) built %s by %s",
 | |
| 		Version,
 | |
| 		Buildarch,
 | |
| 		Buildtime,
 | |
| 		Builduser,
 | |
| 	)
 | |
| 
 | |
| 	if len(args) < 2 {
 | |
| 		usage()
 | |
| 		return -1
 | |
| 	}
 | |
| 
 | |
| 	mode := args[0]
 | |
| 
 | |
| 	paths := args[1:]
 | |
| 
 | |
| 	switch mode {
 | |
| 	case "cron":
 | |
| 		x := xsfCheckAndUpdate(paths)
 | |
| 		if x != nil {
 | |
| 			log.Debug(x)
 | |
| 			return -1
 | |
| 		} else {
 | |
| 			return 0
 | |
| 		}
 | |
| 	case "check-and-update":
 | |
| 		x := xsfCheckAndUpdate(paths)
 | |
| 		if x != nil {
 | |
| 			log.Debug(x)
 | |
| 			return -1
 | |
| 		} else {
 | |
| 			return 0
 | |
| 		}
 | |
| 	case "check":
 | |
| 		x := xsfCheck(paths)
 | |
| 		if x != nil {
 | |
| 			log.Debug(x)
 | |
| 			return -1
 | |
| 		} else {
 | |
| 			return 0
 | |
| 		}
 | |
| 	case "update":
 | |
| 		x := xsfUpdate(paths)
 | |
| 		if x != nil {
 | |
| 			log.Debug(x)
 | |
| 			return -1
 | |
| 		} else {
 | |
| 			return 0
 | |
| 		}
 | |
| 	default:
 | |
| 		usage()
 | |
| 		return -1
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func usage() {
 | |
| 	fmt.Fprintf(os.Stderr, "usage: %s [-v] <update|check|check-and-update|cron> <path> [path2] [...]\n", os.Args[0])
 | |
| 	flag.PrintDefaults()
 | |
| }
 | |
| 
 | |
| func xsfCheck(paths []string) error {
 | |
| 	log.Debugf("check")
 | |
| 	for _, path := range paths {
 | |
| 		x := newXsf(path)
 | |
| 		err := x.Check()
 | |
| 		if err != nil {
 | |
| 			fmt.Printf("%s\tERROR (expected=%s actual=%s)\n", x.path, x.xmultihash, x.multihash)
 | |
| 			return err
 | |
| 		} else {
 | |
| 			fmt.Printf("%s\tOK (hash=%s)\n", x.path, x.multihash)
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func showError(e error) {
 | |
| 	fmt.Fprintf(os.Stderr, "error: %s\n", e)
 | |
| }
 | |
| 
 | |
| func xsfUpdate(paths []string) error {
 | |
| 	log.Debugf("update")
 | |
| 	for _, path := range paths {
 | |
| 		x := newXsf(path)
 | |
| 		err := x.Update()
 | |
| 		if err != nil {
 | |
| 			failure := fmt.Errorf("%s\tERROR (error=%s)\n", x.path, err)
 | |
| 			log.Error(failure)
 | |
| 			return failure
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func xsfCheckAndUpdate(paths []string) error {
 | |
| 	log.Debugf("check-and-update")
 | |
| 	err := xsfCheck(paths)
 | |
| 	if err != nil {
 | |
| 		//xsfCheck() does the printing of errors itself, we just need to
 | |
| 		//bubble it up and not update
 | |
| 		log.Error(err)
 | |
| 		return err
 | |
| 	}
 | |
| 	return xsfUpdate(paths)
 | |
| }
 | |
| 
 | |
| func HashFile(fp *os.File) (string, error) {
 | |
| 	h := sha256.New()
 | |
| 
 | |
| 	if _, err := io.Copy(h, fp); err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	mHashBuf, err := multihash.EncodeName(h.Sum(nil), "sha2-256")
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return base58.Encode(mHashBuf), nil
 | |
| }
 | |
| 
 | |
| func stringInSlice(a string, list []string) bool {
 | |
| 	for _, b := range list {
 | |
| 		if b == a {
 | |
| 			return true
 | |
| 		}
 | |
| 	}
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| /////////////////////////////////////////////////////////////////////////////////
 | |
| // type xsf
 | |
| /////////////////////////////////////////////////////////////////////////////////
 | |
| 
 | |
| type xsf struct {
 | |
| 	fi         *os.FileInfo
 | |
| 	fp         *os.File
 | |
| 	multihash  string
 | |
| 	xmultihash string
 | |
| 	mtime      string
 | |
| 	xmtime     string
 | |
| 	path       string
 | |
| 	size       uint64
 | |
| 	xsize      uint64
 | |
| }
 | |
| 
 | |
| /////////////////////////////////////////////////////////////////////////////////
 | |
| // constructor
 | |
| /////////////////////////////////////////////////////////////////////////////////
 | |
| 
 | |
| func newXsf(path string) *xsf {
 | |
| 	x := xsf{}
 | |
| 	x.path = path
 | |
| 	return &x
 | |
| }
 | |
| 
 | |
| //FIXME calling .List() three times might be slow, memoize if necessary
 | |
| 
 | |
| func (x *xsf) hasMtimeXattr() bool {
 | |
| 	xn := fmt.Sprintf("%s.%s", namespacePrefix, "mtime")
 | |
| 	l, err := xattr.FList(x.fp)
 | |
| 	if err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	return stringInSlice(xn, l)
 | |
| }
 | |
| 
 | |
| func (x *xsf) readMtimeXattr() error {
 | |
| 	log.Infof("reading mtime xattr")
 | |
| 	xn := fmt.Sprintf("%s.%s", namespacePrefix, "mtime")
 | |
| 
 | |
| 	v, err := xattr.FGet(x.fp, xn)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	x.xmtime = string(v)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (x *xsf) hasMultihashXattr() bool {
 | |
| 	xn := fmt.Sprintf("%s.%s", namespacePrefix, "multihash")
 | |
| 	l, err := xattr.FList(x.fp)
 | |
| 	if err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	return stringInSlice(xn, l)
 | |
| }
 | |
| 
 | |
| func (x *xsf) readMultihashXattr() error {
 | |
| 	log.Infof("reading multihash xattr")
 | |
| 	xn := fmt.Sprintf("%s.%s", namespacePrefix, "multihash")
 | |
| 	v, err := xattr.FGet(x.fp, xn)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	x.xmultihash = string(v)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (x *xsf) hasSizeXattr() bool {
 | |
| 	xn := fmt.Sprintf("%s.%s", namespacePrefix, "size")
 | |
| 	l, err := xattr.FList(x.fp)
 | |
| 	if err != nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	return stringInSlice(xn, l)
 | |
| }
 | |
| 
 | |
| func (x *xsf) readSizeXattr() error {
 | |
| 	log.Infof("reading size xattr")
 | |
| 	xn := fmt.Sprintf("%s.%s", namespacePrefix, "size")
 | |
| 	v, err := xattr.FGet(x.fp, xn)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	a, b := strconv.ParseInt(string(v), 10, 64)
 | |
| 	if b != nil {
 | |
| 		return b
 | |
| 	}
 | |
| 	x.xsize = uint64(a)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (x *xsf) writeXattrs() error {
 | |
| 	log.Infof("writing xattrs")
 | |
| 
 | |
| 	var xn string
 | |
| 	var err error
 | |
| 
 | |
| 	xn = fmt.Sprintf("%s.%s", namespacePrefix, "mtime")
 | |
| 	log.Infof("writing xattr %s=%s", xn, x.mtime)
 | |
| 	err = xattr.FSet(x.fp, xn, []byte(x.mtime))
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	xn = fmt.Sprintf("%s.%s", namespacePrefix, "size")
 | |
| 	log.Infof("writing xattr %s=%s", xn, fmt.Sprintf("%d", x.size))
 | |
| 	err = xattr.FSet(x.fp, xn, []byte(fmt.Sprintf("%d", x.size)))
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	xn = fmt.Sprintf("%s.%s", namespacePrefix, "multihash")
 | |
| 	log.Infof("writing xattr %s=%s", xn, x.multihash)
 | |
| 	err = xattr.FSet(x.fp, xn, []byte(x.multihash))
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (x *xsf) stat() error {
 | |
| 	fi, err := x.fp.Stat()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	x.size = uint64(fi.Size())
 | |
| 	log.Debugf("size: %d", x.size)
 | |
| 	t := fi.ModTime().UTC().Format(time.RFC3339)
 | |
| 	log.Debugf("modtime: %s", t)
 | |
| 	x.mtime = t
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (x *xsf) hash() error {
 | |
| 	log.Debugf("hashing...")
 | |
| 	var err error
 | |
| 	if x.multihash, err = HashFile(x.fp); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	log.Debugf("hash: %s", x.multihash)
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (x *xsf) Check() error {
 | |
| 	fp, err := os.Open(x.path)
 | |
| 	defer fp.Close()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	x.fp = fp
 | |
| 
 | |
| 	serr := x.stat()
 | |
| 	if serr != nil {
 | |
| 		log.Errorf("error stat(): %s", serr)
 | |
| 		return serr
 | |
| 	}
 | |
| 
 | |
| 	if x.missingXattrs() == true {
 | |
| 		log.Infof("can't check file %s, does not have appropriate xattrs", x.path)
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	//check to see if file needs update (wrong mtime, wrong size)
 | |
| 	if x.needsUpdate() == true {
 | |
| 		log.Infof("can't check file %s, needs update (xattrs not current)", x.path)
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	//finally hash the file
 | |
| 	err2 := x.readMultihashXattr()
 | |
| 	if err2 != nil {
 | |
| 		log.Errorf("error reading file hash: %s", err2)
 | |
| 		return err2
 | |
| 	}
 | |
| 
 | |
| 	predictedHash := x.xmultihash
 | |
| 
 | |
| 	err3 := x.hash()
 | |
| 	if err3 != nil {
 | |
| 		log.Errorf("error hashing file: %s", err2)
 | |
| 		return err3
 | |
| 	}
 | |
| 
 | |
| 	actualHash := x.multihash
 | |
| 
 | |
| 	if predictedHash != actualHash {
 | |
| 		failure := fmt.Errorf("file corruption detected: expected=%s actual=%s", predictedHash, actualHash)
 | |
| 		return failure
 | |
| 	} else {
 | |
| 		log.Infof("file OK hash=%s", actualHash)
 | |
| 		return nil
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (x *xsf) missingXattrs() bool {
 | |
| 	if x.hasMtimeXattr() == false {
 | |
| 		log.Debugf("file needs update, missing mtime xattr")
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	if x.hasMultihashXattr() == false {
 | |
| 		log.Debugf("file needs update, missing multihash xattr")
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	if x.hasSizeXattr() == false {
 | |
| 		log.Debugf("file needs update, missing size xattr")
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (x *xsf) needsUpdate() bool {
 | |
| 	log.Debugf("checking if file needs update")
 | |
| 	// this expects stat() to have been called on the xsf
 | |
| 	// by Update already, so we have x.mtime et al populated from the
 | |
| 	// filesystem
 | |
| 
 | |
| 	// if the file doesn't have all 3 xattrs, it needs an update.
 | |
| 	if x.missingXattrs() == true {
 | |
| 		log.Debugf("file is missing xattrs")
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	// if the size doesn't match, it needs an update
 | |
| 	e := x.readSizeXattr()
 | |
| 	if e != nil {
 | |
| 		log.Debugf("unable to read file size attribute")
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	if x.size != x.xsize {
 | |
| 		log.Debugf("file needs update, size is %s, xattr size is %s", x.size, x.xsize)
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	// if the mtime is not the same, it needs an update
 | |
| 
 | |
| 	e2 := x.readMtimeXattr()
 | |
| 	if e2 != nil {
 | |
| 		log.Debugf("unable to read file mtime attribute")
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	if x.mtime != x.xmtime {
 | |
| 		log.Debugf("file needs update, mtime is %s, xattr mtime is %s", x.mtime, x.xmtime)
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	return false
 | |
| }
 | |
| 
 | |
| func (x *xsf) Update() error {
 | |
| 	log.Debugf("updating file (path: %s)", x.path)
 | |
| 	fp, err := os.Open(x.path)
 | |
| 	x.fp = fp
 | |
| 	defer fp.Close()
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if err = x.stat(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	// reminder: needsUpdate() must be called after stat() so that the
 | |
| 	// struct is populated
 | |
| 	if x.needsUpdate() == false {
 | |
| 		log.Debugf("skipping update on already hashed file %s", x.path)
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	if err = x.hash(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if err = x.writeXattrs(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |