mirror of
https://github.com/Dvorinka/excalidraw-full.git
synced 2026-06-03 22:02:57 +00:00
Add different data providers and improve docs
This adds support for switching between in-memory, local filesystem, s3 or sqlite.
This commit is contained in:
@@ -1,17 +1,69 @@
|
|||||||
# Exalidraw Complete
|
# Excalidraw Complete: A Self-Hosted Solution
|
||||||
|
|
||||||
Frustrated on how difficult it is to setup excalidraw self-hosted but with data
|
Excalidraw Complete simplifies the deployment of Excalidraw, bringing an
|
||||||
storage and collaboration function this represents and attempt to run the
|
all-in-one solution to self-hosting this versatile virtual whiteboard. Designed
|
||||||
necessary function with a single binary implemented in go. This includes:
|
for ease of setup and use, Excalidraw Complete integrates essential features
|
||||||
|
into a single Go binary. This solution encompasses:
|
||||||
|
|
||||||
- the frontend UI
|
- The intuitive Excalidraw frontend UI for seamless user experience.
|
||||||
- a in-memory data layer
|
- An integrated data layer ensuring fast and efficient data handling based on different data providers.
|
||||||
- socket.io implementation for collaboration
|
- A socket.io implementation to enable real-time collaboration among users.
|
||||||
|
|
||||||
|
The project goal is to alleviate the setup complexities traditionally associated with self-hosting Excalidraw, especially in scenarios requiring data persistence and collaborative functionalities.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To get started, download the latest release binary:
|
||||||
|
|
||||||
Apply the patch to the frontend and build excalidraw into `frontend`. Run
|
|
||||||
```bash
|
```bash
|
||||||
go run main.go
|
# Visit https://github.com/PatWie/excalidraw-complete/releases/ for the download URL
|
||||||
|
wget <binary-download-url>
|
||||||
|
chmod +x excalidraw-complete
|
||||||
|
./excalidraw-complete
|
||||||
```
|
```
|
||||||
|
|
||||||
Everything will be served under `localhost:3002`
|
Once launched, Excalidraw Complete is accessible at `localhost:3002`, ready for
|
||||||
|
drawing and collaboration.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Excalidraw Complete adapts to your preferences with customizable storage solutions, adjustable via the `STORAGE_TYPE` environment variable:
|
||||||
|
|
||||||
|
- **Filesystem:** Opt for `STORAGE_TYPE=filesystem` and define `LOCAL_STORAGE_PATH` to use a local directory.
|
||||||
|
- **SQLite:** Select `STORAGE_TYPE=sqlite` with `DATA_SOURCE_NAME` for local SQLite storage, including the option for `:memory:` for ephemeral data.
|
||||||
|
- **AWS S3:** Choose `STORAGE_TYPE=s3` and specify `S3_BUCKET_NAME` to leverage S3 bucket storage, ideal for cloud-based solutions.
|
||||||
|
|
||||||
|
These flexible configurations ensure Excalidraw Complete fits seamlessly into your existing setup, whether on-premise or in the cloud.
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
Interested in contributing or customizing? Build Excalidraw Complete from source with these steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone and prepare the Excalidraw frontend
|
||||||
|
git clone https://github.com/excalidraw/excalidraw.git
|
||||||
|
cd excalidraw
|
||||||
|
git checkout tags/v0.17.3
|
||||||
|
git apply ../frontend.patch
|
||||||
|
# Follow build instructions to compile assets into the frontend directory
|
||||||
|
```
|
||||||
|
|
||||||
|
Compile the Go application:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -o excalidraw-complete main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./excalidraw-complete
|
||||||
|
```
|
||||||
|
|
||||||
|
Excalidraw Complete is now running on your machine, ready to bring your collaborative whiteboard ideas to life.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Excalidraw is a fantastic tool, but self-hosting it can be tricky. I welcome
|
||||||
|
your contributions to improve Excalidraw Complete — be it through adding new
|
||||||
|
features, improving existing ones, or bug reports.
|
||||||
|
|||||||
@@ -16,10 +16,29 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/ajg/form v1.5.1 // indirect
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.26.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.27.9 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.9 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.20.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 // indirect
|
||||||
|
github.com/aws/smithy-go v1.20.1 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect
|
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect
|
||||||
github.com/gookit/color v1.5.4 // indirect
|
github.com/gookit/color v1.5.4 // indirect
|
||||||
github.com/gorilla/websocket v1.5.0 // indirect
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
|
|||||||
@@ -2,6 +2,42 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
|||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.26.0 h1:/Ce4OCiM3EkpW7Y+xUnfAFpchU78K7/Ug01sZni9PgA=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.26.0/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.27.9 h1:gRx/NwpNEFSk+yQlgmk1bmxxvQ5TyJ76CWXs9XScTqg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.27.9/go.mod h1:dK1FQfpwpql83kbD873E9vz4FyAxuJtR22wzoXn3qq0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.9 h1:N8s0/7yW+h8qR8WaRlPQeJ6czVMNQVNtNdUqf6cItao=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.9/go.mod h1:446YhIdmSV0Jf/SLafGZalQo+xr2iw7/fzXGDPTU1yQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0 h1:af5YzcLf80tv4Em4jWVD75lpnOHSBkPUZxZfGkrI3HI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0/go.mod h1:nQ3how7DMnFMWiU1SpECohgC82fpn4cKZ875NDMmwtA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4 h1:0ScVK/4qZ8CIW0k8jOeFVsyS/sAiXpYxRBLolMkuLQM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4/go.mod h1:84KyjNZdHC6QZW08nfHI6yZgPd+qRgaWcYsyLUo3QY8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4 h1:sHmMWWX5E7guWEFQ9SVo6A3S4xpPrWnd77a6y4WM6PU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4/go.mod h1:WjpDrhWisWOIoS9n3nk67A3Ll1vfULJ9Kq6h29HTD48=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.4 h1:SIkD6T4zGQ+1YIit22wi37CGNkrE7mXV1vNA5VpI3TI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.4/go.mod h1:XfeqbsG0HNedNs0GT+ju4Bs+pFAwsrlzcRdMvdNVf5s=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.6 h1:NkHCgg0Ck86c5PTOzBZ0JRccI51suJDg5lgFtxBu1ek=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.6/go.mod h1:mjTpxjC8v4SeINTngrnKFgm2QUi+Jm+etTbCxh8W4uU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6 h1:b+E7zIUHMmcB4Dckjpkapoy47W6C9QBv/zoUP+Hn8Kc=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6/go.mod h1:S2fNV0rxrP78NhPbCZeQgY8H9jdDMeGtwcfZIRxzBqU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.4 h1:uDj2K47EM1reAYU9jVlQ1M5YENI1u6a/TxJpf6AeOLA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.4/go.mod h1:XKCODf4RKHppc96c2EZBGV/oCUC7OClxAo2MEyg4pIk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.0 h1:r3o2YsgW9zRcIP3Q0WCmttFVhTuugeKIvT5z9xDspc0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.0/go.mod h1:w2E4f8PUfNtyjfL6Iu+mWI96FGttE03z3UdNcUEC4tA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.20.3 h1:mnbuWHOcM70/OFUlZZ5rcdfA8PflGXXiefU/O+1S3+8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.20.3/go.mod h1:5HFu51Elk+4oRBZVxmHrSds5jFXmFj8C3w7DVF2gnrs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 h1:uLq0BKatTmDzWa/Nu4WO0M1AaQDaPpwTKAeByEc6WFM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3/go.mod h1:b+qdhjnxj8GSR6t5YfphOffeoQSQ1KmpoVVuBn+PWxs=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 h1:J/PpTf/hllOjx8Xu9DMflff3FajfLxqM5+tepvVXmxg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.28.5/go.mod h1:0ih0Z83YDH/QeQ6Ori2yGE2XvWYv/Xm+cZc01LC6oK0=
|
||||||
|
github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
|
||||||
|
github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -27,6 +63,8 @@ github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
|||||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package documents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"excalidraw-complete/core"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/go-chi/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
DocumentCreateResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleCreate(documentStore core.DocumentStore) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := new(bytes.Buffer)
|
||||||
|
_, err := io.Copy(data, r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to copy", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := documentStore.Create(r.Context(), &core.Document{Data: *data})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to save", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.JSON(w, r, DocumentCreateResponse{ID: id})
|
||||||
|
render.Status(r, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleGet(documentStore core.DocumentStore) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id := chi.URLParam(r, "id")
|
||||||
|
document, err := documentStore.FindID(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(document.Data.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,26 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"embed"
|
"embed"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"excalidraw-complete/core"
|
"excalidraw-complete/core"
|
||||||
"excalidraw-complete/documents/memory"
|
documents "excalidraw-complete/handlers/api"
|
||||||
|
"excalidraw-complete/stores"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/go-chi/cors"
|
"github.com/go-chi/cors"
|
||||||
"github.com/go-chi/render"
|
|
||||||
"github.com/zishang520/engine.io/v2/types"
|
"github.com/zishang520/engine.io/v2/types"
|
||||||
socketio "github.com/zishang520/socket.io/v2/socket"
|
socketio "github.com/zishang520/socket.io/v2/socket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
DocumentCreateResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
UserToFollow struct {
|
UserToFollow struct {
|
||||||
SocketId string
|
SocketId string
|
||||||
Username string
|
Username string
|
||||||
@@ -42,33 +34,35 @@ type (
|
|||||||
//go:embed all:frontend
|
//go:embed all:frontend
|
||||||
var assets embed.FS
|
var assets embed.FS
|
||||||
|
|
||||||
func Assets() (fs.FS, error) {
|
func handleUI() http.Handler {
|
||||||
return fs.Sub(assets, "frontend")
|
|
||||||
}
|
|
||||||
|
|
||||||
type FrontEndHandler struct{}
|
|
||||||
|
|
||||||
func (h FrontEndHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
frontendHandler(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frontendHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
upath := r.URL.Path
|
|
||||||
if !strings.HasPrefix(upath, "/") {
|
|
||||||
upath = "/" + upath
|
|
||||||
r.URL.Path = upath
|
|
||||||
}
|
|
||||||
upath = path.Clean(upath)
|
|
||||||
|
|
||||||
sub, err := fs.Sub(assets, "frontend")
|
sub, err := fs.Sub(assets, "frontend")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
http.FileServer(http.FS(sub)).ServeHTTP(w, r)
|
return http.FileServer(http.FS(sub))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func setupRouter(documentStore core.DocumentStore) *chi.Mux {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
|
||||||
|
r.Use(cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: []string{"https://*", "http://*"},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "Content-Length", "X-CSRF-Token", "Token", "session", "Origin", "Host", "Connection", "Accept-Encoding", "Accept-Language", "X-Requested-With"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.Route("/api/v2", func(r chi.Router) {
|
||||||
|
r.Post("/post/", documents.HandleCreate(documentStore))
|
||||||
|
r.Route("/{id}", func(r chi.Router) {
|
||||||
|
r.Get("/", documents.HandleGet(documentStore))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
func setupSocketIO() *socketio.Server {
|
||||||
opts := socketio.DefaultServerOptions()
|
opts := socketio.DefaultServerOptions()
|
||||||
opts.SetMaxHttpBufferSize(5000000)
|
opts.SetMaxHttpBufferSize(5000000)
|
||||||
opts.SetPath("/socket.io")
|
opts.SetPath("/socket.io")
|
||||||
@@ -153,56 +147,11 @@ func main() {
|
|||||||
socket.Disconnect(true)
|
socket.Disconnect(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
return ioo
|
||||||
|
|
||||||
r := chi.NewRouter()
|
}
|
||||||
r.Use(middleware.Logger)
|
|
||||||
|
|
||||||
r.Use(cors.Handler(cors.Options{
|
|
||||||
AllowedOrigins: []string{"https://*", "http://*"},
|
|
||||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
|
||||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "Content-Length", "X-CSRF-Token", "Token", "session", "Origin", "Host", "Connection", "Accept-Encoding", "Accept-Language", "X-Requested-With"},
|
|
||||||
AllowCredentials: true,
|
|
||||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
|
||||||
}))
|
|
||||||
|
|
||||||
documentStore := memory.NewDocumentStore()
|
|
||||||
|
|
||||||
r.Mount("/", FrontEndHandler{})
|
|
||||||
|
|
||||||
r.Route("/api/v2", func(r chi.Router) {
|
|
||||||
r.Post("/post/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
data := new(bytes.Buffer)
|
|
||||||
_, err := io.Copy(data, r.Body)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to copy", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id, err := documentStore.Create(r.Context(), &core.Document{Data: *data})
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Failed to save", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.JSON(w, r, DocumentCreateResponse{ID: id})
|
|
||||||
render.Status(r, http.StatusOK)
|
|
||||||
})
|
|
||||||
r.Route("/{id}", func(r chi.Router) {
|
|
||||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id := chi.URLParam(r, "id")
|
|
||||||
document, err := documentStore.FindID(r.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(document.Data.Bytes())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
r.Handle("/socket.io/", ioo.ServeHandler(nil))
|
|
||||||
go http.ListenAndServe(":3002", r)
|
|
||||||
fmt.Println("listen on 3002")
|
|
||||||
|
|
||||||
|
func waitForShutdown(ioo *socketio.Server) {
|
||||||
exit := make(chan struct{})
|
exit := make(chan struct{})
|
||||||
SignalC := make(chan os.Signal)
|
SignalC := make(chan os.Signal)
|
||||||
|
|
||||||
@@ -220,4 +169,20 @@ func main() {
|
|||||||
<-exit
|
<-exit
|
||||||
ioo.Close(nil)
|
ioo.Close(nil)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
fmt.Println("Shutting down...")
|
||||||
|
// TODO(patwie): Close other resources
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
documentStore := stores.GetStore() // Make sure this is well-defined in your "stores" package
|
||||||
|
r := setupRouter(documentStore)
|
||||||
|
ioo := setupSocketIO()
|
||||||
|
r.Handle("/socket.io/", ioo.ServeHandler(nil))
|
||||||
|
r.Mount("/", handleUI())
|
||||||
|
|
||||||
|
go http.ListenAndServe(":3002", r)
|
||||||
|
fmt.Println("listen on 3002")
|
||||||
|
waitForShutdown(ioo)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"excalidraw-complete/core"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type documentStore struct {
|
||||||
|
s3Client *s3.Client
|
||||||
|
bucket string // Name of the S3 bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentStore(bucketName string) core.DocumentStore {
|
||||||
|
cfg, err := config.LoadDefaultConfig(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to load SDK config, %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Client := s3.NewFromConfig(cfg)
|
||||||
|
|
||||||
|
return &documentStore{
|
||||||
|
s3Client: s3Client,
|
||||||
|
bucket: bucketName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentStore) FindID(ctx context.Context, id string) (*core.Document, error) {
|
||||||
|
resp, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(id),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get document with id %s: %v", id, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read document data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
document := core.Document{
|
||||||
|
Data: *bytes.NewBuffer(data),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &document, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentStore) Create(ctx context.Context, document *core.Document) (string, error) {
|
||||||
|
id := ulid.Make().String()
|
||||||
|
|
||||||
|
_, err := s.s3Client.PutObject(ctx, &s3.PutObjectInput{
|
||||||
|
Bucket: aws.String(s.bucket),
|
||||||
|
Key: aws.String(id),
|
||||||
|
Body: bytes.NewReader(document.Data.Bytes()),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to upload document: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package filesystem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"excalidraw-complete/core"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type documentStore struct {
|
||||||
|
basePath string // Directory where documents are stored.
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentStore(basePath string) core.DocumentStore {
|
||||||
|
if err := os.MkdirAll(basePath, 0755); err != nil {
|
||||||
|
log.Fatalf("failed to create base directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &documentStore{basePath: basePath}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentStore) FindID(ctx context.Context, id string) (*core.Document, error) {
|
||||||
|
filePath := filepath.Join(s.basePath, id)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("document with id %s not found", id)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
document := core.Document{
|
||||||
|
Data: *bytes.NewBuffer(data),
|
||||||
|
}
|
||||||
|
|
||||||
|
return &document, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentStore) Create(ctx context.Context, document *core.Document) (string, error) {
|
||||||
|
id := ulid.Make().String()
|
||||||
|
filePath := filepath.Join(s.basePath, id)
|
||||||
|
|
||||||
|
if err := os.WriteFile(filePath, document.Data.Bytes(), 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package sqlite
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"excalidraw-complete/core"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var savedDocuments = make(map[string]core.Document)
|
||||||
|
|
||||||
|
type documentStore struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocumentStore(dataSourceName string) core.DocumentStore {
|
||||||
|
// db, err := sql.Open("sqlite3", ":memory:")
|
||||||
|
db, err := sql.Open("sqlite3", dataSourceName)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
sts := `CREATE TABLE IF NOT EXISTS documents (id TEXT PRIMARY KEY, data BLOB);`
|
||||||
|
_, err = db.Exec(sts)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return &documentStore{db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentStore) FindID(ctx context.Context, id string) (*core.Document, error) {
|
||||||
|
var data []byte
|
||||||
|
err := s.db.QueryRowContext(ctx, "SELECT data FROM documents WHERE id = ?", id).Scan(&data)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, fmt.Errorf("document with id %s not found", id)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
document := core.Document{
|
||||||
|
Data: *bytes.NewBuffer(data),
|
||||||
|
}
|
||||||
|
return &document, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *documentStore) Create(ctx context.Context, document *core.Document) (string, error) {
|
||||||
|
id := ulid.Make().String()
|
||||||
|
data := document.Data.Bytes()
|
||||||
|
_, err := s.db.ExecContext(ctx, "INSERT INTO documents (id, data) VALUES (?, ?)", id, data)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package stores
|
||||||
|
|
||||||
|
import (
|
||||||
|
"excalidraw-complete/core"
|
||||||
|
"excalidraw-complete/stores/aws"
|
||||||
|
"excalidraw-complete/stores/filesystem"
|
||||||
|
"excalidraw-complete/stores/memory"
|
||||||
|
"excalidraw-complete/stores/sqlite"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetStore() core.DocumentStore {
|
||||||
|
storageType := os.Getenv("STORAGE_TYPE")
|
||||||
|
var store core.DocumentStore
|
||||||
|
|
||||||
|
switch storageType {
|
||||||
|
case "filesystem":
|
||||||
|
basePath := os.Getenv("LOCAL_STORAGE_PATH")
|
||||||
|
store = filesystem.NewDocumentStore(basePath)
|
||||||
|
case "sqlite":
|
||||||
|
dataSourceName := os.Getenv("DATA_SOURCE_NAME")
|
||||||
|
store = sqlite.NewDocumentStore(dataSourceName)
|
||||||
|
case "s3":
|
||||||
|
bucketName := os.Getenv("S3_BUCKET_NAME")
|
||||||
|
store = aws.NewDocumentStore(bucketName)
|
||||||
|
default:
|
||||||
|
store = memory.NewDocumentStore()
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user