feat(workspace): add text editing and image previews
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.misaka.ren/m1saka/dsh-web/internal/store"
|
||||
@@ -20,6 +21,188 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWorkspaceText(t *testing.T) {
|
||||
a, sid := runtimeTestAPI(t, func(http.ResponseWriter, *http.Request) { t.Error("unexpected Docker call") })
|
||||
for _, i := range []store.Instance{{ID: "one", UID: "owner"}, {ID: "foreign", UID: "other"}} {
|
||||
if err := a.s.AddInstance(i); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
dir := filepath.Join(a.root, "workspaces/owner/instances/one/home")
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file := filepath.Join(dir, "file")
|
||||
putFile := func(content string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(file, []byte(content), 0751); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
putFile("original")
|
||||
if _, err := a.s.DB.Exec("UPDATE users SET quota=? WHERE id='owner'", 32<<20); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := func(method, query, body, session string, want int) map[string]string {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
a.workspace(w, runtimeRequest(method, "/api/ws/text?"+query, body, session))
|
||||
if w.Code != want {
|
||||
t.Fatalf("%s %s: got %d want %d: %s", method, query, w.Code, want, w.Body.String())
|
||||
}
|
||||
out := map[string]string{}
|
||||
if want == 200 {
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
q := "instance=one&path=file"
|
||||
body := func(content, version string) string {
|
||||
b, _ := json.Marshal(map[string]string{"content": content, "version": version})
|
||||
return string(b)
|
||||
}
|
||||
request("GET", q, "", "", 401)
|
||||
request("GET", "instance=foreign&path=file", "", sid, 404)
|
||||
request("GET", "path=file", "", sid, 410)
|
||||
for _, p := range []string{"../file", "..", "/etc/passwd", ".", "", "\x00", "./file", "a/../file", "a/./file", "a//file", "file/"} {
|
||||
for _, method := range []string{"GET", "PUT"} {
|
||||
request(method, "instance=one&path="+url.QueryEscape(p), "", sid, 400)
|
||||
}
|
||||
}
|
||||
for _, tc := range []struct{ exact, counterpart string }{
|
||||
{"config.yaml ", "config.yaml"},
|
||||
{" config.yaml", "config.yaml"},
|
||||
{`a\b`, "a/b"},
|
||||
} {
|
||||
for name, content := range map[string]string{tc.exact: "exact", tc.counterpart: "counterpart"} {
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
exactQuery := "instance=one&path=" + url.QueryEscape(tc.exact)
|
||||
got := request("GET", exactQuery, "", sid, 200)
|
||||
if got["content"] != "exact" {
|
||||
t.Fatalf("GET %q returned %q", tc.exact, got["content"])
|
||||
}
|
||||
request("PUT", exactQuery, body("updated", got["version"]), sid, 200)
|
||||
if got := request("GET", exactQuery, "", sid, 200); got["content"] != "updated" {
|
||||
t.Fatalf("GET %q after PUT returned %q", tc.exact, got["content"])
|
||||
}
|
||||
for name, want := range map[string]string{tc.exact: "updated", tc.counterpart: "counterpart"} {
|
||||
got, err := os.ReadFile(filepath.Join(dir, name))
|
||||
if err != nil || string(got) != want {
|
||||
t.Fatalf("file %q: got %q, want %q: %v", name, got, want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
request("POST", q, "", sid, 405)
|
||||
v := request("GET", q, "", sid, 200)["version"]
|
||||
if v == "" {
|
||||
t.Fatal("missing version")
|
||||
}
|
||||
request("PUT", q, "{}", sid, 400)
|
||||
request("PUT", q, body("next", "stale"), sid, 409)
|
||||
request("PUT", q, body("next", ""), sid, 400)
|
||||
for _, content := range []string{"nul\x00", "del\x7f", "control\x01", "invalid\xff"} {
|
||||
putFile(content)
|
||||
request("GET", q, "", sid, 415)
|
||||
}
|
||||
putFile(strings.Repeat("x", maxTextBytes+1))
|
||||
request("GET", q, "", sid, 413)
|
||||
putFile(strings.Repeat("x", maxTextBytes))
|
||||
request("GET", q, "", sid, 200)
|
||||
putFile("original")
|
||||
v = request("GET", q, "", sid, 200)["version"]
|
||||
request("PUT", q, body(strings.Repeat("x", maxTextBytes+1), v), sid, 413)
|
||||
request("PUT", q, body("bad\x00", v), sid, 415)
|
||||
request("PUT", q, "{\"content\":\"\xff\",\"version\":\"v\"}", sid, 415)
|
||||
request("PUT", q, strings.Repeat(" ", 6*maxTextBytes+4097), sid, 413)
|
||||
if err := os.Symlink(file, filepath.Join(dir, "link")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(dir, filepath.Join(dir, "parent")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range []string{"link", "parent/file"} {
|
||||
request("GET", "instance=one&path="+p, "", sid, 404)
|
||||
request("PUT", "instance=one&path="+p, body("next", v), sid, 409)
|
||||
}
|
||||
if err := os.Link(file, filepath.Join(dir, "hard")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request("GET", q, "", sid, 404)
|
||||
request("PUT", q, body("next", v), sid, 409)
|
||||
if err := os.Remove(filepath.Join(dir, "hard")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := a.s.DB.Exec("UPDATE users SET quota=8 WHERE id='owner'"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request("PUT", q, body("next", v), sid, 413)
|
||||
if _, err := a.s.DB.Exec("UPDATE users SET quota=? WHERE id='owner'", 32<<20); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, err := os.Stat(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
next := "你好\tline\r\n"
|
||||
saved := request("PUT", q, body(next, v), sid, 200)["version"]
|
||||
got := request("GET", q, "", sid, 200)
|
||||
if got["content"] != next || got["version"] != saved || saved == v {
|
||||
t.Fatal(got, saved)
|
||||
}
|
||||
after, err := os.Stat(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bs, as := before.Sys().(*syscall.Stat_t), after.Sys().(*syscall.Stat_t)
|
||||
if bs.Mode != as.Mode || bs.Uid != as.Uid || bs.Gid != as.Gid {
|
||||
t.Fatal("metadata changed")
|
||||
}
|
||||
request("PUT", q, body("stale save", v), sid, 409)
|
||||
results := make(chan int, 2)
|
||||
for range 2 {
|
||||
go func() {
|
||||
w := httptest.NewRecorder()
|
||||
a.workspace(w, runtimeRequest("PUT", "/api/ws/text?"+q, body("concurrent", saved), sid))
|
||||
results <- w.Code
|
||||
}()
|
||||
}
|
||||
first, second := <-results, <-results
|
||||
if !(first == 200 && second == 409 || first == 409 && second == 200) {
|
||||
t.Fatalf("concurrent saves: %d %d", first, second)
|
||||
}
|
||||
saved = request("GET", q, "", sid, 200)["version"]
|
||||
putFile("external")
|
||||
request("PUT", q, body("stale save", saved), sid, 409)
|
||||
if err := os.Remove(file); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request("PUT", q, body("recreate", saved), sid, 409)
|
||||
request("GET", q, "", sid, 404)
|
||||
putFile("restored")
|
||||
if _, err := a.s.DB.Exec("UPDATE instances SET status='error' WHERE id='one'"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request("PUT", q, body("blocked", saved), sid, 409)
|
||||
request("GET", q, "", sid, 200)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), ".text-") {
|
||||
t.Fatal("temporary file leaked")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadExtensionPolicy(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, policy string
|
||||
|
||||
Reference in New Issue
Block a user