upspin, client: add client.PutSequenced

PutSequenced is a Put that only succeeds if there is no existing
file of that name or if the existing file's sequence number matches
that in the call.

Change-Id: I7a94cb1a3c35f22021f8a3158fede61fa8314757
Reviewed-on: https://upspin-review.googlesource.com/c/upspin/+/19520
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/client/all_test.go b/client/all_test.go
index e345c01..d833b67 100644
--- a/client/all_test.go
+++ b/client/all_test.go
@@ -132,6 +132,60 @@
 	}
 }
 
+func TestPutSequencedGetTopLevelFile(t *testing.T) {
+	const (
+		user = "user1@google.com"
+		root = user + "/"
+	)
+	client := New(setup(baseCfg, user))
+	const (
+		fileName = root + "file"
+		text     = "hello sailor"
+		text2    = "put your lips together and blow"
+	)
+	// Put the initial version, remembering the sequence.
+	d, err := client.PutSequenced(fileName, upspin.SeqIgnore, []byte(text))
+	if err != nil {
+		t.Fatal("put file:", err)
+	}
+	data, err := client.Get(fileName)
+	if err != nil {
+		t.Fatal("get file:", err)
+	}
+	if string(data) != text {
+		t.Fatalf("get of %q has text %q; should be %q", fileName, data, text)
+	}
+	seq := d.Sequence
+	// PutSequenced another version using that sequence number. This should work.
+	d, err = client.PutSequenced(fileName, seq, []byte(text2))
+	if err != nil {
+		t.Fatal("put file:", err)
+	}
+	if d.Sequence == seq {
+		t.Fatalf("sequence number should have advanced")
+	}
+	data, err = client.Get(fileName)
+	if err != nil {
+		t.Fatal("get file:", err)
+	}
+	if string(data) != text2 {
+		t.Fatalf("get of %q has text %q; should be %q", fileName, data, text2)
+	}
+	// Now try it a PutSequenced with the old sequence number. This should fail.
+	_, err = client.PutSequenced(fileName, seq, []byte(text))
+	if err == nil {
+		t.Fatalf("PutSequenced with wrong sequence number should have failed")
+	}
+	// Make sure the data didn't change.
+	data, err = client.Get(fileName)
+	if err != nil {
+		t.Fatal("get file:", err)
+	}
+	if string(data) != text2 {
+		t.Fatalf("get of %q has text %q; should be %q", fileName, data, text2)
+	}
+}
+
 const Max = 100 * 1000 // Must be > 100.
 
 func setupFileIO(user upspin.UserName, fileName upspin.PathName, max int, t *testing.T) (upspin.Client, upspin.File, []byte) {
diff --git a/client/client.go b/client/client.go
index f5db872..f76a542 100644
--- a/client/client.go
+++ b/client/client.go
@@ -100,6 +100,11 @@
 
 // Put implements upspin.Client.
 func (c *Client) Put(name upspin.PathName, data []byte) (*upspin.DirEntry, error) {
+	return c.PutSequenced(name, upspin.SeqIgnore, data)
+}
+
+// PutSequenced implements upspin.Client.
+func (c *Client) PutSequenced(name upspin.PathName, seq int64, data []byte) (*upspin.DirEntry, error) {
 	const op errors.Op = "client.Put"
 	m, s := newMetric(op)
 	defer m.Done()
@@ -147,7 +152,7 @@
 		SignedName: name,
 		Packing:    packer.Packing(),
 		Time:       upspin.Now(),
-		Sequence:   upspin.SeqIgnore,
+		Sequence:   seq,
 		Writer:     c.config.UserName(),
 		Link:       "",
 		Attr:       upspin.AttrNone,
diff --git a/client/file/file_test.go b/client/file/file_test.go
index 2df95e9..509c0e5 100644
--- a/client/file/file_test.go
+++ b/client/file/file_test.go
@@ -120,6 +120,11 @@
 	copy(d.putData, data)
 	return nil, nil
 }
+func (d *dummyClient) PutSequenced(name upspin.PathName, seq int64, data []byte) (*upspin.DirEntry, error) {
+	d.putData = make([]byte, len(data))
+	copy(d.putData, data)
+	return nil, nil
+}
 func (d *dummyClient) PutLink(oldName, newName upspin.PathName) (*upspin.DirEntry, error) {
 	return nil, nil
 }
diff --git a/upspin/upspin.go b/upspin/upspin.go
index 9439694..0a6ee62 100644
--- a/upspin/upspin.go
+++ b/upspin/upspin.go
@@ -710,6 +710,20 @@
 	// new sequence number.
 	Put(name PathName, data []byte) (*DirEntry, error)
 
+	// PutSequenced stores the data at the given name only if
+	// there is no preexisting data stored with that name or if the
+	// sequence number of the preexisting data matches that given.
+	// PutSequenced with SeqIgnore is the same as Put.
+	// On success any preexisting data will no longer be available using
+	// the name, although it may still exist in the storage server. (See
+	// the documentation for Delete.) Like Get, it is not the usual
+	// access method. The file-like API is preferred.
+	//
+	// A successful PutSequenced returns an incomplete DirEntry (see the
+	// description of AttrIncomplete) containing only the
+	// new sequence number.
+	PutSequenced(name PathName, seq int64, data []byte) (*DirEntry, error)
+
 	// PutLink creates a link from the new name to the old name. The
 	// new name must not look like the path to an Access or Group file.
 	// If something is already stored with the new name, it is first