Initial commit
commit
b8998f6090
|
@ -0,0 +1 @@
|
||||||
|
ruff
|
|
@ -0,0 +1,5 @@
|
||||||
|
module git.tilde.town/diff/ruff
|
||||||
|
|
||||||
|
go 1.14
|
||||||
|
|
||||||
|
require github.com/mdp/qrterminal v1.0.1
|
|
@ -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=
|
|
@ -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 <insert neat file transfer
|
||||||
|
// utility here>, 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 = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 18pt auto;
|
||||||
|
font-size: 16pt;
|
||||||
|
color: #212121;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>`
|
||||||
|
|
||||||
|
var baseFooter = `</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
var uploadTemplate = `{{template "BaseHeader" "RUFF Upload Form"}}
|
||||||
|
<form enctype="multipart/form-data" action="/" method="post">
|
||||||
|
<label for="file">Select a file for upload:</label>
|
||||||
|
<input type="file" name="file">
|
||||||
|
<input type="submit" value="Upload"{{if .multiple}} multiple{{end}}>
|
||||||
|
</form>
|
||||||
|
{{template "BaseFooter"}}`
|
||||||
|
|
||||||
|
var errorTemplate = `{{template "BaseHeader" "UploadError"}}
|
||||||
|
<p>{{.}}</p>
|
||||||
|
<p><a href="/">Go back</a></p>
|
||||||
|
{{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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue