forked from tildetown/town
stubby wip for town share
This commit is contained in:
parent
ecfe9c19ef
commit
788900d60e
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,3 +13,5 @@ external/cmd/helpers/emailtouser/emailtouser
|
||||
external/cmd/helpers/createkeyfile/createkeyfile
|
||||
external/cmd/helpers/registeruser/registeruser
|
||||
external/cmd/helpers/appendkeyfile/appendkeyfile
|
||||
share/share
|
||||
share/uploads
|
||||
|
69
share/README.md
Normal file
69
share/README.md
Normal file
@ -0,0 +1,69 @@
|
||||
# town share
|
||||
|
||||
_being a way to share multimedia with each other that mimics rot_
|
||||
|
||||
sometimes, townies want to share images and videos with each other.
|
||||
|
||||
this leads to a set of behaviors:
|
||||
|
||||
- using a corporate photo album soft ware to create a public link (feels antithetical to town culture)
|
||||
- uploading to a photo sharing site to get a link (there aren't a ton of these left, many are shady)
|
||||
- SCPing a photo to town (clumsy when you just want to share from phone, uses disk space inefficiently)
|
||||
|
||||
though this is not a bleeding wound of a community issue, it is one that makes me itch. a kind of mosquito bite.
|
||||
|
||||
i do not wish to be in the business of just hosting people's massive photos. that sounds unfun and annoying.
|
||||
|
||||
however, i _do_ wish to be in the business of hosting artfully lossy versions of people's photos. the business grows even tastier if those photos rot -- one pixel at a time, perhaps -- until there's nothing left. disk usage should always converge on zero. the undifferentiated smear of a brown leafed forest floor. better yet: every photo leaves a unique stain of pixels. storage compact, just enough to say "there was a thing here. here's what is left."
|
||||
|
||||
## the flow
|
||||
|
||||
1. user has a picture they want to share with a persistent, easy to copy and paste url
|
||||
2. user opens a webpage, https://share.tilde.town, which has an "upload image" form at the top
|
||||
3. they select a style, one of:
|
||||
a. dithered
|
||||
b. heavily compressed
|
||||
c. 1 bit
|
||||
d. ascii art
|
||||
4. they choose whether or not to secretify this upload
|
||||
5. the image is uploaded; the bits are stored somewhere (disk or blob storage) and its initial checksum is saved as a seed value
|
||||
6. the user is redirected to their photo
|
||||
|
||||
meanwhile, a cron job rots the photos on disk. an array of pixel values is computed based on the original checksum: these pixels are sacred and uncorruptible. N random cells are rotted (fewer for ascii art). fully rotted images are noted as done and ignored by the cron job.
|
||||
|
||||
## a bugaboo
|
||||
|
||||
i want to think through authentication. my first thought was that this would be fully public. sure, that's an option. but it will keep me up at night.
|
||||
|
||||
better that townies all have a secret unique to them they can print out while on server for storage in a cookie. that cookie lets them upload. it also generates galleries for them including the uploads not marked as secretified.
|
||||
|
||||
thus, https://share.tilde.town/~vilmibm will have my photos flowed on out.
|
||||
|
||||
## cli tool
|
||||
|
||||
town share has a corresponding CLI tool for use on or off the town. it reads the same secret used on the web site and uploads the image to the server.
|
||||
|
||||
```
|
||||
$ town-share foo.jpg
|
||||
flavor: 1. dithering 2. heavy compression 3. 1 bit 4. ascii art
|
||||
include in web galley? Yn
|
||||
|
||||
file's done.
|
||||
|
||||
https://share.tilde.town/~vilmibm/abc123.gif
|
||||
```
|
||||
|
||||
## future dreams
|
||||
|
||||
the big future dream is to handle other forms of media -- video and sound. but to start I am happy with just images.
|
||||
|
||||
## the questions
|
||||
|
||||
- what image format to use in storage? iirc the dithering tricks only work for gif so that's easy. ascii art is txt. maybe heavy compression can be jpg? 1 bit could be a bmp..?
|
||||
- where to store files?
|
||||
- duh -- in people's home dirs. if they want to delete an image, remove it from the directory.
|
||||
- this has the side effect of letting people put arbitrary images into the directory; i think that's a feature not a bug.
|
||||
- what format for image names?
|
||||
- initially i had the thought they should be date sorted. but, why! fuck that, just lexigraphically sort and let the filename be a random hash of some kind. something like: 8 digits, alphanumeric, dash in the middle, extension
|
||||
- what sort of secret? i can just mimic whatever i do for invite codes.
|
||||
- what state is required beyond the files? i want a notion of "done" for rotting. that could be a file size. ie, rot until a file is below a certain size. since the whole point of rotting (in addition to aesthetics/philosophy/lulz) is to reduce disk impact over time, file size should be the indication of whether further rot is required.
|
37
share/go.mod
Normal file
37
share/go.mod
Normal file
@ -0,0 +1,37 @@
|
||||
module git.tilde.town/tildetown/share
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/google/uuid v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
91
share/go.sum
Normal file
91
share/go.sum
Normal file
@ -0,0 +1,91 @@
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
107
share/main.go
Normal file
107
share/main.go
Normal file
@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
UPLOAD_DIR = "./uploads"
|
||||
MAX_FILE_SIZE = 10 << 20
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := os.MkdirAll(UPLOAD_DIR, 0755); err != nil {
|
||||
panic(fmt.Sprintf("Failed to create upload directory: %v", err))
|
||||
}
|
||||
|
||||
router := gin.Default()
|
||||
|
||||
router.LoadHTMLGlob("templates/*")
|
||||
|
||||
router.GET("/", homeHandler)
|
||||
router.POST("/upload", uploadHandler)
|
||||
|
||||
router.Static("/static", "./static")
|
||||
|
||||
router.Run(":8787")
|
||||
}
|
||||
|
||||
func homeHandler(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "town share",
|
||||
})
|
||||
}
|
||||
|
||||
func uploadHandler(c *gin.Context) {
|
||||
file, header, err := c.Request.FormFile("image")
|
||||
if err != nil {
|
||||
renderError(c, "melancholy")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if header.Size > MAX_FILE_SIZE {
|
||||
renderError(c, "max size is 10mb")
|
||||
return
|
||||
}
|
||||
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if !isValidImageType(contentType) {
|
||||
renderError(c, "only jpg, png, and gif are supported")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO tweak this
|
||||
extension := filepath.Ext(header.Filename)
|
||||
uniqueFilename := fmt.Sprintf("%s-%s%s",
|
||||
uuid.New().String(),
|
||||
time.Now().Format("20060102-150405"),
|
||||
extension,
|
||||
)
|
||||
|
||||
destPath := filepath.Join(UPLOAD_DIR, uniqueFilename)
|
||||
dest, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
renderError(c, "Failed to save file")
|
||||
return
|
||||
}
|
||||
defer dest.Close()
|
||||
|
||||
if _, err := io.Copy(dest, file); err != nil {
|
||||
renderError(c, "Failed to save file")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO global const for title
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "town share",
|
||||
"success": fmt.Sprintf("file uploaded successfully as %s", uniqueFilename),
|
||||
})
|
||||
}
|
||||
|
||||
// renderError renders the home page with an error message
|
||||
func renderError(c *gin.Context, message string) {
|
||||
c.HTML(http.StatusOK, "index.html", gin.H{
|
||||
"title": "town share",
|
||||
"error": message,
|
||||
})
|
||||
}
|
||||
|
||||
// isValidImageType checks if the content type is a supported image format
|
||||
func isValidImageType(contentType string) bool {
|
||||
validTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/jpg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
}
|
||||
return validTypes[contentType]
|
||||
}
|
87
share/templates/index.html
Normal file
87
share/templates/index.html
Normal file
@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ .title }}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
}
|
||||
.message {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
.success {
|
||||
background-color: #dff0d8;
|
||||
color: #3c763d;
|
||||
border: 1px solid #d6e9c6;
|
||||
}
|
||||
.error {
|
||||
background-color: #f2dede;
|
||||
color: #a94442;
|
||||
border: 1px solid #ebccd1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>town share</h1>
|
||||
<p style="text-align: center;"></p>
|
||||
|
||||
<form action="/upload" method="post" id="uploadForm" class="upload-form" enctype="multipart/form-data">
|
||||
<input type="file" id="imageInput" name="image" accept="image/jpeg,image/png,image/gif" required>
|
||||
<p>jpg, png, gif (max 10mb)</p>
|
||||
<button type="submit" class="upload-button" id="uploadButton" disabled>Upload Image</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{ if .error }}
|
||||
<div class="message error">{{ .error }}</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if .success }}
|
||||
<div class="message success">{{ .success }}</div>
|
||||
{{ end }}
|
||||
|
||||
<div id="messageArea" class="message">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fileInput = document.getElementById('imageInput');
|
||||
const uploadForm = document.getElementById('uploadForm');
|
||||
const uploadButton = document.getElementById('uploadButton');
|
||||
const messageArea = document.getElementById('messageArea');
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFileSelection(e.target.files);
|
||||
});
|
||||
|
||||
function handleFileSelection(files) {
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type.startsWith('image/')) {
|
||||
if (file.size <= 10 * 1024 * 1024) { // 10MB limit
|
||||
fileInput.files = files;
|
||||
uploadButton.disabled = false;
|
||||
dropZone.querySelector('p').textContent = `Selected: ${file.name}`;
|
||||
} else {
|
||||
showMessage('File is too large. Maximum size is 10MB.', 'error');
|
||||
}
|
||||
} else {
|
||||
showMessage('Please select an image file (JPG, PNG, or GIF).', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showMessage(text, type) {
|
||||
messageArea.innerHTML = `<div class="message ${type}">${text}</div>`;
|
||||
if (type === 'error') {
|
||||
uploadButton.disabled = true;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user