Compare commits
10 Commits
a6f5bc3192
...
4b7b3e3c68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b7b3e3c68 | ||
|
|
7ce2104b61 | ||
|
|
002595a407 | ||
|
|
4d8f75f7c9 | ||
|
|
3a3ee3912d | ||
|
|
cf3a289a54 | ||
|
|
ec5223d530 | ||
|
|
25a43c8a0b | ||
|
|
c2cb535105 | ||
|
|
d67654e601 |
@@ -4,6 +4,7 @@ import (
|
||||
"io/ioutil"
|
||||
"local/args"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -13,6 +14,7 @@ type Config struct {
|
||||
Database string
|
||||
Driver []string
|
||||
FilePrefix string
|
||||
APIPrefix string
|
||||
FileRoot string
|
||||
Auth bool
|
||||
AuthLifetime time.Duration
|
||||
@@ -20,6 +22,7 @@ type Config struct {
|
||||
RPS int
|
||||
SysRPS int
|
||||
Delay time.Duration
|
||||
StaticRoot string
|
||||
}
|
||||
|
||||
func New() Config {
|
||||
@@ -33,6 +36,8 @@ func New() Config {
|
||||
|
||||
as.Append(args.INT, "p", "port to listen on", 18114)
|
||||
as.Append(args.STRING, "fileprefix", "path prefix for file service", "/__files__")
|
||||
as.Append(args.STRING, "api-prefix", "path prefix for api", "api")
|
||||
as.Append(args.STRING, "static-root", "path to the root of a static file server", "./public")
|
||||
as.Append(args.STRING, "fileroot", "path to file hosting root", "/tmp/")
|
||||
as.Append(args.STRING, "database", "database name to use", "db")
|
||||
as.Append(args.STRING, "driver", "database driver args to use, like [local/storage.Type,arg1,arg2...] or [/path/to/boltdb]", "map")
|
||||
@@ -60,5 +65,7 @@ func New() Config {
|
||||
MaxFileSize: int64(as.GetInt("max-file-size")),
|
||||
RPS: as.GetInt("rps"),
|
||||
SysRPS: as.GetInt("sys-rps"),
|
||||
APIPrefix: strings.TrimPrefix(as.GetString("api-prefix"), "/"),
|
||||
StaticRoot: path.Join(as.GetString("static-root")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ function main() {
|
||||
go test ./...
|
||||
fi
|
||||
GOOS=linux GOARCH=arm GOARM=5 gobuild $exec
|
||||
scp -r ./public/swagger/* zach@tickle.lan:./dndex1/files/swagger/
|
||||
scp $exec zach@tickle.lan:./dndex1/dndex.new
|
||||
ssh zach@tickle.lan bash -c "true; while [ -e ./dndex1/dndex.new ]; do printf '.'; sleep 3; done; echo"
|
||||
if [ -n "$BIG" ]; then
|
||||
@@ -17,6 +16,9 @@ function main() {
|
||||
echo ssh bel@remote.blapointe.com bash -c 'true; md5sum ./services/bin/dndex*; while [ -e ./services/bin/dndex.new ]; do printf "."; sleep 3; done; md5sum ./services/bin/dndex*'
|
||||
big
|
||||
fi
|
||||
ssh zach@tickle.lan bash -c "true; rm -rf ./dndex1/files/swagger/"
|
||||
scp -r ./public/swagger/ zach@tickle.lan:./dndex1/files/swagger/
|
||||
ssh zach@tickle.lan bash -c "true; cd ./dndex1/public; rm -rf ./swagger; ln -s ../files/swagger"
|
||||
rm $exec
|
||||
}
|
||||
|
||||
@@ -42,7 +44,10 @@ function big() {
|
||||
npm install
|
||||
npm run build
|
||||
ssh zach@tickle.lan rm -rf ./dndex-ui/public
|
||||
ssh zach@tickle.lan bash -c "true; rm -rf ./dndex-ui/public"
|
||||
scp -r ./dist zach@tickle.lan:./dndex-ui/public
|
||||
ssh zach@tickle.lan bash -c "true; rm -rf ./dndex1/public"
|
||||
scp -r ./dist zach@tickle.lan:./dndex1/public
|
||||
popd
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"local/dndex/config"
|
||||
"local/dndex/server/auth"
|
||||
"local/dndex/storage/entity"
|
||||
"net/http"
|
||||
@@ -27,6 +28,8 @@ func Test(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(nil))
|
||||
s.Close()
|
||||
p := strings.Split(s.URL, ":")[2]
|
||||
os.Args = []string{"a"}
|
||||
s.URL = s.URL + "/" + config.New().APIPrefix
|
||||
os.Args = strings.Split(fmt.Sprintf(`dndex -auth=true -database db -delay 5ms -driver map -fileprefix /files -fileroot %s -p %v -rps 50 -sys-rps 40`, d, p), " ")
|
||||
|
||||
go main()
|
||||
|
||||
BIN
public/swagger/favicon-16x16.png
Normal file
BIN
public/swagger/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
BIN
public/swagger/favicon-32x32.png
Normal file
BIN
public/swagger/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
@@ -18,6 +18,7 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/token"
|
||||
- $ref: "#/components/parameters/id"
|
||||
- $ref: "#/components/parameters/md"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/schemas/responseOneResolved"
|
||||
@@ -54,6 +55,13 @@ components:
|
||||
$ref: "../swagger.yaml#/components/parameters/token"
|
||||
id:
|
||||
$ref: "../swagger.yaml#/components/parameters/id"
|
||||
md:
|
||||
name: md
|
||||
description: "render the text section as markdown"
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: bool
|
||||
|
||||
schemas:
|
||||
requestOne:
|
||||
|
||||
@@ -5,6 +5,7 @@ paths:
|
||||
- files
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/token"
|
||||
- $ref: "#/components/parameters/direct"
|
||||
requestBody:
|
||||
$ref: "#/components/schemas/requestForm"
|
||||
|
||||
@@ -12,6 +13,13 @@ components:
|
||||
parameters:
|
||||
token:
|
||||
$ref: "../swagger.yaml#/components/parameters/token"
|
||||
direct:
|
||||
name: direct
|
||||
description: "interpret content as a direct link to actual content"
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: bool
|
||||
schemas:
|
||||
requestForm:
|
||||
$ref: "../swagger.yaml#/components/schemas/requestForm"
|
||||
|
||||
6
public/swagger/v1/index.yaml
Normal file
6
public/swagger/v1/index.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
paths:
|
||||
get:
|
||||
tags: []
|
||||
summary: "Access a static read-only file server"
|
||||
responses:
|
||||
200: {}
|
||||
@@ -15,24 +15,26 @@ servers:
|
||||
- url: http://api1.dndex.lan:8080/
|
||||
|
||||
paths:
|
||||
/version:
|
||||
/api/version:
|
||||
$ref: "./version.yaml#/paths"
|
||||
/dump:
|
||||
/api/dump:
|
||||
$ref: "./dump.yaml#/paths"
|
||||
/files:
|
||||
/api/files:
|
||||
$ref: "./files/index.yaml#/paths"
|
||||
/files/{path}:
|
||||
/api/files/{path}:
|
||||
$ref: "./files/one.yaml#/paths"
|
||||
/users/register:
|
||||
/api/users/register:
|
||||
$ref: "./users/register.yaml#/paths"
|
||||
/users/login:
|
||||
/api/users/login:
|
||||
$ref: "./users/login.yaml#/paths"
|
||||
/entities:
|
||||
/api/entities:
|
||||
$ref: "./entities/index.yaml#/paths"
|
||||
/entities/{id}:
|
||||
/api/entities/{id}:
|
||||
$ref: "./entities/id.yaml#/paths"
|
||||
/entities/{id}/{path}:
|
||||
/api/entities/{id}/{path}:
|
||||
$ref: "./entities/idsub.yaml#/paths"
|
||||
/:
|
||||
$ref: "./index.yaml#/paths"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
|
||||
@@ -6,13 +6,10 @@ paths:
|
||||
- users
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
$ref: "#/components/schemas/requestLogin"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
DnDex-User:
|
||||
type: string
|
||||
example: "namespace"
|
||||
$ref: "#/components/schemas/requestLogin"
|
||||
responses:
|
||||
200:
|
||||
content:
|
||||
@@ -26,3 +23,13 @@ paths:
|
||||
salt:
|
||||
type: string
|
||||
example: def-456
|
||||
|
||||
components:
|
||||
schemas:
|
||||
requestLogin:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
DnDex-User:
|
||||
type: string
|
||||
example: "namespace"
|
||||
|
||||
@@ -5,16 +5,10 @@ paths:
|
||||
- users
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
$ref: "#/components/schemas/requestRegister"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
DnDex-User:
|
||||
type: string
|
||||
example: "namespace"
|
||||
DnDex-Auth:
|
||||
type: string
|
||||
example: "password"
|
||||
$ref: "#/components/schemas/requestRegister"
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/schemas/responseOK"
|
||||
@@ -23,3 +17,13 @@ components:
|
||||
schemas:
|
||||
responseOK:
|
||||
$ref: "../swagger.yaml#/components/schemas/responseOK"
|
||||
requestRegister:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
DnDex-User:
|
||||
type: string
|
||||
example: "namespace"
|
||||
DnDex-Auth:
|
||||
type: string
|
||||
example: "password"
|
||||
|
||||
42
public/ui/index.html
Normal file
42
public/ui/index.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>DnDex UI</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
|
||||
<script src="/ui/dndex.js"></script>
|
||||
</head>
|
||||
<body class="has-navbar-fixed-top">
|
||||
<nav class="navbar is-fixed-top is-primary is-bold" role="navigation">
|
||||
<div class="navbar-brand"></div>
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">top</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item">a</a>
|
||||
<hr class="navbar-divider">
|
||||
<a class="navbar-item">b</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<a class="button">Sign Up</a>
|
||||
<a class="button">Log In</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<section class="hero is-primary is-bold is-fullheight-with-navbar">
|
||||
<div class="hero-head">
|
||||
<div class="container">
|
||||
<div class="title">Hero</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
Submodule public/vue/dndex-ui updated: 72595fae58...70133bf814
@@ -1,13 +1,19 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"local/dndex/storage"
|
||||
"local/dndex/storage/entity"
|
||||
"net/http"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
)
|
||||
|
||||
func GeneratePlain(g storage.RateLimitedGraph, r *http.Request) (string, error) {
|
||||
@@ -47,7 +53,26 @@ func readRequestedNamespace(r *http.Request) string {
|
||||
}
|
||||
|
||||
func readRequested(r *http.Request, key string) string {
|
||||
return r.FormValue(key)
|
||||
switch r.Header.Get("Content-Type") {
|
||||
case "application/json":
|
||||
b, _ := ioutil.ReadAll(r.Body)
|
||||
r.Body = struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
Reader: bytes.NewReader(b),
|
||||
Closer: r.Body,
|
||||
}
|
||||
m := bson.M{}
|
||||
json.Unmarshal(b, &m)
|
||||
v, ok := m[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(v)
|
||||
default:
|
||||
return r.FormValue(key)
|
||||
}
|
||||
}
|
||||
|
||||
func getKeyForNamespace(ctx context.Context, g storage.RateLimitedGraph, namespace string) (string, error) {
|
||||
|
||||
@@ -95,3 +95,30 @@ func TestGenerate(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadRequested(t *testing.T) {
|
||||
t.Run("form: ignore query params", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodPost, "/a=c", nil)
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if got := readRequested(r, "a"); got != "" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("form: body beats query params", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodPost, "/a=c", strings.NewReader(`a=b`))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if got := readRequested(r, "a"); got != "b" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("json: OK", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodPost, "/a=c", strings.NewReader(`{"a": "b"}`))
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
if got := readRequested(r, "a"); got != "b" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
@@ -119,6 +122,12 @@ func (rest *REST) entitiesGetOne(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
resp[entity.Connections] = m
|
||||
}
|
||||
_, md := r.URL.Query()["md"]
|
||||
if md {
|
||||
renderer := html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.TOC})
|
||||
parser := parser.NewWithExtensions(parser.CommonExtensions | parser.HeadingIDs | parser.AutoHeadingIDs | parser.Titleblock)
|
||||
resp["md"] = markdownHead + string(markdown.ToHTML([]byte(one.Text), parser, renderer)) + markdownTail
|
||||
}
|
||||
rest.respMap(w, entityScope[0], resp)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,21 @@ func TestEntities(t *testing.T) {
|
||||
return one.Name == "myname"
|
||||
})
|
||||
|
||||
w = testEntitiesMethod(t, authit, rest, http.MethodGet, "/"+id, ``)
|
||||
w = testEntitiesMethod(t, authit, rest, http.MethodGet, "/"+id+"?md", ``)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatal(w.Code)
|
||||
}
|
||||
b := w.Body.Bytes()
|
||||
var markdowned map[string]struct {
|
||||
MD string `json:"md"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &markdowned); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(markdowned) != 1 {
|
||||
t.Fatal(len(markdowned))
|
||||
} else if len(markdowned[id].MD) == 0 {
|
||||
t.Fatal(markdowned, string(b))
|
||||
}
|
||||
id2 := testEntitiesGetOneResponse(t, w.Body, func(one entity.One) bool {
|
||||
return one.Name == "myname"
|
||||
})
|
||||
|
||||
@@ -2,10 +2,13 @@ package server
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"local/dndex/config"
|
||||
"local/simpleserve/simpleserve"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -50,12 +53,34 @@ func (rest *REST) filesCreate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.Copy(f, r.Body); err != nil {
|
||||
if err := rest.filesStream(r, f); err != nil {
|
||||
rest.respError(w, err)
|
||||
return
|
||||
}
|
||||
w.Write([]byte(id))
|
||||
}
|
||||
|
||||
func (rest *REST) filesStream(r *http.Request, f io.Writer) error {
|
||||
var reader io.Reader = r.Body
|
||||
_, direct := r.URL.Query()["direct"]
|
||||
if direct {
|
||||
target, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := http.Get(string(target))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
reader = resp.Body
|
||||
}
|
||||
if _, err := io.Copy(f, reader); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rest *REST) filesDelete(w http.ResponseWriter, r *http.Request) {
|
||||
localPath := rest.filesPath(r)
|
||||
if stat, err := os.Stat(localPath); os.IsNotExist(err) {
|
||||
@@ -72,6 +97,8 @@ func (rest *REST) filesDelete(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (rest *REST) filesGet(w http.ResponseWriter, r *http.Request) {
|
||||
simpleserve.SetContentTypeIfMedia(w, r)
|
||||
r.URL.Path = strings.TrimSuffix(r.URL.Path, path.Ext(r.URL.Path))
|
||||
localPath := rest.filesPath(r)
|
||||
if stat, err := os.Stat(localPath); os.IsNotExist(err) {
|
||||
rest.respNotFound(w)
|
||||
@@ -106,10 +133,12 @@ func (rest *REST) filesUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.Copy(f, r.Body); err != nil {
|
||||
|
||||
if err := rest.filesStream(r, f); err != nil {
|
||||
rest.respError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Rename(localPath+".tmp", localPath); err != nil {
|
||||
rest.respError(w, err)
|
||||
return
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -11,14 +13,17 @@ import (
|
||||
|
||||
func TestFiles(t *testing.T) {
|
||||
cases := map[string]func(*testing.T, *REST, func(*http.Request)){
|
||||
"create-get": func(t *testing.T, rest *REST, scope func(r *http.Request)) {
|
||||
"create-get w/ content type": func(t *testing.T, rest *REST, scope func(r *http.Request)) {
|
||||
content := uuid.New().String()
|
||||
w := testFilesPost(t, rest, scope, content)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatal(w.Code, string(w.Body.Bytes()))
|
||||
}
|
||||
s := string(w.Body.Bytes())
|
||||
s := fmt.Sprintf("%s.jpg", w.Body.Bytes())
|
||||
w = testFilesGet(t, rest, s, scope)
|
||||
if w.Header().Get("Content-Type") != "image/jpeg" {
|
||||
t.Fatal(w.Header())
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatal(w.Code, string(w.Body.Bytes()))
|
||||
}
|
||||
@@ -101,3 +106,37 @@ func testFilesReq(t *testing.T, rest *REST, id string, scope func(*http.Request)
|
||||
rest.files(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestFilesStream(t *testing.T) {
|
||||
rest, _, clean := testREST(t)
|
||||
defer clean()
|
||||
|
||||
t.Run("simple upload", func(t *testing.T) {
|
||||
value := uuid.New().String()
|
||||
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(value))
|
||||
buff := bytes.NewBuffer(nil)
|
||||
if err := rest.filesStream(r, buff); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s := string(buff.Bytes()); s != value {
|
||||
t.Fatal(s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("direct upload", func(t *testing.T) {
|
||||
s := httptest.NewServer(http.HandlerFunc(http.NotFound))
|
||||
defer s.Close()
|
||||
r := httptest.NewRequest(http.MethodPost, "/?direct", strings.NewReader(s.URL))
|
||||
buff := bytes.NewBuffer(nil)
|
||||
if err := rest.filesStream(r, buff); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
http.NotFound(w, nil)
|
||||
want := string(w.Body.Bytes())
|
||||
got := string(buff.Bytes())
|
||||
if want != got {
|
||||
t.Fatal(want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
10
server/markdown.go
Normal file
10
server/markdown.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package server
|
||||
|
||||
const (
|
||||
markdownHead = `<div>
|
||||
<style scoped>
|
||||
@charset"UTF-8";button,input,textarea{transition:background-color 0.1s linear, border-color 0.1s linear, color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease}h1{font-size:2.2em;margin-top:0}h1,h2,h3,h4,h5,h6{margin-bottom:12px}h1,h2,h3,h4,h5,h6,strong{color:#000000}b,h1,h2,h3,h4,h5,h6,strong,th{font-weight:600}blockquote{border-left:4px solid #0096bfab;margin:1.5em 0;padding:0.5em 1em;font-style:italic}blockquote > footer{margin-top:10px;font-style:normal}blockquote cite{font-style:normal}address{font-style:normal}a[href^='mailto']::before{content:'📧 '}a[href^='tel']::before{content:'📞 '}a[href^='sms']::before{content:'💬 '}button,input[type='submit'],input[type='button'],input[type='checkbox']{cursor:pointer}input:not([type='checkbox']):not([type='radio']),select{display:block}button,input,select,textarea{color:#000000;background-color:#efefef;font-family:inherit;font-size:inherit;margin-right:6px;margin-bottom:6px;padding:10px;border:none;border-radius:6px;outline:none}input:not([type='checkbox']):not([type='radio']),select,button,textarea{-webkit-appearance:none}textarea{margin-right:0;width:100%;box-sizing:border-box;resize:vertical}button,input[type='submit'],input[type='button']{padding-right:30px;padding-left:30px}button:hover,input[type='submit']:hover,input[type='button']:hover{background:#dddddd}button:focus,input:focus,select:focus,textarea:focus{box-shadow:0 0 0 2px #0096bfab}input[type='checkbox']:active,input[type='radio']:active,input[type='submit']:active,input[type='button']:active,button:active{transform:translateY(2px)}button:disabled,input:disabled,select:disabled,textarea:disabled{cursor:not-allowed;opacity:0.5}::-webkit-input-placeholder{color:#949494}:-ms-input-placeholder{color:#949494}::-ms-input-placeholder{color:#949494}::placeholder{color:#949494}a{text-decoration:none;color:#0076d1}a:hover{text-decoration:underline}code,kbd{background:#efefef;color:#000000;padding:5px;border-radius:6px}pre > code{padding:10px;display:block;overflow-x:auto}img{max-width:100%}hr{border:none;border-top:1px solid #dbdbdb}table{border-collapse:collapse;margin-bottom:10px;width:100%}td,th{padding:6px;text-align:left}th{border-bottom:1px solid #dbdbdb}tbody tr:nth-child(even){background-color:#efefef}::-webkit-scrollbar{height:10px;width:10px}::-webkit-scrollbar-track{background:#efefef;border-radius:6px}::-webkit-scrollbar-thumb{background:#d5d5d5;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#c4c4c4}
|
||||
</style>
|
||||
`
|
||||
markdownTail = `</div>`
|
||||
)
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"local/dndex/server/auth"
|
||||
"local/gziphttp"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -69,3 +70,11 @@ func (rest *REST) shift(foo http.HandlerFunc) http.HandlerFunc {
|
||||
foo(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (rest *REST) deprefix(foo http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
newpath := strings.TrimPrefix(r.URL.Path, path.Join("/", config.New().APIPrefix))
|
||||
r.URL.Path = newpath
|
||||
foo(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,3 +131,20 @@ func TestMiddlewareShift(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiddlewareDeprefix(t *testing.T) {
|
||||
resolved := ""
|
||||
bar := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resolved = r.URL.Path
|
||||
})
|
||||
|
||||
os.Args = []string{"a"}
|
||||
os.Setenv("API_PREFIX", "myprefix")
|
||||
r := httptest.NewRequest(http.MethodGet, "/myprefix/abc", nil)
|
||||
rest := &REST{}
|
||||
rest.deprefix(bar)(nil, r)
|
||||
|
||||
if resolved != "/abc" {
|
||||
t.Fatal(resolved)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"local/dndex/storage"
|
||||
"local/router"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -42,11 +43,11 @@ func NewREST(g storage.RateLimitedGraph) (*REST, error) {
|
||||
fmt.Sprintf("dump"): rest.dump,
|
||||
}
|
||||
|
||||
for path, foo := range paths {
|
||||
for urlpath, foo := range paths {
|
||||
bar := foo
|
||||
bar = rest.shift(bar)
|
||||
bar = rest.scoped(bar)
|
||||
switch strings.Split(path, "/")[0] {
|
||||
switch strings.Split(urlpath, "/")[0] {
|
||||
case "users":
|
||||
case "version":
|
||||
default:
|
||||
@@ -54,11 +55,20 @@ func NewREST(g storage.RateLimitedGraph) (*REST, error) {
|
||||
}
|
||||
bar = rest.defend(bar)
|
||||
bar = rest.delay(bar)
|
||||
if err := rest.router.Add(path, bar); err != nil {
|
||||
bar = rest.deprefix(bar)
|
||||
routerpath := path.Join("/", config.New().APIPrefix, urlpath)
|
||||
if err := rest.router.Add(routerpath, bar); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
bar := rest.static
|
||||
bar = rest.defend(bar)
|
||||
bar = rest.delay(bar)
|
||||
if err := rest.router.Add(params, bar); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rest, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -163,10 +163,10 @@ func TestRESTRouter(t *testing.T) {
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
path := name
|
||||
urlpath := path.Join("/", config.New().APIPrefix, name)
|
||||
rest, setAuth, clean := testREST(t)
|
||||
defer clean()
|
||||
r := httptest.NewRequest(c.method, path, strings.NewReader(``))
|
||||
r := httptest.NewRequest(c.method, urlpath, strings.NewReader(``))
|
||||
setAuth(r)
|
||||
w := httptest.NewRecorder()
|
||||
rest.router.ServeHTTP(w, r)
|
||||
|
||||
18
server/static.go
Normal file
18
server/static.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"local/dndex/config"
|
||||
"local/simpleserve/simpleserve"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
func (rest *REST) static(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := os.Stat(path.Join(config.New().StaticRoot, r.URL.Path)); os.IsNotExist(err) {
|
||||
r.URL.Path = "/"
|
||||
}
|
||||
simpleserve.SetContentTypeIfMedia(w, r)
|
||||
server := http.FileServer(http.Dir(config.New().StaticRoot))
|
||||
server.ServeHTTP(w, r)
|
||||
}
|
||||
46
server/static_test.go
Normal file
46
server/static_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"local/dndex/config"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRESTStatic(t *testing.T) {
|
||||
os.Args = []string{"a"}
|
||||
d, err := ioutil.TempDir(os.TempDir(), "static*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Setenv("STATIC_ROOT", d)
|
||||
if err := ioutil.WriteFile(path.Join(d, "index.html"), []byte("Hello, world"), os.ModePerm); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rest, _, clean := testREST(t)
|
||||
defer clean()
|
||||
|
||||
t.Run("assert nonstatic OK", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, path.Join("/", config.New().APIPrefix, "version"), nil)
|
||||
w := httptest.NewRecorder()
|
||||
rest.router.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatal(w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("assert static OK", func(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
rest.router.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatal(w.Code)
|
||||
}
|
||||
if s := string(w.Body.Bytes()); s != "Hello, world" {
|
||||
t.Fatal(s)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -16,7 +16,7 @@ func (rest *REST) users(w http.ResponseWriter, r *http.Request) {
|
||||
rest.respNotFound(w)
|
||||
return
|
||||
}
|
||||
r.Header.Set("Application-Type", "application/x-www-form-urlencoded")
|
||||
rest.usersContentType(r)
|
||||
switch r.URL.Path {
|
||||
case "/register":
|
||||
rest.usersRegister(w, r)
|
||||
@@ -28,6 +28,7 @@ func (rest *REST) users(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (rest *REST) usersRegister(w http.ResponseWriter, r *http.Request) {
|
||||
rest.usersContentType(r)
|
||||
err := auth.Register(rest.g, r)
|
||||
if err != nil {
|
||||
rest.respError(w, err)
|
||||
@@ -37,6 +38,7 @@ func (rest *REST) usersRegister(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (rest *REST) usersLogin(w http.ResponseWriter, r *http.Request) {
|
||||
rest.usersContentType(r)
|
||||
salt := uuid.New().String()[:5]
|
||||
var token string
|
||||
var err error
|
||||
@@ -56,3 +58,9 @@ func (rest *REST) usersLogin(w http.ResponseWriter, r *http.Request) {
|
||||
"salt": salt,
|
||||
})
|
||||
}
|
||||
|
||||
func (rest *REST) usersContentType(r *http.Request) {
|
||||
if r.Header.Get("Application-Type") == "" {
|
||||
r.Header.Set("Application-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,11 @@ func TestUsersRegister(t *testing.T) {
|
||||
defer clean()
|
||||
|
||||
t.Run("register ok", func(t *testing.T) {
|
||||
user := uuid.New().String()[:5]
|
||||
pwd := uuid.New().String()[:5]
|
||||
testRegisterOK(t, rest, user, pwd)
|
||||
for _, json := range []bool{false, true} {
|
||||
user := uuid.New().String()[:5]
|
||||
pwd := uuid.New().String()[:5]
|
||||
testRegisterOK(t, rest, user, pwd, json)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("register 400: nil body", func(t *testing.T) {
|
||||
@@ -76,36 +78,54 @@ func TestUsersLogin(t *testing.T) {
|
||||
defer clean()
|
||||
|
||||
t.Run("login ok", func(t *testing.T) {
|
||||
user := uuid.New().String()[:5]
|
||||
pwd := uuid.New().String()[:5]
|
||||
testRegisterOK(t, rest, user, pwd)
|
||||
testLoginOK(t, rest, user, pwd)
|
||||
for _, json := range []bool{false, true} {
|
||||
user := uuid.New().String()[:5]
|
||||
pwd := uuid.New().String()[:5]
|
||||
testRegisterOK(t, rest, user, pwd, json)
|
||||
testLoginOK(t, rest, user, pwd, json)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("login 404 user", func(t *testing.T) {
|
||||
pwd := uuid.New().String()[:5]
|
||||
testLoginNotOK(t, rest, "bad", pwd)
|
||||
for _, json := range []bool{false, true} {
|
||||
pwd := uuid.New().String()[:5]
|
||||
testLoginNotOK(t, rest, "bad", pwd, json)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("login bad user", func(t *testing.T) {
|
||||
user := uuid.New().String()[:5]
|
||||
pwd := uuid.New().String()[:5]
|
||||
testRegisterOK(t, rest, user, pwd)
|
||||
testLoginNotOK(t, rest, "bad", pwd)
|
||||
for _, json := range []bool{false, true} {
|
||||
user := uuid.New().String()[:5]
|
||||
pwd := uuid.New().String()[:5]
|
||||
testRegisterOK(t, rest, user, pwd, json)
|
||||
testLoginNotOK(t, rest, "bad", pwd, json)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("login bad pwd", func(t *testing.T) {
|
||||
user := uuid.New().String()[:5]
|
||||
pwd := uuid.New().String()[:5]
|
||||
testRegisterOK(t, rest, user, pwd)
|
||||
testLoginNotOK(t, rest, user, "bad")
|
||||
for _, json := range []bool{false, true} {
|
||||
user := uuid.New().String()[:5]
|
||||
pwd := uuid.New().String()[:5]
|
||||
testRegisterOK(t, rest, user, pwd, json)
|
||||
testLoginNotOK(t, rest, user, "bad", json)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testRegisterOK(t *testing.T, rest *REST, user, pwd string) {
|
||||
func testRegisterOK(t *testing.T, rest *REST, user, pwd string, useJSON bool) {
|
||||
body := fmt.Sprintf(`%s=%s&%s=%s`, auth.UserKey, user, auth.AuthKey, pwd)
|
||||
if useJSON {
|
||||
s, _ := json.Marshal(map[string]string{
|
||||
auth.UserKey: user,
|
||||
auth.AuthKey: pwd,
|
||||
})
|
||||
body = string(s)
|
||||
}
|
||||
r := httptest.NewRequest(http.MethodPost, "/register", strings.NewReader(body))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if useJSON {
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
rest.users(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
@@ -113,10 +133,19 @@ func testRegisterOK(t *testing.T, rest *REST, user, pwd string) {
|
||||
}
|
||||
}
|
||||
|
||||
func testLoginNotOK(t *testing.T, rest *REST, user, pwd string) {
|
||||
func testLoginNotOK(t *testing.T, rest *REST, user, pwd string, useJSON bool) {
|
||||
body := fmt.Sprintf(`%s=%s`, auth.UserKey, user)
|
||||
if useJSON {
|
||||
s, _ := json.Marshal(map[string]string{
|
||||
auth.UserKey: user,
|
||||
})
|
||||
body = string(s)
|
||||
}
|
||||
r := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(body))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if useJSON {
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
rest.users(w, r)
|
||||
if w.Code < http.StatusBadRequest {
|
||||
@@ -136,10 +165,19 @@ func testLoginNotOK(t *testing.T, rest *REST, user, pwd string) {
|
||||
}
|
||||
}
|
||||
|
||||
func testLoginOK(t *testing.T, rest *REST, user, pwd string) string {
|
||||
func testLoginOK(t *testing.T, rest *REST, user, pwd string, useJSON bool) string {
|
||||
body := fmt.Sprintf(`%s=%s`, auth.UserKey, user)
|
||||
if useJSON {
|
||||
s, _ := json.Marshal(map[string]string{
|
||||
auth.UserKey: user,
|
||||
})
|
||||
body = string(s)
|
||||
}
|
||||
r := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(body))
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
if useJSON {
|
||||
r.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
rest.users(w, r)
|
||||
if w.Code != http.StatusOK {
|
||||
|
||||
@@ -2,6 +2,7 @@ package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"local/dndex/storage/entity"
|
||||
"local/storage"
|
||||
@@ -63,7 +64,7 @@ func (s *Storage) Update(ctx context.Context, ns string, filter, operator interf
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v, err = bson.Marshal(n)
|
||||
v, err = json.Marshal(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -72,12 +73,12 @@ func (s *Storage) Update(ctx context.Context, ns string, filter, operator interf
|
||||
}
|
||||
|
||||
func (s *Storage) Insert(ctx context.Context, ns string, doc interface{}) error {
|
||||
b, err := bson.Marshal(doc)
|
||||
b, err := json.Marshal(doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := bson.M{}
|
||||
if err := bson.Unmarshal(b, &m); err != nil {
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -122,11 +123,15 @@ func (s *Storage) forEach(ctx context.Context, ns string, filter interface{}, fo
|
||||
return err
|
||||
} else {
|
||||
n := bson.M{}
|
||||
if err := bson.Unmarshal(v, &n); err != nil {
|
||||
if err := json.Unmarshal(v, &n); err != nil {
|
||||
return err
|
||||
}
|
||||
if matches(n, m) {
|
||||
if err := foo(id, append(bson.Raw{}, bson.Raw(v)...)); err != nil {
|
||||
b, err := bson.Marshal(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := foo(id, b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"local/dndex/storage/entity"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func TestNewStorage(t *testing.T) {
|
||||
@@ -37,7 +37,7 @@ func fillStorage(t *testing.T, s *Storage) {
|
||||
Connections: map[string]entity.Connection{p.ID: entity.Connection{p.Name}},
|
||||
Attachments: map[string]entity.Attachment{"filename": {"/path/to/file"}},
|
||||
}
|
||||
b, err := bson.Marshal(o)
|
||||
b, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package entity
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
@@ -62,6 +61,18 @@ func (o One) Generic() bson.M {
|
||||
return m
|
||||
}
|
||||
|
||||
func (o One) MarshalJSON() ([]byte, error) {
|
||||
b, err := o.MarshalBSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := bson.M{}
|
||||
if err := bson.Unmarshal(b, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (o One) MarshalBSON() ([]byte, error) {
|
||||
isMin := fmt.Sprint(o) == fmt.Sprint(o.Query())
|
||||
if !isMin {
|
||||
@@ -73,19 +84,39 @@ func (o One) MarshalBSON() ([]byte, error) {
|
||||
if o.Attachments == nil {
|
||||
o.Attachments = make(map[string]Attachment)
|
||||
}
|
||||
b, err := json.Marshal(o)
|
||||
var v interface{}
|
||||
b, err := o.toBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := bson.M{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
if err := fromBytes(b, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range m {
|
||||
switch v.(type) {
|
||||
case string:
|
||||
m[k] = strings.TrimSpace(v.(string))
|
||||
}
|
||||
}
|
||||
return bson.Marshal(m)
|
||||
return bson.Marshal(v)
|
||||
}
|
||||
|
||||
func (o One) toBytes() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
ID string `bson:"_id,omitempty" json:"_id"`
|
||||
Name string `bson:"name,omitempty" json:"name"`
|
||||
Type string `bson:"type,omitempty" json:"type"`
|
||||
Title string `bson:"title,omitempty" json:"title"`
|
||||
Text string `bson:"text,omitempty" json:"text"`
|
||||
Modified int64 `bson:"modified,omitempty" json:"modified"`
|
||||
Connections map[string]Connection `bson:"connections" json:"connections"`
|
||||
Attachments map[string]Attachment `bson:"attachments" json:"attachments"`
|
||||
}{
|
||||
ID: o.ID,
|
||||
Name: o.Name,
|
||||
Type: o.Type,
|
||||
Title: o.Title,
|
||||
Text: o.Text,
|
||||
Modified: o.Modified,
|
||||
Connections: o.Connections,
|
||||
Attachments: o.Attachments,
|
||||
})
|
||||
}
|
||||
|
||||
func fromBytes(b []byte, v interface{}) error {
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
@@ -57,3 +57,24 @@ func TestOneMarshalBSON(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToFromBytes(t *testing.T) {
|
||||
o := One{
|
||||
Name: "hello",
|
||||
Connections: map[string]Connection{
|
||||
"world": Connection{Relationship: "!"},
|
||||
},
|
||||
}
|
||||
|
||||
b, err := o.toBytes()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var v interface{}
|
||||
if err := fromBytes(b, &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log(v)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user