When we put something in the queue and respond "250 ok" to the client, that is taken as accepting the email. As part of putting something in the queue, we write it to disk, but today we don't do an fsync on that file. That leaves a gap where a badly timed crash on some systems could lead to the file being empty, causing us to lose an email that we accepted. To elliminate (or drastically reduce on some filesystems) the chances of that situation, we call fsync on the file that gets written when we put something in the queue. Thanks to nolanl@github for reporting this in https://github.com/albertito/chasquid/issues/78.
120 lines
2.8 KiB
Go
120 lines
2.8 KiB
Go
// Package protoio contains I/O functions for protocol buffers.
|
|
package protoio
|
|
|
|
import (
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"blitiri.com.ar/go/chasquid/internal/safeio"
|
|
|
|
"google.golang.org/protobuf/encoding/prototext"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// ReadMessage reads a protocol buffer message from fname, and unmarshalls it
|
|
// into pb.
|
|
func ReadMessage(fname string, pb proto.Message) error {
|
|
in, err := os.ReadFile(fname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return proto.Unmarshal(in, pb)
|
|
}
|
|
|
|
// ReadTextMessage reads a text format protocol buffer message from fname, and
|
|
// unmarshalls it into pb.
|
|
func ReadTextMessage(fname string, pb proto.Message) error {
|
|
in, err := os.ReadFile(fname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return prototext.Unmarshal(in, pb)
|
|
}
|
|
|
|
// WriteMessage marshals pb and atomically writes it into fname.
|
|
func WriteMessage(fname string, pb proto.Message, perm os.FileMode, ops ...safeio.FileOp) error {
|
|
out, err := proto.Marshal(pb)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return safeio.WriteFile(fname, out, perm, ops...)
|
|
}
|
|
|
|
var textOpts = prototext.MarshalOptions{
|
|
Multiline: true,
|
|
}
|
|
|
|
// WriteTextMessage marshals pb in text format and atomically writes it into
|
|
// fname.
|
|
func WriteTextMessage(fname string, pb proto.Message, perm os.FileMode, ops ...safeio.FileOp) error {
|
|
out, err := textOpts.Marshal(pb)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return safeio.WriteFile(fname, out, perm, ops...)
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
// Store represents a persistent protocol buffer message store.
|
|
type Store struct {
|
|
// Directory where the store is.
|
|
dir string
|
|
}
|
|
|
|
// NewStore returns a new Store instance. It will create dir if needed.
|
|
func NewStore(dir string) (*Store, error) {
|
|
s := &Store{dir}
|
|
err := os.MkdirAll(dir, 0770)
|
|
return s, err
|
|
}
|
|
|
|
const storeIDPrefix = "s:"
|
|
|
|
// idToFname takes a generic id and returns the corresponding file for it
|
|
// (which may or may not exist).
|
|
func (s *Store) idToFname(id string) string {
|
|
return s.dir + "/" + storeIDPrefix + url.QueryEscape(id)
|
|
}
|
|
|
|
// Put a message into the store.
|
|
func (s *Store) Put(id string, m proto.Message) error {
|
|
return WriteTextMessage(s.idToFname(id), m, 0660)
|
|
}
|
|
|
|
// Get a message from the store.
|
|
func (s *Store) Get(id string, m proto.Message) (bool, error) {
|
|
err := ReadTextMessage(s.idToFname(id), m)
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return err == nil, err
|
|
}
|
|
|
|
// ListIDs in the store.
|
|
func (s *Store) ListIDs() ([]string, error) {
|
|
ids := []string{}
|
|
|
|
entries, err := os.ReadDir(s.dir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, e := range entries {
|
|
if !strings.HasPrefix(e.Name(), storeIDPrefix) {
|
|
continue
|
|
}
|
|
|
|
id := e.Name()[len(storeIDPrefix):]
|
|
id, err = url.QueryUnescape(id)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
ids = append(ids, id)
|
|
}
|
|
|
|
return ids, nil
|
|
}
|