new tree crud test

master
Bel LaPointe 2022-02-09 11:20:45 -07:00
parent ba154be6c2
commit eedb6cc6a5
4 changed files with 119 additions and 442 deletions

View File

@ -12,10 +12,8 @@ import (
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/google/uuid"
)
@ -83,7 +81,8 @@ func (server *Server) apiV0TreeHandler(w http.ResponseWriter, r *http.Request) e
func (server *Server) apiV0TreePrettyHandler(w http.ResponseWriter, r *http.Request) error {
tree := server.tree()
branches, err := tree.GetPretty()
//branches, err := tree.GetPretty()
branches, err := tree.GetRoot()
if err != nil {
return err
}
@ -92,7 +91,7 @@ func (server *Server) apiV0TreePrettyHandler(w http.ResponseWriter, r *http.Requ
func (server *Server) apiV0TreePlainHandler(w http.ResponseWriter, r *http.Request) error {
tree := server.tree()
branches, err := tree.Get()
branches, err := tree.GetRoot()
if err != nil {
return err
}
@ -137,12 +136,7 @@ func (server *Server) apiV0MediaIDDelHandler(w http.ResponseWriter, r *http.Requ
func (server *Server) apiV0MediaIDGetHandler(w http.ResponseWriter, r *http.Request) error {
id := path.Base(r.URL.Path)
tree := server.tree()
fullId, err := tree.FullId(id)
if err != nil {
return err
}
return server.getContentHandler(server.diskMediaPath(fullId), w, r)
return server.getContentHandler(server.diskMediaPath(id), w, r)
}
func (server *Server) getContentHandler(filePath string, w http.ResponseWriter, r *http.Request) error {
@ -217,15 +211,7 @@ func (server *Server) rootHandler(w http.ResponseWriter, r *http.Request) error
}
func (server *Server) tree() *Tree {
return NewTree(path.Dir(server.diskFileDir("id")))
}
func (server *Server) diskFileDir(id string) string {
return path.Dir(server.diskFilePath(id))
}
func (server *Server) diskFilePath(id string) string {
return path.Join(server.root, "files", id, "data")
return nil
}
func (server *Server) diskMediaPath(id string) string {
@ -233,109 +219,11 @@ func (server *Server) diskMediaPath(id string) string {
}
func (server *Server) apiV0FilesHandler(w http.ResponseWriter, r *http.Request) error {
fileDir := server.diskFileDir("id")
tree := server.tree()
if pid := r.Header.Get("PID"); pid == "" {
} else if fullId, err := tree.FullId(pid); err != nil {
return err
} else {
fileDir = server.diskFileDir(fullId)
}
id, err := server.postContentHandler(fileDir, w, r)
if err != nil {
return err
}
if err := tree.Put(id, Branch{
Title: r.Header.Get("Title"),
PID: r.Header.Get("PID"),
}); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]map[string]string{
"data": map[string]string{
"filePath": path.Join("/api/v0/files", id),
},
})
return errors.New("not impl")
}
func (server *Server) apiV0FilesIDHandler(w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case http.MethodPut:
return server.apiV0FilesIDPutHandler(w, r)
case http.MethodGet:
return server.apiV0FilesIDGetHandler(w, r)
case http.MethodDelete:
return server.apiV0FilesIDDelHandler(w, r)
}
http.NotFound(w, r)
return nil
}
func (server *Server) apiV0FilesIDPutHandler(w http.ResponseWriter, r *http.Request) error {
id := path.Base(r.URL.Path)
tree := server.tree()
branches, err := tree.Get()
if err != nil {
return err
}
branch, _ := branches[id]
branch.Updated = time.Now().UTC()
if title := r.Header.Get("Title"); title != "" {
branch.Title = title
}
if pid := r.Header.Get("PID"); pid != "" {
branch.PID = pid
}
branch.Deleted = false
fullPid, err := tree.FullId(branch.PID)
if err != nil {
return err
}
fullId := path.Join(fullPid, id)
if err := server.putContentHandler(server.diskFilePath(fullId), w, r); err != nil {
return err
}
if err := tree.Put(id, branch); err != nil {
return err
}
return json.NewEncoder(w).Encode(map[string]map[string]string{
"data": map[string]string{
"filePath": path.Join("/api/v0/files", id),
},
})
}
func (server *Server) apiV0FilesIDGetHandler(w http.ResponseWriter, r *http.Request) error {
tree := server.tree()
branches, err := tree.Get()
if err != nil {
return err
}
id := path.Base(r.URL.Path)
fullId, err := tree.FullId(id)
if err != nil {
return err
}
branch, _ := branches[id]
w.Header().Set("Title", branch.Title)
w.Header().Set("PID", branch.PID)
return server.getContentHandler(server.diskFilePath(fullId), w, r)
}
func (server *Server) apiV0FilesIDDelHandler(w http.ResponseWriter, r *http.Request) error {
tree := server.tree()
branches, err := tree.Get()
if err != nil {
return err
}
id := path.Base(r.URL.Path)
branch, ok := branches[id]
if !ok {
return nil
}
branch.Deleted = true
branch.Updated = time.Now().UTC()
return tree.Put(id, branch)
return errors.New("not impl")
}
func (server *Server) apiV0SearchHandler(w http.ResponseWriter, r *http.Request) error {
@ -357,30 +245,5 @@ func (server *Server) apiV0SearchHandler(w http.ResponseWriter, r *http.Request)
w.Write([]byte(`[]`))
return nil
}
results := []string{}
if err := filepath.Walk(server.diskFileDir("id"), func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if info.Name() != "data" {
return nil
}
b, err := ioutil.ReadFile(p)
if err != nil {
return err
}
for _, pattern := range patterns {
if !pattern.Match(b) {
return nil
}
}
results = append(results, path.Base(p))
return err
}); err != nil {
return err
}
return json.NewEncoder(w).Encode(results)
return errors.New("not impl")
}

View File

@ -1,15 +1,6 @@
package main
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"
)
/*
func TestServerRoutes(t *testing.T) {
server := NewServer(t.TempDir())
if err := server.Routes(); err != nil {
@ -198,3 +189,4 @@ func TestServerPutTreeGetFile(t *testing.T) {
}
})
}
*/

View File

@ -1,190 +1,130 @@
package main
import (
"errors"
"io/ioutil"
"os"
"path"
"path/filepath"
"time"
yaml "gopkg.in/yaml.v2"
)
var errChainNotFound = errors.New("pid chain not found")
type Branch struct {
Leaf Leaf
Branches map[string]Branch
}
func (branch Branch) IsZero() bool {
return branch.Leaf == (Leaf{}) && len(branch.Branches) == 0
}
type Leaf struct {
Title string
Deleted bool
Content string
}
func (base Leaf) Merge(updated Leaf) Leaf {
if updated.Title != "" {
base.Title = updated.Title
}
base.Deleted = updated.Deleted
base.Content = updated.Content
return base
}
type Tree struct {
root string
cachedRoot Branch
}
type Branch struct {
Title string
Deleted bool
Updated time.Time
PID string
func NewTree(root string) Tree {
return Tree{root: root}
}
type Pretty struct {
Children map[string]*Pretty
Title string
func (tree Tree) WithRoot(root string) Tree {
tree.root = root
return tree
}
func NewTree(root string) *Tree {
return &Tree{
root: root,
func (tree Tree) GetRoot() (Branch, error) {
if !tree.cachedRoot.IsZero() {
return tree.cachedRoot, nil
}
got, err := tree.getRoot()
if err == nil {
tree.cachedRoot = got
}
return got, err
}
func (tree *Tree) GetPretty() (map[string]*Pretty, error) {
branches, err := tree.Get()
if err != nil {
return nil, err
}
_ = branches
realpretty := map[string]*Pretty{}
lookuppretty := map[string]*Pretty{}
timesZero := 0
for len(branches) > 0 && timesZero < 3 {
topop := []string{}
for id, branch := range branches {
if branch.Deleted {
} else if branch.PID == "" {
realpretty[id] = &Pretty{
Title: branch.Title,
Children: map[string]*Pretty{},
}
lookuppretty[id] = realpretty[id]
} else if ptr, ok := lookuppretty[branch.PID]; ok {
ptr.Children[id] = &Pretty{
Title: branch.Title,
Children: map[string]*Pretty{},
}
lookuppretty[id] = ptr.Children[id]
} else {
continue
}
topop = append(topop, id)
}
for _, id := range topop {
delete(branches, id)
}
if len(topop) == 0 {
timesZero++
} else {
timesZero = 0
}
}
return realpretty, nil
}
func (tree *Tree) Get() (map[string]Branch, error) {
m := map[string]Branch{}
err := filepath.Walk(tree.root, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if info.Name() != "meta.yaml" {
return nil
}
b, err := ioutil.ReadFile(p)
if err != nil {
return err
}
var branch Branch
if err := yaml.Unmarshal(b, &branch); err != nil {
return err
}
id := path.Base(path.Dir(p))
pidFullPath := path.Dir(path.Dir(p))
if pidFullPath != tree.root {
branch.PID = path.Base(pidFullPath)
} else {
branch.PID = ""
}
m[id] = branch
return nil
})
func (tree Tree) getRoot() (Branch, error) {
m := Branch{Branches: map[string]Branch{}}
entries, err := os.ReadDir(tree.root)
if os.IsNotExist(err) {
return m, nil
}
return m, err
}
func (tree *Tree) FullId(id string) (string, error) {
fullId, _, err := tree.fullIdAndMeta(id)
return fullId, err
}
func (tree *Tree) fullIdAndMeta(id string) (string, Branch, error) {
m, err := tree.Get()
if err != nil {
return "", Branch{}, err
return Branch{}, err
}
branch, ok := m[id]
if !ok {
return "", Branch{}, errChainNotFound
for _, entry := range entries {
if entry.Name() == "data.yaml" {
if b, err := ioutil.ReadFile(path.Join(tree.root, entry.Name())); err != nil {
return Branch{}, err
} else if err := yaml.Unmarshal(b, &m.Leaf); err != nil {
return Branch{}, err
}
p := id
for branch.PID != "" {
p = path.Join(branch.PID, p)
branch, ok = m[branch.PID]
if !ok {
return "", Branch{}, errChainNotFound
} else if entry.IsDir() {
if branch, err := tree.WithRoot(path.Join(tree.root, entry.Name())).getRoot(); err != nil {
return Branch{}, err
} else {
m.Branches[entry.Name()] = branch
}
}
return path.Join(tree.root, p), branch, nil
}
return m, nil
}
func (tree *Tree) putMeta(fullId string, branch Branch) error {
b, err := yaml.Marshal(branch)
func (tree Tree) toDir(id []string) string {
return path.Dir(tree.toData(id))
}
func (tree Tree) toData(id []string) string {
return path.Join(tree.root, path.Join(id...), "data.yaml")
}
func (tree Tree) Put(id []string, input Leaf) error {
if _, err := os.Stat(tree.toData(id)); os.IsNotExist(err) {
b, err := yaml.Marshal(Leaf{})
if err != nil {
return err
}
return ensureAndWrite(path.Join(fullId, "meta.yaml"), b)
if err := ensureAndWrite(tree.toData(id), b); err != nil {
return err
}
}
old, err := tree.Get(id)
if err != nil {
return err
}
b, err := yaml.Marshal(old.Merge(input))
if err != nil {
return err
}
return ensureAndWrite(tree.toData(id), b)
}
func (tree *Tree) Del(id string) error {
fullId, meta, err := tree.fullIdAndMeta(id)
if err != nil {
if err == errChainNotFound {
func (tree Tree) Del(id []string) error {
os.RemoveAll(tree.toDir(id))
return nil
}
return err
}
meta.Updated = time.Now().UTC()
meta.Deleted = true
return tree.putMeta(fullId, meta)
}
func (tree *Tree) Put(id string, branch Branch) error {
fullId, meta, err := tree.fullIdAndMeta(id)
if err == errChainNotFound {
if branch.PID != "" {
fullId, _, err = tree.fullIdAndMeta(branch.PID)
fullId = path.Join(fullId, id)
} else {
err = nil
fullId = path.Join(tree.root, fullId, id)
}
}
func (tree Tree) Get(id []string) (Leaf, error) {
f, err := os.Open(tree.toData(id))
if err != nil {
return err
return Leaf{}, err
}
meta.Updated = time.Now().UTC()
if meta.Title == "" {
meta.Title = "Untitled (" + time.Now().UTC().String() + ")"
}
if branch.Title != "" {
meta.Title = branch.Title
}
meta.PID = branch.PID
meta.Deleted = branch.Deleted
return tree.putMeta(fullId, meta)
defer f.Close()
var got Leaf
err = yaml.NewDecoder(f).Decode(&got)
return got, err
}

View File

@ -1,148 +1,30 @@
package main
import (
"bytes"
"encoding/json"
"testing"
"time"
import "testing"
"go.mongodb.org/mongo-driver/bson"
)
func TestTree(t *testing.T) {
func TestTreeCrud(t *testing.T) {
tree := NewTree(t.TempDir())
t.Logf("tree.Get() from zero")
if m, err := tree.Get(); err != nil {
t.Fatal("failed to get empty tree:", err)
} else if m == nil {
t.Fatal("got a nil tree:", m)
if m, err := tree.GetRoot(); err != nil {
t.Fatal(err)
} else if m.Branches == nil {
t.Fatal(m)
}
t.Logf("tree.Del() from zero")
if err := tree.Del("id"); err != nil {
t.Fatal("failed to del a nil id:", err)
}
t.Logf("tree.Put(bad pid) from zero")
if err := tree.Put("id", Branch{PID: "fake"}); err == nil {
t.Fatal("failed to put with a fake pid:", err)
}
t.Logf("tree.Put() from zero")
if err := tree.Put("id", Branch{}); err != nil {
t.Fatal("failed to put with no pid:", err)
} else if branches, err := tree.Get(); err != nil {
t.Fatal("failed to get after put:", err)
} else if branch, ok := branches["id"]; !ok {
t.Fatal("got tree without put id:", branches)
} else if branch.Title == "" {
t.Fatal("got no default title", branch)
} else if time.Since(branch.Updated) > time.Hour {
t.Fatal("got not updated", branch)
} else if branch.Deleted {
t.Fatal("got deleted after put", branch)
}
t.Logf("tree.Put(good pid)")
if err := tree.Put("id2", Branch{PID: "id"}); err != nil {
if err := tree.Del([]string{"id"}); err != nil {
t.Fatal(err)
}
t.Logf("tree.Del(good pid)")
if err := tree.Del("id"); err != nil {
want := Leaf{
Title: "leaf title",
Deleted: true,
Content: "leaf content",
}
if err := tree.Put([]string{"id"}, want); err != nil {
t.Fatal(err)
} else if branches, err := tree.Get(); err != nil {
} else if l, err := tree.Get([]string{"id"}); err != nil {
t.Fatal(err)
} else if branch, ok := branches["id"]; !ok {
t.Fatal(ok)
} else if !branch.Deleted {
t.Fatal(branch)
}
}
func TestTreePretty(t *testing.T) {
tree := NewTree(t.TempDir())
tree.Put("A", Branch{Title: "A", PID: ""})
tree.Put("AA", Branch{Title: "AA", PID: "A", Deleted: true})
tree.Put("B", Branch{Title: "B", PID: ""})
tree.Put("BA", Branch{Title: "BA", PID: "B"})
tree.Put("BAA", Branch{Title: "BAA", PID: "BA", Deleted: true})
tree.Put("BB", Branch{Title: "BB", PID: "B"})
tree.Put("BBA", Branch{Title: "BBA", PID: "BB"})
tree.Put("BBB", Branch{Title: "BBB", PID: "BB"})
tree.Put("BBBC", Branch{Title: "BBBC", PID: "BBB"})
tree.Put("C", Branch{Title: "C", PID: "", Deleted: true})
tree.Put("D", Branch{Title: "D", PID: ""})
tree.Put("DA", Branch{Title: "DA", PID: "D"})
tree.Put("DAA", Branch{Title: "DAA", PID: "DA", Deleted: true})
tree.Put("DAAA", Branch{Title: "DAAA", PID: "DAA"})
got, err := tree.GetPretty()
if err != nil {
t.Fatal(err)
}
gotb, _ := json.MarshalIndent(got, "", " ")
t.Logf("%s", gotb)
/*
A
-AA
B
BA
-BAA
BB
BBA
BBB
BBBC
-C
D
DA
-DAA
DAAA
*/
want := bson.M{
"A": bson.M{
"Title": "A",
"Children": bson.M{},
},
"B": bson.M{
"Title": "B",
"Children": bson.M{
"BA": bson.M{
"Title": "BA",
"Children": bson.M{},
},
"BB": bson.M{
"Title": "BB",
"Children": bson.M{
"BBA": bson.M{
"Title": "BBA",
"Children": bson.M{},
},
"BBB": bson.M{
"Title": "BBB",
"Children": bson.M{
"BBBC": bson.M{
"Title": "BBBC",
"Children": bson.M{},
},
},
},
},
},
},
},
"D": bson.M{
"Title": "D",
"Children": bson.M{
"DA": bson.M{
"Title": "DA",
"Children": bson.M{},
},
},
},
}
wantb, _ := json.MarshalIndent(want, "", " ")
if !bytes.Equal(gotb, wantb) {
t.Fatalf("want:\n\t%s, got\n\t%s", wantb, gotb)
} else if l != want {
t.Fatal(want, l)
}
}