diff --git a/go.mod b/go.mod index f9ea395..d7316ce 100644 --- a/go.mod +++ b/go.mod @@ -1,40 +1,10 @@ module simpleshare -go 1.23.3 +go 1.25.12 -toolchain go1.24.3 +require github.com/spf13/cobra v1.10.2 require ( - github.com/gin-gonic/gin v1.10.1 - github.com/spf13/cobra v1.9.1 -) - -require ( - github.com/bytedance/sonic v1.13.3 // indirect - github.com/bytedance/sonic/loader v0.2.4 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect - github.com/gin-contrib/sse v1.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.26.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // 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.4 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.14 // indirect - golang.org/x/arch v0.18.0 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/spf13/pflag v1.0.9 // indirect ) diff --git a/go.sum b/go.sum index 5384d8f..a6ee3e0 100644 --- a/go.sum +++ b/go.sum @@ -1,92 +1,10 @@ -github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= -github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= -github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -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.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= -github.com/gin-gonic/gin v1.10.1/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.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= -github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -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.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -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.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -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/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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.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.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw= -github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= -golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -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.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= diff --git a/http.go b/http.go index a915c45..6ba7452 100644 --- a/http.go +++ b/http.go @@ -2,14 +2,18 @@ package main import ( "embed" + "encoding/json" + "errors" "fmt" "html/template" + "io" "log" "net/http" "os" + "path" "path/filepath" - - "github.com/gin-gonic/gin" + "strings" + "sync" ) //go:embed web @@ -26,130 +30,189 @@ var ( sData string imgList []string // img url list fileList []tmpFile // file list + stateMu sync.RWMutex ) func runServer() error { - gin.SetMode(gin.ReleaseMode) - r := gin.Default() - err := r.SetTrustedProxies(nil) + mux, err := setRouter() if err != nil { - panic(err) + return err } - setRouter(r) - loadOldFiles() - // Set memory limit for multipart forms - r.MaxMultipartMemory = 20 << 20 // 20 MiB - ads := address + ":" + port fmt.Println("server started at http://" + ads) - return r.Run(ads) + srv := &http.Server{ + Addr: ads, + Handler: mux, + } + + return srv.ListenAndServe() } -func setRouter(r *gin.Engine) { +func setRouter() (*http.ServeMux, error) { + tempHTML, err := template.New("").ParseFS(webDir, "web/*.html") + if err != nil { + return nil, err + } + + mux := http.NewServeMux() - tempHtml := template.Must(template.New("").ParseFS(webDir, "web/*.html")) - r.SetHTMLTemplate(tempHtml) - r.LoadHTMLGlob("web/*.html") - // r.Static("/static", "./static") - r.StaticFS("/static", http.FS(webDir)) - r.GET("/", func(c *gin.Context) { - c.HTML(http.StatusOK, "index.html", gin.H{"data": sData, "imgList": imgList, "fileList": fileList}) + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(webDir)))) + mux.Handle("/tFile/", http.StripPrefix("/tFile/", http.FileServer(http.Dir(tmpFileDir)))) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + renderIndex(w, tempHTML) }) - r.POST("/submit", func(c *gin.Context) { - sData = c.PostForm("sData") - c.JSON(http.StatusOK, gin.H{ + mux.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if err := r.ParseForm(); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) + return + } + + stateMu.Lock() + sData = r.PostFormValue("sData") + resp := map[string]any{ "success": true, "data": sData, - "imgList": imgList, - "fileList": fileList, - }) + "imgList": append([]string(nil), imgList...), + "fileList": append([]tmpFile(nil), fileList...), + } + stateMu.Unlock() + + writeJSON(w, http.StatusOK, resp) }) - r.POST("/clearAll", func(c *gin.Context) { + mux.HandleFunc("/clearAll", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + stateMu.Lock() sData = "" clearTmpFile() fileList = nil imgList = nil - c.HTML(http.StatusOK, "index.html", gin.H{"data": sData, "imgList": imgList, "fileList": fileList}) + stateMu.Unlock() + renderIndex(w, tempHTML) }) - r.POST("/deleteFile", func(c *gin.Context) { - sData = "" - fileName := c.PostForm("fileName") - if fileName == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "fileName is required"}) + mux.HandleFunc("/deleteFile", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if err := r.ParseForm(); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } - // 删除文件 - filePath := filepath.Join(tmpFileDir, fileName) - err := os.Remove(filePath) + fileName, err := sanitizeFileName(r.PostFormValue("fileName")) if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file: " + err.Error()}) + writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()}) return } - // 从列表中移除 - fileUrl := "tFile/" + fileName + filePath := filepath.Join(tmpFileDir, fileName) + if err := os.Remove(filePath); err != nil { + if errors.Is(err, os.ErrNotExist) { + writeJSON(w, http.StatusNotFound, map[string]any{"error": "file not found"}) + return + } + writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "failed to delete file: " + err.Error()}) + return + } + + fileURL := "tFile/" + fileName + stateMu.Lock() if isImgSimple(fileName) { - // 从图片列表中移除 for i, img := range imgList { - if img == fileUrl { + if img == fileURL { imgList = append(imgList[:i], imgList[i+1:]...) break } } } else { - // 从文件列表中移除 for i, file := range fileList { - if file.N == fileUrl { + if file.N == fileURL { fileList = append(fileList[:i], fileList[i+1:]...) break } } } + stateMu.Unlock() - c.JSON(http.StatusOK, gin.H{"success": true, "message": "File deleted successfully"}) + writeJSON(w, http.StatusOK, map[string]any{"success": true, "message": "File deleted successfully"}) }) - r.POST("/upload", func(c *gin.Context) { - // Single file - file, err := c.FormFile("file") + mux.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + if err := r.ParseMultipartForm(20 << 20); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + file, fileHeader, err := r.FormFile("file") if err != nil { - fmt.Println(err) - c.String(http.StatusBadRequest, err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return } + defer file.Close() - saveName := file.Filename + saveName, err := sanitizeFileName(fileHeader.Filename) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } - err = c.SaveUploadedFile(file, tmpFileDir+"/"+saveName) + dst, err := os.OpenFile(filepath.Join(tmpFileDir, saveName), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) if err != nil { - fmt.Println(err) - c.String(http.StatusBadRequest, err.Error()) + http.Error(w, err.Error(), http.StatusBadRequest) + return } - fileUrl := "tFile/" + saveName + defer dst.Close() - v, e := c.GetPostForm("isImg") - if !e { - v = "0" + if _, err := io.Copy(dst, file); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return } - if v == "1" { - imgList = append([]string{fileUrl}, imgList...) + + fileURL := "tFile/" + saveName + if r.PostFormValue("isImg") == "1" { + stateMu.Lock() + imgList = append([]string{fileURL}, imgList...) + stateMu.Unlock() } else { - fileList = append(fileList, tmpFile{T: "0", N: fileUrl, Ns: file.Filename}) + stateMu.Lock() + fileList = append(fileList, tmpFile{T: "0", N: fileURL, Ns: saveName}) + stateMu.Unlock() } - c.String(http.StatusOK, fileUrl) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(fileURL)) }) - r.Static("/tFile", "./"+tmpFileDir) - + return mux, nil } // load files in tmpFileDir @@ -159,6 +222,8 @@ func loadOldFiles() { log.Fatal(err) } + stateMu.Lock() + defer stateMu.Unlock() for _, f := range files { fileUrl := "tFile/" + f.Name() if isImgSimple(f.Name()) { @@ -186,3 +251,41 @@ func isImgSimple(name string) bool { } return false } + +func renderIndex(w http.ResponseWriter, tpl *template.Template) { + stateMu.RLock() + data := map[string]any{ + "data": sData, + "imgList": append([]string(nil), imgList...), + "fileList": append([]tmpFile(nil), fileList...), + } + stateMu.RUnlock() + + if err := tpl.ExecuteTemplate(w, "index.html", data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func writeJSON(w http.ResponseWriter, status int, payload any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(payload) +} + +func sanitizeFileName(name string) (string, error) { + trimmed := strings.TrimSpace(name) + if trimmed == "" { + return "", errors.New("fileName is required") + } + + normalized := strings.ReplaceAll(trimmed, "\\", "/") + baseName := path.Base(normalized) + if baseName == "." || baseName == "/" || baseName == "" { + return "", errors.New("invalid file name") + } + if strings.Contains(baseName, "/") || strings.Contains(baseName, string(os.PathSeparator)) { + return "", errors.New("invalid file name") + } + + return baseName, nil +} diff --git a/http_test.go b/http_test.go new file mode 100644 index 0000000..e469ef7 --- /dev/null +++ b/http_test.go @@ -0,0 +1,409 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "testing" +) + +func resetState(t *testing.T) string { + t.Helper() + + dir := t.TempDir() + tmpFileDir = dir + + stateMu.Lock() + sData = "" + imgList = nil + fileList = nil + stateMu.Unlock() + + return dir +} + +func newTestMux(t *testing.T) *http.ServeMux { + t.Helper() + mux, err := setRouter() + if err != nil { + t.Fatalf("setRouter() error = %v", err) + } + return mux +} + +func TestSanitizeFileName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want string + wantErr bool + }{ + {name: "plain name", input: "notes.txt", want: "notes.txt"}, + {name: "trim spaces", input: " photo.png ", want: "photo.png"}, + {name: "strip unix path", input: "../../etc/passwd", want: "passwd"}, + {name: "strip windows path", input: `..\..\secret.txt`, want: "secret.txt"}, + {name: "empty", input: " ", wantErr: true}, + {name: "dot", input: ".", wantErr: true}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := sanitizeFileName(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("sanitizeFileName(%q) expected error, got %q", tt.input, got) + } + return + } + if err != nil { + t.Fatalf("sanitizeFileName(%q) unexpected error: %v", tt.input, err) + } + if got != tt.want { + t.Fatalf("sanitizeFileName(%q) = %q, want %q", tt.input, got, tt.want) + } + }) + } +} + +func TestIsImgSimple(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + want bool + }{ + {name: "a.jpg", want: true}, + {name: "a.jpeg", want: true}, + {name: "a.png", want: true}, + {name: "a.webp", want: true}, + {name: "a.txt", want: false}, + {name: "a.PNG", want: false}, + {name: "noext", want: false}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := isImgSimple(tt.name); got != tt.want { + t.Fatalf("isImgSimple(%q) = %v, want %v", tt.name, got, tt.want) + } + }) + } +} + +func TestIndexPage(t *testing.T) { + resetState(t) + mux := newTestMux(t) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("GET / status = %d, want %d", rec.Code, http.StatusOK) + } + body := rec.Body.String() + if !strings.Contains(body, "simple share") { + t.Fatalf("GET / body missing page title, got: %s", body) + } +} + +func TestSubmitText(t *testing.T) { + resetState(t) + mux := newTestMux(t) + + form := url.Values{} + form.Set("sData", "hello shared text") + req := httptest.NewRequest(http.MethodPost, "/submit", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("POST /submit status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + var resp map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode response: %v", err) + } + if resp["success"] != true { + t.Fatalf("success = %v, want true", resp["success"]) + } + if resp["data"] != "hello shared text" { + t.Fatalf("data = %v, want hello shared text", resp["data"]) + } + + stateMu.RLock() + defer stateMu.RUnlock() + if sData != "hello shared text" { + t.Fatalf("sData = %q, want hello shared text", sData) + } +} + +func TestUploadAndServeFile(t *testing.T) { + dir := resetState(t) + mux := newTestMux(t) + + fileURL := uploadFile(t, mux, "hello.txt", []byte("file-content"), "0") + if fileURL != "tFile/hello.txt" { + t.Fatalf("upload url = %q, want tFile/hello.txt", fileURL) + } + + saved := filepath.Join(dir, "hello.txt") + content, err := os.ReadFile(saved) + if err != nil { + t.Fatalf("read uploaded file: %v", err) + } + if string(content) != "file-content" { + t.Fatalf("uploaded content = %q, want file-content", content) + } + + stateMu.RLock() + if len(fileList) != 1 || fileList[0].Ns != "hello.txt" { + stateMu.RUnlock() + t.Fatalf("fileList = %+v, want one hello.txt entry", fileList) + } + stateMu.RUnlock() + + req := httptest.NewRequest(http.MethodGet, "/"+fileURL, nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("GET /%s status = %d, want %d", fileURL, rec.Code, http.StatusOK) + } + if rec.Body.String() != "file-content" { + t.Fatalf("served content = %q, want file-content", rec.Body.String()) + } +} + +func TestUploadImage(t *testing.T) { + resetState(t) + mux := newTestMux(t) + + fileURL := uploadFile(t, mux, "pic.png", []byte("png-bytes"), "1") + if fileURL != "tFile/pic.png" { + t.Fatalf("upload url = %q, want tFile/pic.png", fileURL) + } + + stateMu.RLock() + defer stateMu.RUnlock() + if len(imgList) != 1 || imgList[0] != fileURL { + t.Fatalf("imgList = %+v, want [%s]", imgList, fileURL) + } + if len(fileList) != 0 { + t.Fatalf("fileList = %+v, want empty", fileList) + } +} + +func TestUploadRejectsInvalidFileName(t *testing.T) { + resetState(t) + mux := newTestMux(t) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", ".") + if err != nil { + t.Fatalf("CreateFormFile: %v", err) + } + if _, err := part.Write([]byte("x")); err != nil { + t.Fatalf("write part: %v", err) + } + _ = writer.WriteField("isImg", "0") + if err := writer.Close(); err != nil { + t.Fatalf("close writer: %v", err) + } + + req := httptest.NewRequest(http.MethodPost, "/upload", body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("POST /upload status = %d, want %d; body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) + } +} + +func TestDeleteFile(t *testing.T) { + dir := resetState(t) + mux := newTestMux(t) + _ = uploadFile(t, mux, "delete-me.txt", []byte("bye"), "0") + + form := url.Values{} + form.Set("fileName", "delete-me.txt") + req := httptest.NewRequest(http.MethodPost, "/deleteFile", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("POST /deleteFile status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + var resp map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("decode response: %v", err) + } + if resp["success"] != true { + t.Fatalf("success = %v, want true", resp["success"]) + } + + if _, err := os.Stat(filepath.Join(dir, "delete-me.txt")); !os.IsNotExist(err) { + t.Fatalf("expected file removed, stat err = %v", err) + } + + stateMu.RLock() + defer stateMu.RUnlock() + if len(fileList) != 0 { + t.Fatalf("fileList = %+v, want empty after delete", fileList) + } +} + +func TestDeleteFileMissing(t *testing.T) { + resetState(t) + mux := newTestMux(t) + + form := url.Values{} + form.Set("fileName", "missing.txt") + req := httptest.NewRequest(http.MethodPost, "/deleteFile", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusNotFound { + t.Fatalf("POST /deleteFile status = %d, want %d; body=%s", rec.Code, http.StatusNotFound, rec.Body.String()) + } +} + +func TestDeleteFileRejectsEmptyName(t *testing.T) { + resetState(t) + mux := newTestMux(t) + + form := url.Values{} + form.Set("fileName", " ") + req := httptest.NewRequest(http.MethodPost, "/deleteFile", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Fatalf("POST /deleteFile status = %d, want %d; body=%s", rec.Code, http.StatusBadRequest, rec.Body.String()) + } +} + +func TestClearAll(t *testing.T) { + dir := resetState(t) + mux := newTestMux(t) + + _ = uploadFile(t, mux, "a.txt", []byte("a"), "0") + _ = uploadFile(t, mux, "b.png", []byte("b"), "1") + + form := url.Values{} + form.Set("sData", "to-clear") + submitReq := httptest.NewRequest(http.MethodPost, "/submit", strings.NewReader(form.Encode())) + submitReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + submitRec := httptest.NewRecorder() + mux.ServeHTTP(submitRec, submitReq) + if submitRec.Code != http.StatusOK { + t.Fatalf("setup submit status = %d", submitRec.Code) + } + + req := httptest.NewRequest(http.MethodPost, "/clearAll", nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("POST /clearAll status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if len(entries) != 0 { + t.Fatalf("tmp dir entries = %d, want 0", len(entries)) + } + + stateMu.RLock() + defer stateMu.RUnlock() + if sData != "" || len(fileList) != 0 || len(imgList) != 0 { + t.Fatalf("state not cleared: sData=%q fileList=%+v imgList=%+v", sData, fileList, imgList) + } +} + +func TestLoadOldFiles(t *testing.T) { + dir := resetState(t) + if err := os.WriteFile(filepath.Join(dir, "old.txt"), []byte("old"), 0o644); err != nil { + t.Fatalf("WriteFile: %v", err) + } + if err := os.WriteFile(filepath.Join(dir, "old.png"), []byte("img"), 0o644); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + loadOldFiles() + + stateMu.RLock() + defer stateMu.RUnlock() + if len(fileList) != 1 || fileList[0].Ns != "old.txt" { + t.Fatalf("fileList = %+v, want one old.txt", fileList) + } + if len(imgList) != 1 || imgList[0] != "tFile/old.png" { + t.Fatalf("imgList = %+v, want [tFile/old.png]", imgList) + } +} + +func TestMethodNotAllowed(t *testing.T) { + resetState(t) + mux := newTestMux(t) + + endpoints := []string{"/submit", "/clearAll", "/deleteFile", "/upload"} + for _, endpoint := range endpoints { + req := httptest.NewRequest(http.MethodGet, endpoint, nil) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + if rec.Code != http.StatusMethodNotAllowed { + t.Fatalf("GET %s status = %d, want %d", endpoint, rec.Code, http.StatusMethodNotAllowed) + } + } +} + +func uploadFile(t *testing.T, mux http.Handler, filename string, content []byte, isImg string) string { + t.Helper() + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", filename) + if err != nil { + t.Fatalf("CreateFormFile: %v", err) + } + if _, err := io.Copy(part, bytes.NewReader(content)); err != nil { + t.Fatalf("copy content: %v", err) + } + if err := writer.WriteField("isImg", isImg); err != nil { + t.Fatalf("WriteField: %v", err) + } + if err := writer.Close(); err != nil { + t.Fatalf("close writer: %v", err) + } + + req := httptest.NewRequest(http.MethodPost, "/upload", body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("POST /upload status = %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String()) + } + return rec.Body.String() +}