From 63caf9ed03c04167c9472bd7e0b7574e2fe57aeb Mon Sep 17 00:00:00 2001 From: Bel LaPointe Date: Wed, 16 Feb 2022 07:58:34 -0700 Subject: [PATCH] merge --- spike/review/reinvent/ezmded/server/id.go | 51 ++++++++ spike/review/reinvent/ezmded/server/server.go | 76 +++++------- .../reinvent/ezmded/server/server_test.go | 25 +++- spike/review/reinvent/ezmded/server/todo.yaml | 2 +- spike/review/reinvent/ezmded/server/tree.go | 58 +++------ .../reinvent/ezmded/server/tree_test.go | 76 ++---------- .../ezmded/ui/templates/_filetree.ctmpl | 6 +- .../ezmded/ui/templates/_import.ctmpl | 116 ++++++++++-------- 8 files changed, 199 insertions(+), 211 deletions(-) create mode 100644 spike/review/reinvent/ezmded/server/id.go diff --git a/spike/review/reinvent/ezmded/server/id.go b/spike/review/reinvent/ezmded/server/id.go new file mode 100644 index 0000000..042dee4 --- /dev/null +++ b/spike/review/reinvent/ezmded/server/id.go @@ -0,0 +1,51 @@ +package main + +import ( + "net/url" + "os" + "path" + "strings" +) + +type ID string + +func NewID(s string) ID { + return ID(path.Clean(s)).withClean() +} + +func (id ID) Push(child string) ID { + return ID(path.Join(id.String(), child)).withClean() +} + +func (id ID) Pop() ID { + pid := path.Clean(ID(path.Dir(id.String())).withClean().String()) + if strings.HasPrefix(pid, ".") { + return "" + } + return NewID(pid) +} + +func (id ID) URLSafeString() string { + splits := strings.Split(string(id), "/") + for i := range splits { + splits[i] = url.PathEscape(splits[i]) + } + return strings.Join(splits, "/") +} + +func (id ID) String() string { + return string(id) +} + +func (id ID) withClean() ID { + splits := strings.Split(id.String(), string([]rune{os.PathSeparator})) + for i := range splits { + splits[i] = strings.Trim(splits[i], string([]rune{os.PathSeparator})) + splits[i] = strings.Trim(splits[i], "/") + t, err := url.PathUnescape(splits[i]) + if err == nil { + splits[i] = t + } + } + return ID(path.Join(splits...)) +} diff --git a/spike/review/reinvent/ezmded/server/server.go b/spike/review/reinvent/ezmded/server/server.go index 96a21e8..12f0ae0 100644 --- a/spike/review/reinvent/ezmded/server/server.go +++ b/spike/review/reinvent/ezmded/server/server.go @@ -12,7 +12,6 @@ import ( "local/simpleserve/simpleserve" "log" "net/http" - "net/url" "os" "path" "regexp" @@ -201,7 +200,6 @@ func (server *Server) putContentHandler(filePath string, w http.ResponseWriter, } func (server *Server) uiSearchHandler(w http.ResponseWriter, r *http.Request) error { - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/ui/files") t, err := server.uiSubTemplates() if err != nil { return err @@ -238,9 +236,8 @@ func (server *Server) uiSearchHandler(w http.ResponseWriter, r *http.Request) er } func (server *Server) uiFilesHandler(w http.ResponseWriter, r *http.Request) error { - r.URL.Path = strings.TrimPrefix(r.URL.Path, "/ui/files") - id := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/") - if len(id) == 0 || (len(id) == 1 && id[0] == "") { + id := NewID(strings.TrimPrefix(r.URL.Path, "/ui/files")) + if id == "" { return server.rootHandler(w, r) } t, err := server.uiSubTemplates() @@ -261,25 +258,25 @@ func (server *Server) uiFilesHandler(w http.ResponseWriter, r *http.Request) err return err } var parent Leaf - if len(id) > 1 { - parent, err = tree.Get(id[:len(id)-1]) + if id.Pop() != "" { + parent, err = tree.Get(id.Pop()) if err != nil { - return err + return fmt.Errorf("failed to get pid %q: %v", id.Pop(), err) } } leaf, err := tree.Get(id) if err != nil { - if len(id) > 1 && parent.Title == "" { - return err + if id.Pop() != "" { + return fmt.Errorf("failed to get id %q: %v", id, err) } leaf.Title = "My New File" } data := map[string]interface{}{ - "This": map[string]string{ + "This": map[string]interface{}{ "Title": leaf.Title, "Content": leaf.Content, - "ID": ID, - "PID": ID.Pop(), + "ID": id, + "PID": id.Pop(), "PTitle": parent.Title, }, "Tree": string(branchesJSON), @@ -362,20 +359,20 @@ func (server *Server) apiV0FilesPostHandler(w http.ResponseWriter, r *http.Reque } pid := server.fileId(r) - id := append(pid, strings.Split(uuid.New().String(), "-")[0]) + id := NewID(pid).Push(strings.Split(uuid.New().String(), "-")[0]) if err := server.tree().Put(id, Leaf{Title: r.Header.Get("Title"), Content: string(b)}); err != nil { return err } return json.NewEncoder(w).Encode(map[string]map[string]string{ "data": map[string]string{ - "filePath": path.Join("/api/v0/files/", server.urlFileId(id)), + "filePath": path.Join("/api/v0/files/", id.URLSafeString()), }, }) } func (server *Server) apiV0FilesIDGetHandler(w http.ResponseWriter, r *http.Request) error { - id := server.fileId(r) - if len(id) == 0 || id[0] == "" { + id := NewID(server.fileId(r)) + if id.String() == "" { return fmt.Errorf("no id found: %+v", id) } @@ -393,8 +390,8 @@ func (server *Server) apiV0FilesIDGetHandler(w http.ResponseWriter, r *http.Requ } func (server *Server) apiV0FilesIDDelHandler(w http.ResponseWriter, r *http.Request) error { - id := server.fileId(r) - if len(id) == 0 || id[0] == "" { + id := NewID(server.fileId(r)) + if id.String() == "" { return fmt.Errorf("no id found: %+v", id) } @@ -409,32 +406,19 @@ func (server *Server) apiV0FilesIDDelHandler(w http.ResponseWriter, r *http.Requ return server.tree().Put(id, leaf) } -func (server *Server) urlFileId(id []string) string { - if len(id) == 0 { - return "" - } - result := id[0] - for i := 1; i < len(id); i++ { - result = strings.Join([]string{result, url.PathEscape(id[i])}, "/") - } - return result -} - -func (server *Server) fileId(r *http.Request) []string { - return strings.Split( - strings.Trim( - strings.TrimPrefix( - strings.Trim(r.URL.Path, "/"), - "api/v0/files", - ), - "/"), +func (server *Server) fileId(r *http.Request) string { + return strings.Trim( + strings.TrimPrefix( + strings.Trim(r.URL.Path, "/"), + "api/v0/files", + ), "/", ) } func (server *Server) apiV0FilesIDPutHandler(w http.ResponseWriter, r *http.Request) error { - id := server.fileId(r) - if len(id) == 0 || id[0] == "" { + id := NewID(server.fileId(r)) + if id.String() == "" { return fmt.Errorf("no id found: %+v", id) } @@ -457,7 +441,7 @@ func (server *Server) apiV0FilesIDPutHandler(w http.ResponseWriter, r *http.Requ } return json.NewEncoder(w).Encode(map[string]map[string]string{ "data": map[string]string{ - "filePath": path.Join("/api/v0/files/", server.urlFileId(id)), + "filePath": path.Join("/api/v0/files/", id.URLSafeString()), }, }) } @@ -496,21 +480,23 @@ func (server *Server) _apiV0SearchHandler(query string) ([][2]string, error) { return nil, err } result := [][2]string{} - if err := tree.ForEach(func(id []string, leaf Leaf) error { + if err := tree.ForEach(func(id ID, leaf Leaf) error { for _, pattern := range patterns { if !pattern.MatchString(leaf.Content) && !pattern.MatchString(leaf.Title) { return nil } } title := leaf.Title - for i := len(id) - 1; i >= 1; i-- { - parent, err := server.tree().Get(id[:i]) + pid := id.Pop() + for pid != "" { + parent, err := server.tree().Get(pid) if err != nil { return err } title = path.Join(parent.Title, title) + pid = pid.Pop() } - result = append(result, [2]string{server.urlFileId(id), title}) + result = append(result, [2]string{id.URLSafeString(), title}) return nil }); err != nil { return nil, err diff --git a/spike/review/reinvent/ezmded/server/server_test.go b/spike/review/reinvent/ezmded/server/server_test.go index ef10966..fae9df0 100644 --- a/spike/review/reinvent/ezmded/server/server_test.go +++ b/spike/review/reinvent/ezmded/server/server_test.go @@ -31,11 +31,11 @@ func TestServerRoutes(t *testing.T) { ensureAndWrite(server.diskMediaPath("delid"), []byte("hi")) tree := server.tree() - if err := tree.Put([]string{"getfid"}, Leaf{Title: "", Content: "getfid body"}); err != nil { + if err := tree.Put(ID("getfid"), Leaf{Title: "", Content: "getfid body"}); err != nil { t.Fatal(err) } - tree.Put([]string{"putfid"}, Leaf{Title: "putfid title", Content: "initial putfid body"}) - tree.Put([]string{"delfid"}, Leaf{Title: "delfid title", Content: "delfid body"}) + tree.Put(ID("putfid"), Leaf{Title: "putfid title", Content: "initial putfid body"}) + tree.Put(ID("delfid"), Leaf{Title: "delfid title", Content: "delfid body"}) t.Log(tree.GetRoot()) ensureAndWrite(path.Join(server.root, "index.html"), []byte("mom")) @@ -151,7 +151,7 @@ func TestServerPutTreeGetFile(t *testing.T) { if err := server.Routes(); err != nil { t.Fatal(err) } - server.tree().Put([]string{"my pid"}, Leaf{}) + server.tree().Put(ID("my pid"), Leaf{}) var id string t.Run("put to create an id", func(t *testing.T) { r := httptest.NewRequest(http.MethodPut, "/my%20pid/my-put-id", strings.NewReader("body")) @@ -210,6 +210,23 @@ func TestServerPutTreeGetFile(t *testing.T) { if !bytes.Contains(w.Body.Bytes(), []byte(`{"Title":"my title","Deleted":false,"Content":"`)) { t.Fatal(w) } + var branch Branch + if err := json.NewDecoder(w.Body).Decode(&branch); err != nil { + t.Fatal(err) + } + t.Logf("TODO: %+v", branch) + if branch.Leaf != (Leaf{}) { + t.Error(branch.Leaf) + } + if parent, ok := branch.Branches["my pid"]; !ok { + t.Error(ok, branch) + } else if parent.Leaf.Title != "Untitled" { + t.Error(parent.Leaf) + } else if child, ok := parent.Branches[NewID(id)]; !ok { + t.Error(ok, NewID("my pid").Push(id), parent) + } else if child.Leaf.Title != "my title" { + t.Error(child.Leaf) + } }) t.Run("get", func(t *testing.T) { r := httptest.NewRequest(http.MethodGet, "/"+url.PathEscape(id), nil) diff --git a/spike/review/reinvent/ezmded/server/todo.yaml b/spike/review/reinvent/ezmded/server/todo.yaml index 10aa692..90ca4d6 100644 --- a/spike/review/reinvent/ezmded/server/todo.yaml +++ b/spike/review/reinvent/ezmded/server/todo.yaml @@ -6,7 +6,6 @@ todo: - delete button does nothing - do not rewrite .md title vs. link cause hrefs to ./gobs.md wont work - https://codepen.io/bisserof/pen/nrMveb -- breadcrumb; https://concisecss.com/documentation/ui - alert box; https://concisecss.com/documentation/ui - scrape odo - only one scroll bar @@ -28,3 +27,4 @@ done: - preview default via q param - only 1 pid link in tree as title - fix images +- breadcrumb; https://concisecss.com/documentation/ui diff --git a/spike/review/reinvent/ezmded/server/tree.go b/spike/review/reinvent/ezmded/server/tree.go index d85f5f0..15e02ba 100644 --- a/spike/review/reinvent/ezmded/server/tree.go +++ b/spike/review/reinvent/ezmded/server/tree.go @@ -10,37 +10,23 @@ import ( type Branch struct { Leaf Leaf - Branches map[string]Branch + Branches map[ID]Branch } func (branch Branch) IsZero() bool { return branch.Leaf == (Leaf{}) && len(branch.Branches) == 0 } -func (branch Branch) Find(baseId string) ([]string, bool) { - if _, ok := branch.Branches[baseId]; ok { - return []string{baseId}, true - } - for pid, child := range branch.Branches { - if subids, ok := child.Find(baseId); ok { - return append([]string{pid}, subids...), true - } - } - return nil, false +func (branch Branch) ForEach(foo func(ID, Leaf) error) error { + return branch.forEach(NewID(""), foo) } -func (branch Branch) ForEach(foo func([]string, Leaf) error) error { - return branch.forEach([]string{}, foo) -} - -func (branch Branch) forEach(preid []string, foo func([]string, Leaf) error) error { +func (branch Branch) forEach(preid ID, foo func(ID, Leaf) error) error { if err := foo(preid, branch.Leaf); err != nil { return err } - postid := append(preid, "") for id, child := range branch.Branches { - postid[len(postid)-1] = id - if err := child.forEach(postid, foo); err != nil { + if err := child.forEach(preid.Push(string(id)), foo); err != nil { return err } } @@ -80,31 +66,23 @@ func (tree Tree) WithRoot(root string) Tree { return tree } -func (tree Tree) Find(baseId string) ([]string, bool) { - root, err := tree.GetRoot() - if err != nil { - return nil, false - } - return root.Find(baseId) -} - func (tree Tree) GetRootMeta() (Branch, error) { - return tree.getRoot(false, false) + return tree.getRoot(NewID(""), false, false) } func (tree Tree) GetRoot() (Branch, error) { if !tree.cachedRoot.IsZero() { return tree.cachedRoot, nil } - got, err := tree.getRoot(true, false) + got, err := tree.getRoot(NewID(""), true, false) if err == nil { tree.cachedRoot = got } return got, err } -func (tree Tree) getRoot(withContent, withDeleted bool) (Branch, error) { - m := Branch{Branches: map[string]Branch{}} +func (tree Tree) getRoot(pid ID, withContent, withDeleted bool) (Branch, error) { + m := Branch{Branches: map[ID]Branch{}} entries, err := os.ReadDir(tree.root) if os.IsNotExist(err) { return m, nil @@ -127,25 +105,25 @@ func (tree Tree) getRoot(withContent, withDeleted bool) (Branch, error) { } } else if entry.IsDir() { subtree := tree.WithRoot(path.Join(tree.root, entry.Name())) - if branch, err := subtree.getRoot(withContent, withDeleted); err != nil { + if branch, err := subtree.getRoot(pid.Push(entry.Name()), withContent, withDeleted); err != nil { return Branch{}, err } else if !branch.IsZero() && (!branch.Leaf.Deleted || withDeleted) { - m.Branches[entry.Name()] = branch + m.Branches[pid.Push(entry.Name())] = branch } } } return m, nil } -func (tree Tree) toDir(id []string) string { +func (tree Tree) toDir(id ID) 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) toData(id ID) string { + return path.Join(tree.root, string(id), "data.yaml") } -func (tree Tree) Put(id []string, input Leaf) error { +func (tree Tree) Put(id ID, input Leaf) error { if _, err := os.Stat(tree.toData(id)); os.IsNotExist(err) { b, err := yaml.Marshal(Leaf{}) if err != nil { @@ -170,7 +148,7 @@ func (tree Tree) Put(id []string, input Leaf) error { return nil } -func (tree Tree) Del(id []string) error { +func (tree Tree) Del(id ID) error { got, err := tree.Get(id) if os.IsNotExist(err) { return nil @@ -185,13 +163,13 @@ func (tree Tree) Del(id []string) error { return tree.Put(id, got) } -func (tree Tree) HardDel(id []string) error { +func (tree Tree) HardDel(id ID) error { os.RemoveAll(tree.toDir(id)) tree.cachedRoot = Branch{} return nil } -func (tree Tree) Get(id []string) (Leaf, error) { +func (tree Tree) Get(id ID) (Leaf, error) { f, err := os.Open(tree.toData(id)) if err != nil { return Leaf{}, err diff --git a/spike/review/reinvent/ezmded/server/tree_test.go b/spike/review/reinvent/ezmded/server/tree_test.go index 184c001..a13b0b4 100644 --- a/spike/review/reinvent/ezmded/server/tree_test.go +++ b/spike/review/reinvent/ezmded/server/tree_test.go @@ -3,22 +3,21 @@ package main import ( "encoding/json" "fmt" - "path" "testing" ) func TestTreeDel(t *testing.T) { tree := NewTree(t.TempDir()) - if err := tree.Put([]string{"id"}, Leaf{}); err != nil { + if err := tree.Put(ID("id"), Leaf{}); err != nil { t.Fatal(err) } - if err := tree.Put([]string{"id", "subid"}, Leaf{}); err != nil { + if err := tree.Put(ID("id/subid"), Leaf{}); err != nil { t.Fatal(err) } - if err := tree.Del([]string{"id"}); err != nil { + if err := tree.Del(ID("id")); err != nil { t.Fatal(err) - } else if got, err := tree.Get([]string{"id"}); err != nil { + } else if got, err := tree.Get(ID("id")); err != nil { t.Fatal(err) } else if !got.Deleted { t.Fatal(got) @@ -30,7 +29,7 @@ func TestTreeDel(t *testing.T) { t.Fatal(root.Branches) } - if root, err := tree.getRoot(false, true); err != nil { + if root, err := tree.getRoot(NewID(""), false, true); err != nil { t.Fatal(err) } else if len(root.Branches) != 1 { t.Fatal(root.Branches) @@ -46,7 +45,7 @@ func TestTreeCrud(t *testing.T) { t.Fatal(m) } - if err := tree.Del([]string{"id"}); err != nil { + if err := tree.Del(ID("id")); err != nil { t.Fatal(err) } @@ -55,9 +54,9 @@ func TestTreeCrud(t *testing.T) { Deleted: false, Content: "leaf content", } - if err := tree.Put([]string{"id"}, want); err != nil { + if err := tree.Put(ID("id"), want); err != nil { t.Fatal(err) - } else if l, err := tree.Get([]string{"id"}); err != nil { + } else if l, err := tree.Get(ID("id")); err != nil { t.Fatal(err) } else if l != want { t.Fatal(want, l) @@ -73,62 +72,3 @@ func TestTreeCrud(t *testing.T) { t.Fatalf("without content == with content: \n\twith=%s\n\twout=%s", with, without) } } - -func TestBranchFind(t *testing.T) { - cases := map[string]struct { - input string - want []string - found bool - branch Branch - }{ - "empty": { - input: "id", - want: nil, - found: false, - branch: Branch{}, - }, - "yes top level": { - input: "id", - want: []string{"id"}, - found: true, - branch: Branch{ - Branches: map[string]Branch{"id": Branch{}}, - }, - }, - "yes deep level": { - input: "subsubid", - want: []string{"id", "subid", "subsubid"}, - found: true, - branch: Branch{ - Branches: map[string]Branch{"id": Branch{ - Branches: map[string]Branch{"subid": Branch{ - Branches: map[string]Branch{"subsubid": Branch{}}}}, - }}, - }, - }, - "no but has deep levels": { - input: "notsubsubid", - want: nil, - found: false, - branch: Branch{ - Branches: map[string]Branch{"id": Branch{ - Branches: map[string]Branch{"subid": Branch{ - Branches: map[string]Branch{"subsubid": Branch{}}}}, - }}, - }, - }, - } - - for name, d := range cases { - c := d - t.Run(name, func(t *testing.T) { - got, found := c.branch.Find(c.input) - if found != c.found { - t.Error(c.found, found) - } - if path.Join(got...) != path.Join(c.want...) { - t.Error(c.want, got) - } - }) - } -} diff --git a/spike/review/reinvent/ezmded/ui/templates/_filetree.ctmpl b/spike/review/reinvent/ezmded/ui/templates/_filetree.ctmpl index 23f3e26..9f3f9b8 100644 --- a/spike/review/reinvent/ezmded/ui/templates/_filetree.ctmpl +++ b/spike/review/reinvent/ezmded/ui/templates/_filetree.ctmpl @@ -46,7 +46,7 @@ return `
- + ` } function branchesHTML(id, branches) { @@ -55,7 +55,7 @@ var out = `` for(var i in branches) { out += `
` - out += branchHTML((id ? id + "/" : "") + i, branches[i]) + out += branchHTML(i, branches[i]) out += `
` } return out @@ -66,6 +66,6 @@ n += 1 return n > 0 } - drawTree(JSON.parse(`{{ .Tree }}`)) + drawTree(JSON.parse({{ .Tree }})) {{ end }} diff --git a/spike/review/reinvent/ezmded/ui/templates/_import.ctmpl b/spike/review/reinvent/ezmded/ui/templates/_import.ctmpl index 30cdfaa..26d185d 100644 --- a/spike/review/reinvent/ezmded/ui/templates/_import.ctmpl +++ b/spike/review/reinvent/ezmded/ui/templates/_import.ctmpl @@ -15,48 +15,48 @@ --> {{ end }}