Add upload and download to node-files' verbs, as ADR 0468 §9 decides, so mesh-files may serve them (issue 1718)
mesh/merge-gate head 90b20f5a3bda merged into trunk 85b5a2687e6a — pass, with a note: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head 90b20f5a3bda merged into trunk 85b5a2687e6a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
mesh/merge-gate head 90b20f5a3bda merged into trunk 85b5a2687e6a — pass, with a note: builds mesh-sdk (+41 dependent(s)) → ace, g14, novox, shanks; no bus …
mesh/repo-check head 90b20f5a3bda merged into trunk 85b5a2687e6a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/delivery delivered
This commit is contained in:
@@ -74,6 +74,22 @@ func filesVerbs() []Verb {
|
|||||||
map[string]string{"path": "an absolute path", "owner": "an account's name or number",
|
map[string]string{"path": "an absolute path", "owner": "an account's name or number",
|
||||||
"group": "a group's name or number (optional)", "recursive": "\"true\": also everything under a directory"},
|
"group": "a group's name or number (optional)", "recursive": "\"true\": also everything under a directory"},
|
||||||
[]string{"path", "owner"}, []string{"group", "recursive"}, "chown", "chgrp"),
|
[]string{"path", "owner"}, []string{"group", "recursive"}, "chown", "chgrp"),
|
||||||
|
Verb{Name: "upload", Description: "Send one file of this node into a bucket of the object store, over a short signed link " +
|
||||||
|
"and never over the bus; answers the key (the file's SHA-256 when none is given), the size and the SHA-256. " +
|
||||||
|
"At most 100 MiB. A place a secret is kept, a known credential file, or a link to one is refused by name, " +
|
||||||
|
"before anything is sent. Read " + filesAsAccount + ".",
|
||||||
|
Input: schema(map[string]string{"path": "an absolute path to a file", "bucket": "the bucket the file goes into",
|
||||||
|
"key": "the object's key (optional; the file's SHA-256 when not given)"}, []string{"path", "bucket"})},
|
||||||
|
// download is a plain Verb, not through write(): write() types its optional arguments as true/false
|
||||||
|
// switches, which a hash and a reason are not (issue 1719).
|
||||||
|
Verb{Name: "download", Description: "Write one object of a bucket to a file of this node, over a short signed link and " +
|
||||||
|
"never over the bus; the old file goes to the node's trash. At most 100 MiB. The bytes must hash to sha256, or to " +
|
||||||
|
"the key when the key is a SHA-256; otherwise no file is written. Refuses a path a module owns " +
|
||||||
|
"unless override names a reason; done as the node's operator account, through sudo -n where its rights do not reach.",
|
||||||
|
Input: schema(map[string]string{"bucket": "the bucket", "key": "the object's key", "path": "an absolute path",
|
||||||
|
"sha256": "the SHA-256 the bytes must have (optional; the key when that is a SHA-256)",
|
||||||
|
"reason": "why the file is changed, for the audit trail",
|
||||||
|
"override": overrideWord}, []string{"bucket", "key", "path", "reason"})},
|
||||||
Verb{Name: "trash", Description: "What the node's trash holds: each deleted or replaced item with its id, " +
|
Verb{Name: "trash", Description: "What the node's trash holds: each deleted or replaced item with its id, " +
|
||||||
"original path and when it went.",
|
"original path and when it went.",
|
||||||
Input: schema(map[string]string{}, nil), Replaces: []string{"ls ~/.local/share/Trash"}},
|
Input: schema(map[string]string{}, nil), Replaces: []string{"ls ~/.local/share/Trash"}},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package catalogue
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -22,7 +23,8 @@ func TestNodeFilesIsANodeSeatWithEveryVerbOfADR0394(t *testing.T) {
|
|||||||
if v.Optional {
|
if v.Optional {
|
||||||
t.Errorf("%s.%s is optional", FilesSeat, v.Name)
|
t.Errorf("%s.%s is optional", FilesSeat, v.Name)
|
||||||
}
|
}
|
||||||
if len(v.Replaces) == 0 {
|
// upload and download (ADR 0468 §9, issue 1718) move bytes through the object store: no command is theirs.
|
||||||
|
if len(v.Replaces) == 0 && v.Name != "upload" && v.Name != "download" {
|
||||||
t.Errorf("%s.%s says nothing it replaces", FilesSeat, v.Name)
|
t.Errorf("%s.%s says nothing it replaces", FilesSeat, v.Name)
|
||||||
}
|
}
|
||||||
for _, c := range v.Replaces {
|
for _, c := range v.Replaces {
|
||||||
@@ -30,7 +32,7 @@ func TestNodeFilesIsANodeSeatWithEveryVerbOfADR0394(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, name := range []string{"stat", "readlink", "list", "read", "search", "account", "write", "copy", "move",
|
for _, name := range []string{"stat", "readlink", "list", "read", "search", "account", "write", "copy", "move",
|
||||||
"delete", "mkdir", "chmod", "chown", "trash", "restore"} {
|
"delete", "mkdir", "chmod", "chown", "upload", "download", "trash", "restore"} {
|
||||||
if !have[name] {
|
if !have[name] {
|
||||||
t.Errorf("%s serves no verb %s", FilesSeat, name)
|
t.Errorf("%s serves no verb %s", FilesSeat, name)
|
||||||
}
|
}
|
||||||
@@ -109,3 +111,93 @@ func TestNodeFilesRestoreTakesIdAndReason(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Error("no restore verb")
|
t.Error("no restore verb")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1718/AC1: node-files names upload and download, in mesh-files' order, before trash.
|
||||||
|
func TestNodeFilesNamesUploadAndDownloadBeforeTrash(t *testing.T) {
|
||||||
|
s, _ := SeatNamed(FilesSeat)
|
||||||
|
var names []string
|
||||||
|
for _, v := range s.Serves {
|
||||||
|
names = append(names, v.Name)
|
||||||
|
}
|
||||||
|
joined := strings.Join(names, ",")
|
||||||
|
if !strings.Contains(joined, "chown,upload,download,trash") {
|
||||||
|
t.Errorf("1718/AC1: node-files' verbs are %s, want upload and download between chown and trash", joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1718/AC2: a holder serving every verb including upload and download is accepted; one naming a verb outside the
|
||||||
|
// contract is still refused, and one leaving upload out is refused.
|
||||||
|
func TestAHolderOfNodeFilesServesUploadAndDownloadAndNothingElse(t *testing.T) {
|
||||||
|
s, _ := SeatNamed(FilesSeat)
|
||||||
|
var verbs []string
|
||||||
|
for _, v := range s.Serves {
|
||||||
|
verbs = append(verbs, v.Name)
|
||||||
|
}
|
||||||
|
hold := func(serves []string) error {
|
||||||
|
return CanHold(Manifest{Module: "mesh-files", Version: "1",
|
||||||
|
Claims: []Claim{{Name: FilesSeat, Scope: ScopeNode, Serves: serves}}}, s)
|
||||||
|
}
|
||||||
|
if err := hold(verbs); err != nil {
|
||||||
|
t.Errorf("1718/AC2: a holder serving every verb was refused: %v", err)
|
||||||
|
}
|
||||||
|
if err := hold(append(append([]string{}, verbs...), "sync")); err == nil || !strings.Contains(err.Error(), "does not promise") {
|
||||||
|
t.Errorf("1718/AC2: a verb outside the contract was accepted: %v", err)
|
||||||
|
}
|
||||||
|
var without []string
|
||||||
|
for _, v := range verbs {
|
||||||
|
if v != "upload" {
|
||||||
|
without = append(without, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := hold(without); err == nil || !strings.Contains(err.Error(), "upload") {
|
||||||
|
t.Errorf("1718/AC2: a holder leaving upload out was accepted: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1718/AC3: upload requires path and bucket; download requires bucket, key, path and reason; sha256, key (upload)
|
||||||
|
// and override are plain strings, not true/false switches.
|
||||||
|
func TestNodeFilesUploadAndDownloadInputs(t *testing.T) {
|
||||||
|
s, _ := SeatNamed(FilesSeat)
|
||||||
|
want := map[string]struct {
|
||||||
|
required string
|
||||||
|
strs []string
|
||||||
|
}{
|
||||||
|
"upload": {"path,bucket", []string{"path", "bucket", "key"}},
|
||||||
|
"download": {"bucket,key,path,reason", []string{"bucket", "key", "path", "sha256", "reason", "override"}},
|
||||||
|
}
|
||||||
|
seen := 0
|
||||||
|
for _, v := range s.Serves {
|
||||||
|
w, ok := want[v.Name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen++
|
||||||
|
req, _ := v.Input["required"].([]string)
|
||||||
|
got := append([]string{}, req...)
|
||||||
|
sortStrings(got)
|
||||||
|
exp := strings.Split(w.required, ",")
|
||||||
|
sortStrings(exp)
|
||||||
|
if strings.Join(got, ",") != strings.Join(exp, ",") {
|
||||||
|
t.Errorf("1718/AC3: %s requires %v, want %s", v.Name, req, w.required)
|
||||||
|
}
|
||||||
|
props, _ := v.Input["properties"].(map[string]any)
|
||||||
|
if len(props) != len(w.strs) {
|
||||||
|
t.Errorf("1718/AC3: %s has %d inputs, want %v", v.Name, len(props), w.strs)
|
||||||
|
}
|
||||||
|
for _, name := range w.strs {
|
||||||
|
p, has := props[name].(map[string]any)
|
||||||
|
if !has {
|
||||||
|
t.Errorf("1718/AC3: %s has no input %s", v.Name, name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, enum := p["enum"]; enum || p["type"] != "string" {
|
||||||
|
t.Errorf("1718/AC3: %s input %s is %v, want a plain string", v.Name, name, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seen != 2 {
|
||||||
|
t.Errorf("1718/AC3: found %d of upload and download", seen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortStrings(s []string) { sort.Strings(s) }
|
||||||
|
|||||||
Reference in New Issue
Block a user