commit b8998f6090ec93cfbfb4ffd419019264b684303f Author: Diff Date: Wed Mar 24 01:06:30 2021 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ede3eb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +ruff \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8ecfcb9 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.tilde.town/diff/ruff + +go 1.14 + +require github.com/mdp/qrterminal v1.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d84b2ef --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= +github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/main.go b/main.go new file mode 100644 index 0000000..2d11b37 --- /dev/null +++ b/main.go @@ -0,0 +1,198 @@ +// Ruff provides a pop-up web server to Retrieve/Upload Files Fast over +// LAN, inspired by WOOF (Web Offer One File) by Simon Budig. +// +// It's based on the principle that not everyone has , just about every device that can network has an HTTP client, +// making a hyper-simple HTTP server a viable option for file transfer with +// zero notice or setup as long as *somebody* has a copy of RUFF. +// +// Why create RUFF when WOOF exists? WOOF is no longer in the debian repos and +// it's easier to `go get` a tool than it is to hunt down Simon's website for +// the latest copy. +package main + +import ( + "net" + "net/url" + "net/http" + "time" + "context" + "html/template" + + "path" + "os" + "io" + + "fmt" + "flag" + "errors" + "github.com/mdp/qrterminal" +) + +// Config stores all settings for an instance of RUFF. +type Config struct { + downloads int + port int + filePath string + fileName string + hideQR bool + uploading bool +} + +func getConfig() Config { + conf := Config{ + downloads: 1, + port: 8008, + hideQR: false, + uploading: false, + } + + flag.IntVar(&conf.downloads, "count", conf.downloads, "number of downloads before exiting. set to -1 for unlimited downloads.") + flag.IntVar(&conf.port, "port", conf.port, "port to serve file on.") + flag.BoolVar(&conf.hideQR, "hide-qr", conf.hideQR, "hide the QR code.") + flag.BoolVar(&conf.uploading, "upload", false, "upload files instead of downloading") + + flag.IntVar(&conf.downloads, "c", conf.downloads, "number of downloads before exiting. set to -1 for unlimited downloads. (shorthand)") + flag.IntVar(&conf.port, "p", conf.port, "port to serve file on. (shorthand)") + flag.BoolVar(&conf.hideQR, "q", conf.hideQR, "hide the QR code. (shorthand)") + flag.BoolVar(&conf.uploading, "u", false, "upload files instead of downloading (shorthand)") + + flag.Parse() + conf.filePath = flag.Arg(0) + conf.fileName = path.Base(conf.filePath) + + return conf +} + +func getIP() (string, error) { + conn, err := net.Dial("udp", "8.8.8.8:80") + if err != nil { + return "", err + } + + localAddr, ok := conn.LocalAddr().(*net.UDPAddr) + if !ok { + return "", err + } + + return localAddr.IP.String(), nil +} + +func main() { + conf := getConfig() + + server := &http.Server{ + Addr: fmt.Sprintf(":%v", conf.port), + ReadTimeout: 10*time.Second, + WriteTimeout: 10*time.Second, + } + + if conf.uploading { + setupUpload(server, &conf) + } else { + setupDownload(server, &conf) + } + + ip, err := getIP() + if err != nil { + fmt.Println(err) + return + } + + url := fmt.Sprintf("http://%s:%v/%s", ip, conf.port, conf.fileName) + if !conf.hideQR { + qrterminal.GenerateHalfBlock(url, qrterminal.M, os.Stdout) + } + fmt.Println(url) + + err = server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Println(err) + return + } +} + +func setupDownload(server *http.Server, conf *Config) { + downloads := conf.downloads + http.Handle("/", http.RedirectHandler("/"+conf.fileName, http.StatusFound)) // 302 redirect + http.HandleFunc("/"+conf.fileName, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Disposition", "attachment; filename=\""+url.PathEscape(conf.fileName)+"\"") + http.ServeFile(w, r, conf.filePath) + + downloads-- + if downloads == 0 { + server.Shutdown(context.Background()) + } + }) +} + +var baseHeader = ` + + + {{.}} + + + ` + +var baseFooter = ` +` + +var uploadTemplate = `{{template "BaseHeader" "RUFF Upload Form"}} +
+ + + +
+{{template "BaseFooter"}}` + +var errorTemplate = `{{template "BaseHeader" "UploadError"}} +

{{.}}

+

Go back

+{{template "BaseFooter"}}` + +func setupUpload(server *http.Server, conf *Config) { + tpl := template.Must(template.New("BaseHeader").Parse(baseHeader)) + template.Must(tpl.New("BaseFooter").Parse(baseFooter)) + template.Must(tpl.New("UploadForm").Parse(uploadTemplate)) + template.Must(tpl.New("UploadError").Parse(errorTemplate)) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Upload form + if r.Method != http.MethodPost { + err := tpl.ExecuteTemplate(w, "UploadForm", conf) + if err != nil { + panic(err) + } + return + } + + // Handle upload + r.ParseMultipartForm(20 << 20) // Buffer a maximum of 20MB in memory. + + inFile, header, err := r.FormFile("file") + if err != nil { + tpl.ExecuteTemplate(w, "UploadError", err) + return + } + defer inFile.Close() + + outFile, err := os.Create(header.Filename) + if err != nil { + tpl.ExecuteTemplate(w, "UploadError", err) + return + } + defer outFile.Close() + + _, err = io.Copy(outFile, inFile) + if err != nil { + tpl.ExecuteTemplate(w, "UploadError", err) + return + } + }) +} \ No newline at end of file