mirror of
https://github.com/Dvorinka/excalidraw-full.git
synced 2026-06-03 22:02:57 +00:00
Add thumbnail support to Canvas and storage backends
Introduces a 'thumbnail' field to the Canvas model and updates all storage backends (AWS S3, filesystem, memory, and SQLite) to handle storing and retrieving this field. Also updates the API handler to accept and save the thumbnail, and switches SQLite driver to modernc.org/sqlite for improved compatibility. Updates .gitignore to exclude .db files.
This commit is contained in:
@@ -8,3 +8,4 @@ node_modules
|
||||
.env
|
||||
*.env
|
||||
*/build/*
|
||||
*.db
|
||||
|
||||
@@ -11,6 +11,7 @@ type (
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"-"` // Not exposed in JSON responses, used internally.
|
||||
Name string `json:"name"`
|
||||
Thumbnail string `json:"thumbnail,omitempty"`
|
||||
Data []byte `json:"-"` // The full canvas data, not included in list views.
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
|
||||
@@ -11,11 +11,14 @@ require (
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/zishang520/engine.io/v2 v2.0.6
|
||||
github.com/zishang520/socket.io/v2 v2.0.5
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
modernc.org/sqlite v1.38.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -36,30 +39,36 @@ require (
|
||||
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/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/quic-go/quic-go v0.40.1 // indirect
|
||||
github.com/quic-go/webtransport-go v0.6.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
github.com/zishang520/engine.io-go-parser v1.2.3 // indirect
|
||||
github.com/zishang520/socket.io-go-parser/v2 v2.0.4 // indirect
|
||||
go.uber.org/mock v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.17.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
modernc.org/libc v1.65.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -41,6 +41,8 @@ github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
@@ -57,20 +59,24 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ=
|
||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
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/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
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/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
|
||||
@@ -88,6 +94,8 @@ github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1
|
||||
github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
|
||||
github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFDD3NxaZLY=
|
||||
github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -111,26 +119,53 @@ github.com/zishang520/socket.io/v2 v2.0.5 h1:CImu9z6YKFif2mMX2b3y2OUhxxH8nz01PqP
|
||||
github.com/zishang520/socket.io/v2 v2.0.5/go.mod h1:r+spG2g+Q0lxhgTHevGl7/h4DzkKrO00i8AEF9vj2PQ=
|
||||
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
|
||||
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
|
||||
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
|
||||
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
|
||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
@@ -113,19 +113,26 @@ func HandleSaveCanvas(store stores.Store) http.HandlerFunc {
|
||||
AppState struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"appState"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
}
|
||||
// We make a copy of the body because json.Unmarshal will consume the reader.
|
||||
bodyCopy := make([]byte, len(body))
|
||||
copy(bodyCopy, body)
|
||||
|
||||
canvasName := key // Default to key
|
||||
if err := json.Unmarshal(bodyCopy, &canvasData); err == nil && canvasData.AppState.Name != "" {
|
||||
var canvasThumbnail string
|
||||
if err := json.Unmarshal(bodyCopy, &canvasData); err == nil {
|
||||
if canvasData.AppState.Name != "" {
|
||||
canvasName = canvasData.AppState.Name
|
||||
}
|
||||
canvasThumbnail = canvasData.Thumbnail
|
||||
}
|
||||
|
||||
canvas := &core.Canvas{
|
||||
ID: key,
|
||||
UserID: claims.Subject,
|
||||
Name: canvasName,
|
||||
Thumbnail: canvasThumbnail,
|
||||
Data: body,
|
||||
}
|
||||
|
||||
|
||||
+54
-18
@@ -3,15 +3,20 @@ package aws
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"excalidraw-complete/core"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
@@ -90,14 +95,30 @@ func (s *s3Store) List(ctx context.Context, userID string) ([]*core.Canvas, erro
|
||||
|
||||
canvases := make([]*core.Canvas, 0, len(output.Contents))
|
||||
for _, object := range output.Contents {
|
||||
canvasID := path.Base(*object.Key)
|
||||
canvas := &core.Canvas{
|
||||
ID: canvasID,
|
||||
UserID: userID,
|
||||
Name: canvasID, // S3 doesn't have a native 'name' field, using ID.
|
||||
UpdatedAt: *object.LastModified,
|
||||
resp, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: object.Key,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("warn: failed to get object %s: %v", *object.Key, err)
|
||||
continue
|
||||
}
|
||||
canvases = append(canvases, canvas)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
log.Printf("warn: failed to read object body %s: %v", *object.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
var canvas core.Canvas
|
||||
if err := json.Unmarshal(data, &canvas); err != nil {
|
||||
log.Printf("warn: failed to unmarshal canvas %s: %v", *object.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// For list view, we don't need the full data blob.
|
||||
canvas.Data = nil
|
||||
canvases = append(canvases, &canvas)
|
||||
}
|
||||
|
||||
return canvases, nil
|
||||
@@ -111,35 +132,50 @@ func (s *s3Store) Get(ctx context.Context, userID, id string) (*core.Canvas, err
|
||||
})
|
||||
if err != nil {
|
||||
// A specific check for NoSuchKey can be useful here.
|
||||
if bytes.Contains([]byte(err.Error()), []byte("NoSuchKey")) {
|
||||
var nsk *s3types.NoSuchKey
|
||||
if errors.As(err, &nsk) {
|
||||
return nil, fmt.Errorf("canvas not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get canvas %s: %v", id, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read canvas data: %v", err)
|
||||
}
|
||||
|
||||
canvas := &core.Canvas{
|
||||
ID: id,
|
||||
UserID: userID,
|
||||
Name: id,
|
||||
Data: data,
|
||||
UpdatedAt: *resp.LastModified,
|
||||
var canvas core.Canvas
|
||||
if err := json.Unmarshal(data, &canvas); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal canvas data: %v", err)
|
||||
}
|
||||
|
||||
return canvas, nil
|
||||
return &canvas, nil
|
||||
}
|
||||
|
||||
func (s *s3Store) Save(ctx context.Context, canvas *core.Canvas) error {
|
||||
key := s.getCanvasKey(canvas.UserID, canvas.ID)
|
||||
_, err := s.s3Client.PutObject(ctx, &s3.PutObjectInput{
|
||||
|
||||
// Preserve CreatedAt on update
|
||||
if canvas.CreatedAt.IsZero() {
|
||||
existing, err := s.Get(ctx, canvas.UserID, canvas.ID)
|
||||
if err == nil && existing != nil {
|
||||
canvas.CreatedAt = existing.CreatedAt
|
||||
} else {
|
||||
canvas.CreatedAt = time.Now()
|
||||
}
|
||||
}
|
||||
canvas.UpdatedAt = time.Now()
|
||||
|
||||
data, err := json.Marshal(canvas)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal canvas: %v", err)
|
||||
}
|
||||
|
||||
_, err = s.s3Client.PutObject(ctx, &s3.PutObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(key),
|
||||
Body: bytes.NewReader(canvas.Data),
|
||||
Body: bytes.NewReader(data),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save canvas %s: %v", canvas.ID, err)
|
||||
|
||||
+35
-24
@@ -3,6 +3,7 @@ package filesystem
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"excalidraw-complete/core"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -90,18 +91,22 @@ func (s *fsStore) List(ctx context.Context, userID string) ([]*core.Canvas, erro
|
||||
canvases := make([]*core.Canvas, 0, len(files))
|
||||
for _, file := range files {
|
||||
if !file.IsDir() {
|
||||
info, err := file.Info()
|
||||
filePath := filepath.Join(userPath, file.Name())
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.WithError(err).Warn("Failed to get file info, skipping file")
|
||||
log.WithError(err).Warnf("Failed to read canvas file %s, skipping", file.Name())
|
||||
continue
|
||||
}
|
||||
canvas := &core.Canvas{
|
||||
ID: file.Name(),
|
||||
UserID: userID,
|
||||
Name: file.Name(),
|
||||
UpdatedAt: info.ModTime(),
|
||||
|
||||
var canvas core.Canvas
|
||||
if err := json.Unmarshal(data, &canvas); err != nil {
|
||||
log.WithError(err).Warnf("Failed to unmarshal canvas file %s, skipping", file.Name())
|
||||
continue
|
||||
}
|
||||
canvases = append(canvases, canvas)
|
||||
|
||||
// For list view, we don't need the full data blob.
|
||||
canvas.Data = nil
|
||||
canvases = append(canvases, &canvas)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,16 +135,15 @@ func (s *fsStore) Get(ctx context.Context, userID, id string) (*core.Canvas, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canvas := &core.Canvas{
|
||||
ID: id,
|
||||
UserID: userID,
|
||||
Name: id,
|
||||
Data: data,
|
||||
UpdatedAt: info.ModTime(),
|
||||
var canvas core.Canvas
|
||||
if err := json.Unmarshal(data, &canvas); err != nil {
|
||||
log.WithError(err).Error("Failed to unmarshal canvas data")
|
||||
return nil, err
|
||||
}
|
||||
canvas.UpdatedAt = info.ModTime()
|
||||
|
||||
log.Info("Canvas retrieved successfully")
|
||||
return canvas, nil
|
||||
return &canvas, nil
|
||||
}
|
||||
|
||||
func (s *fsStore) Save(ctx context.Context, canvas *core.Canvas) error {
|
||||
@@ -152,21 +156,28 @@ func (s *fsStore) Save(ctx context.Context, canvas *core.Canvas) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set creation/update time before saving
|
||||
info, err := os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
canvas.CreatedAt = time.Now()
|
||||
} else if err == nil {
|
||||
canvas.CreatedAt = info.ModTime() // This is not ideal, but filesystem doesn't store creation time easily.
|
||||
}
|
||||
canvas.UpdatedAt = time.Now()
|
||||
|
||||
log.Info("Saving canvas")
|
||||
err := os.WriteFile(filePath, canvas.Data, 0644)
|
||||
data, err := json.Marshal(canvas)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to marshal canvas for saving")
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(filePath, data, 0644)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to write canvas file")
|
||||
return err
|
||||
}
|
||||
|
||||
// Set modification time for consistency, though WriteFile usually does this.
|
||||
// We preserve created time logic in the storage layer if needed.
|
||||
now := time.Now()
|
||||
canvas.UpdatedAt = now
|
||||
|
||||
// A full implementation would handle CreatedAt by checking if the file exists first.
|
||||
// For this KV-like store, we'll just update ModTime via WriteFile.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ func (s *memStore) List(ctx context.Context, userID string) ([]*core.Canvas, err
|
||||
ID: canvas.ID,
|
||||
UserID: canvas.UserID,
|
||||
Name: canvas.Name,
|
||||
Thumbnail: canvas.Thumbnail,
|
||||
CreatedAt: canvas.CreatedAt,
|
||||
UpdatedAt: canvas.UpdatedAt,
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type sqliteStore struct {
|
||||
@@ -20,7 +20,7 @@ type sqliteStore struct {
|
||||
|
||||
// NewStore creates a new SQLite-based store.
|
||||
func NewStore(dataSourceName string) *sqliteStore {
|
||||
db, err := sql.Open("sqlite3", dataSourceName)
|
||||
db, err := sql.Open("sqlite", dataSourceName)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open sqlite database: %v", err)
|
||||
}
|
||||
@@ -37,6 +37,7 @@ func NewStore(dataSourceName string) *sqliteStore {
|
||||
id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT,
|
||||
thumbnail TEXT,
|
||||
data BLOB,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
@@ -89,7 +90,7 @@ func (s *sqliteStore) Create(ctx context.Context, document *core.Document) (stri
|
||||
|
||||
// CanvasStore implementation
|
||||
func (s *sqliteStore) List(ctx context.Context, userID string) ([]*core.Canvas, error) {
|
||||
rows, err := s.db.QueryContext(ctx, "SELECT id, name, updated_at FROM canvases WHERE user_id = ?", userID)
|
||||
rows, err := s.db.QueryContext(ctx, "SELECT id, name, updated_at, thumbnail FROM canvases WHERE user_id = ?", userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,7 +100,7 @@ func (s *sqliteStore) List(ctx context.Context, userID string) ([]*core.Canvas,
|
||||
for rows.Next() {
|
||||
var canvas core.Canvas
|
||||
canvas.UserID = userID
|
||||
if err := rows.Scan(&canvas.ID, &canvas.Name, &canvas.UpdatedAt); err != nil {
|
||||
if err := rows.Scan(&canvas.ID, &canvas.Name, &canvas.UpdatedAt, &canvas.Thumbnail); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canvases = append(canvases, &canvas)
|
||||
@@ -111,7 +112,7 @@ func (s *sqliteStore) Get(ctx context.Context, userID, id string) (*core.Canvas,
|
||||
var canvas core.Canvas
|
||||
canvas.UserID = userID
|
||||
canvas.ID = id
|
||||
err := s.db.QueryRowContext(ctx, "SELECT name, data, created_at, updated_at FROM canvases WHERE user_id = ? AND id = ?", userID, id).Scan(&canvas.Name, &canvas.Data, &canvas.CreatedAt, &canvas.UpdatedAt)
|
||||
err := s.db.QueryRowContext(ctx, "SELECT name, data, created_at, updated_at, thumbnail FROM canvases WHERE user_id = ? AND id = ?", userID, id).Scan(&canvas.Name, &canvas.Data, &canvas.CreatedAt, &canvas.UpdatedAt, &canvas.Thumbnail)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("canvas not found")
|
||||
@@ -138,10 +139,10 @@ func (s *sqliteStore) Save(ctx context.Context, canvas *core.Canvas) error {
|
||||
|
||||
if exists {
|
||||
// Update
|
||||
_, err = tx.ExecContext(ctx, "UPDATE canvases SET name = ?, data = ?, updated_at = ? WHERE user_id = ? AND id = ?", canvas.Name, canvas.Data, now, canvas.UserID, canvas.ID)
|
||||
_, err = tx.ExecContext(ctx, "UPDATE canvases SET name = ?, data = ?, updated_at = ?, thumbnail = ? WHERE user_id = ? AND id = ?", canvas.Name, canvas.Data, now, canvas.Thumbnail, canvas.UserID, canvas.ID)
|
||||
} else {
|
||||
// Insert
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO canvases (id, user_id, name, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", canvas.ID, canvas.UserID, canvas.Name, canvas.Data, now, now)
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO canvases (id, user_id, name, data, created_at, updated_at, thumbnail) VALUES (?, ?, ?, ?, ?, ?, ?)", canvas.ID, canvas.UserID, canvas.Name, canvas.Data, now, now, canvas.Thumbnail)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user