diff --git a/.gitignore b/.gitignore index 81c391f..aa6a9cb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules .env *.env */build/* +*.db diff --git a/core/canvas.go b/core/canvas.go index a501421..80849df 100644 --- a/core/canvas.go +++ b/core/canvas.go @@ -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"` diff --git a/go.mod b/go.mod index 173ac6c..a51157a 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 6d4df94..3d8aa03 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/handlers/api/kv/kv.go b/handlers/api/kv/kv.go index c7389f9..8e45e6a 100644 --- a/handlers/api/kv/kv.go +++ b/handlers/api/kv/kv.go @@ -113,20 +113,27 @@ 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 != "" { - canvasName = 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, - Data: body, + ID: key, + UserID: claims.Subject, + Name: canvasName, + Thumbnail: canvasThumbnail, + Data: body, } if err := store.Save(r.Context(), canvas); err != nil { diff --git a/stores/aws/store.go b/stores/aws/store.go index c21f942..4bd9e8b 100644 --- a/stores/aws/store.go +++ b/stores/aws/store.go @@ -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) diff --git a/stores/filesystem/store.go b/stores/filesystem/store.go index 7caadab..cc516f4 100644 --- a/stores/filesystem/store.go +++ b/stores/filesystem/store.go @@ -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 } diff --git a/stores/memory/store.go b/stores/memory/store.go index 971b116..d271283 100644 --- a/stores/memory/store.go +++ b/stores/memory/store.go @@ -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, } diff --git a/stores/sqlite/store.go b/stores/sqlite/store.go index 172bb3a..8170a00 100644 --- a/stores/sqlite/store.go +++ b/stores/sqlite/store.go @@ -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 {