diff --git a/Makefile b/Makefile index 67dd283..32e17b7 100644 --- a/Makefile +++ b/Makefile @@ -31,39 +31,39 @@ release/$(APP)_$(VERSION)_osx_x86_64.tar.gz: binaries/osx_x86_64/$(APP) tar cfz release/$(APP)_$(VERSION)_osx_x86_64.tar.gz -C binaries/osx_x86_64 $(APP) binaries/osx_x86_64/$(APP): $(GOFILES) - GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/osx_x86_64/$(APP) . + GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/osx_x86_64/$(APP) ./cmd/... release/$(APP)_$(VERSION)_windows_x86_64.zip: binaries/windows_x86_64/$(APP).exe mkdir -p release cd ./binaries/windows_x86_64 && zip -r -D ../../release/$(APP)_$(VERSION)_windows_x86_64.zip $(APP).exe binaries/windows_x86_64/$(APP).exe: $(GOFILES) - GOOS=windows GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/windows_x86_64/$(APP).exe . + GOOS=windows GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/windows_x86_64/$(APP).exe ./cmd/... release/$(APP)_$(VERSION)_linux_x86_64.tar.gz: binaries/linux_x86_64/$(APP) mkdir -p release tar cfz release/$(APP)_$(VERSION)_linux_x86_64.tar.gz -C binaries/linux_x86_64 $(APP) binaries/linux_x86_64/$(APP): $(GOFILES) - GOOS=linux GOARCH=amd64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/linux_x86_64/$(APP) . + GOOS=linux GOARCH=amd64 go build -ldflags "-X main.Version=$(VERSION)" -o binaries/linux_x86_64/$(APP) ./cmd/... release/$(APP)_$(VERSION)_windows_x86_32.zip: binaries/windows_x86_32/$(APP).exe mkdir -p release cd ./binaries/windows_x86_32 && zip -r -D ../../release/$(APP)_$(VERSION)_windows_x86_32.zip $(APP).exe binaries/windows_x86_32/$(APP).exe: $(GOFILES) - GOOS=windows GOARCH=386 go build -ldflags "-X main.version=$(VERSION)" -o binaries/windows_x86_32/$(APP).exe . + GOOS=windows GOARCH=386 go build -ldflags "-X main.version=$(VERSION)" -o binaries/windows_x86_32/$(APP).exe ./cmd/... release/$(APP)_$(VERSION)_linux_x86_32.tar.gz: binaries/linux_x86_32/$(APP) mkdir -p release tar cfz release/$(APP)_$(VERSION)_linux_x86_32.tar.gz -C binaries/linux_x86_32 $(APP) binaries/linux_x86_32/$(APP): $(GOFILES) - GOOS=linux GOARCH=386 go build -ldflags "-X main.version=$(VERSION)" -o binaries/linux_x86_32/$(APP) . + GOOS=linux GOARCH=386 go build -ldflags "-X main.version=$(VERSION)" -o binaries/linux_x86_32/$(APP) ./cmd/... release/$(APP)_$(VERSION)_linux_arm64.tar.gz: binaries/linux_arm64/$(APP) mkdir -p release tar cfz release/$(APP)_$(VERSION)_linux_arm64.tar.gz -C binaries/linux_arm64 $(APP) binaries/linux_arm64/$(APP): $(GOFILES) - GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/linux_arm64/$(APP) . + GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$(VERSION)" -o binaries/linux_arm64/$(APP) ./cmd/... diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..b787652 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "log" + "net" + "os" + "strconv" + + "github.com/sgreben/httpfileserver" +) + +const ( + addrEnvVarName = "ADDR" + allowUploadsEnvVarName = "UPLOADS" + defaultAddr = ":8080" + portEnvVarName = "PORT" + quietEnvVarName = "QUIET" + sslCertificateEnvVarName = "SSL_CERTIFICATE" + sslKeyEnvVarName = "SSL_KEY" +) + +var Version = ":unknown:" + +var addrFlag string +var portFlag int + +func addr(cfg httpfileserver.Config) (string, error) { + portSet := portFlag != 0 + addrSet := addrFlag != "" + switch { + case portSet && addrSet: + a, err := net.ResolveTCPAddr("tcp", addrFlag) + if err != nil { + return "", err + } + a.Port = portFlag + return a.String(), nil + case !portSet && addrSet: + a, err := net.ResolveTCPAddr("tcp", addrFlag) + if err != nil { + return "", err + } + return a.String(), nil + case portSet && !addrSet: + return fmt.Sprintf(":%d", portFlag), nil + case !portSet && !addrSet: + fallthrough + default: + return defaultAddr, nil + } +} + +func configureRuntime(cfg httpfileserver.Config) httpfileserver.Config { + var quietFlag bool + + log.SetFlags(log.LUTC | log.Ldate | log.Ltime) + log.SetOutput(os.Stderr) + if addrFlag == "" { + addrFlag = defaultAddr + } + flag.StringVar(&addrFlag, "addr", addrFlag, fmt.Sprintf("address to listen on (environment variable %q)", addrEnvVarName)) + flag.StringVar(&addrFlag, "a", addrFlag, "(alias for -addr)") + flag.IntVar(&portFlag, "port", portFlag, fmt.Sprintf("port to listen on (overrides -addr port) (environment variable %q)", portEnvVarName)) + flag.IntVar(&portFlag, "p", portFlag, "(alias for -port)") + flag.BoolVar(&quietFlag, "quiet", quietFlag, fmt.Sprintf("disable all log output (environment variable %q)", quietEnvVarName)) + flag.BoolVar(&quietFlag, "q", quietFlag, "(alias for -quiet)") + flag.BoolVar(&cfg.AllowUploadsFlag, "uploads", cfg.AllowUploadsFlag, fmt.Sprintf("allow uploads (environment variable %q)", allowUploadsEnvVarName)) + flag.BoolVar(&cfg.AllowUploadsFlag, "u", cfg.AllowUploadsFlag, "(alias for -uploads)") + flag.Var(&cfg.Routes, "route", cfg.Routes.Help()) + flag.Var(&cfg.Routes, "r", "(alias for -route)") + flag.StringVar(&cfg.SslCertificate, "ssl-cert", cfg.SslCertificate, fmt.Sprintf("path to SSL server certificate (environment variable %q)", sslCertificateEnvVarName)) + flag.StringVar(&cfg.SslKey, "ssl-key", cfg.SslKey, fmt.Sprintf("path to SSL private key (environment variable %q)", sslKeyEnvVarName)) + flag.Parse() + if quietFlag { + log.SetOutput(ioutil.Discard) + } + for i := 0; i < flag.NArg(); i++ { + arg := flag.Arg(i) + err := cfg.Routes.Set(arg) + if err != nil { + log.Fatalf("%q: %v", arg, err) + } + } + + return cfg +} + +func newConfig() httpfileserver.Config { + portFlag64, _ := strconv.ParseInt(os.Getenv(portEnvVarName), 10, 64) + portFlag = int(portFlag64) + + cfg := httpfileserver.NewConfig() + cfg.AllowUploadsFlag = os.Getenv(allowUploadsEnvVarName) == "true" + cfg.SslCertificate = os.Getenv(sslCertificateEnvVarName) + cfg.SslKey = os.Getenv(sslKeyEnvVarName) + cfg.RootRoute = "/" + + return cfg +} + +func main() { + cfg := configureRuntime(newConfig()) + log.Printf("httpfileserver v%s", Version) + + addr, err := addr(cfg) + if err != nil { + log.Fatalf("address/port: %v", err) + } + + cfg.Addr = addr + err = httpfileserver.Serve(context.Background(), cfg) + if err != nil { + log.Fatalf("start server: %v", err) + } +} diff --git a/go.mod b/go.mod index d178f18..c04de6a 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/sgreben/http-file-server +module github.com/sgreben/httpfileserver go 1.16 diff --git a/httpfileserver.go b/httpfileserver.go new file mode 100644 index 0000000..da99cd7 --- /dev/null +++ b/httpfileserver.go @@ -0,0 +1,72 @@ +package httpfileserver + +import ( + "context" + "log" + "net/http" + "os" + "path/filepath" + + "github.com/sgreben/httpfileserver/internal/routes" +) + +type Config struct { + Addr string + AllowUploadsFlag bool + RootRoute string + SslCertificate string + SslKey string + Routes routes.Routes +} + +func NewConfig() Config { + return Config{ + Addr: ":8080", + AllowUploadsFlag: false, + RootRoute: "/", + SslCertificate: "", + SslKey: "", + } +} + +func Serve(ctx context.Context, cfg Config) error { + mux := http.DefaultServeMux + handlers := make(map[string]http.Handler) + paths := make(map[string]string) + + if len(cfg.Routes.Values) == 0 { + _ = cfg.Routes.Set(".") + } + + for _, route := range cfg.Routes.Values { + handlers[route.Route] = &fileHandler{ + route: route.Route, + path: route.Path, + allowUpload: cfg.AllowUploadsFlag, + } + paths[route.Route] = route.Path + } + + for route, path := range paths { + mux.Handle(route, handlers[route]) + log.Printf("serving local path %q on %q", path, route) + } + + _, rootRouteTaken := handlers[cfg.RootRoute] + if !rootRouteTaken { + route := cfg.Routes.Values[0].Route + mux.Handle(cfg.RootRoute, http.RedirectHandler(route, http.StatusTemporaryRedirect)) + log.Printf("redirecting to %q from %q", route, cfg.RootRoute) + } + + binaryPath, _ := os.Executable() + if binaryPath == "" { + binaryPath = "server" + } + if cfg.SslCertificate != "" && cfg.SslKey != "" { + log.Printf("%s (HTTPS) listening on %q", filepath.Base(binaryPath), cfg.Addr) + return http.ListenAndServeTLS(cfg.Addr, cfg.SslCertificate, cfg.SslKey, mux) + } + log.Printf("%s listening on %q", filepath.Base(binaryPath), cfg.Addr) + return http.ListenAndServe(cfg.Addr, mux) +} diff --git a/routes.go b/internal/routes/routes.go similarity index 88% rename from routes.go rename to internal/routes/routes.go index 01ca7e7..aed16d6 100644 --- a/routes.go +++ b/internal/routes/routes.go @@ -1,4 +1,4 @@ -package main +package routes import ( "fmt" @@ -6,7 +6,7 @@ import ( "strings" ) -type routes struct { +type Routes struct { Separator string Values []struct { @@ -16,7 +16,7 @@ type routes struct { Texts []string } -func (fv *routes) help() string { +func (fv *Routes) Help() string { separator := "=" if fv.Separator != "" { separator = fv.Separator @@ -25,7 +25,7 @@ func (fv *routes) help() string { } // Set is flag.Value.Set -func (fv *routes) Set(v string) error { +func (fv *Routes) Set(v string) error { separator := "=" if fv.Separator != "" { separator = fv.Separator @@ -65,6 +65,6 @@ func (fv *routes) Set(v string) error { return nil } -func (fv *routes) String() string { +func (fv *Routes) String() string { return strings.Join(fv.Texts, ", ") } diff --git a/tar.gz.go b/internal/targz/tar.gz.go similarity index 94% rename from tar.gz.go rename to internal/targz/tar.gz.go index a531213..c741238 100644 --- a/tar.gz.go +++ b/internal/targz/tar.gz.go @@ -1,4 +1,4 @@ -package main +package targz import ( "archive/tar" @@ -9,7 +9,7 @@ import ( "path/filepath" ) -func tarGz(w io.Writer, path string) error { +func TarGz(w io.Writer, path string) error { basePath := path addFile := func(w *tar.Writer, path string, stat os.FileInfo) error { if stat.IsDir() { diff --git a/zip.go b/internal/zip/zip.go similarity index 93% rename from zip.go rename to internal/zip/zip.go index b4a1be1..00ca606 100644 --- a/zip.go +++ b/internal/zip/zip.go @@ -1,4 +1,4 @@ -package main +package zip import ( zipper "archive/zip" @@ -8,7 +8,7 @@ import ( "path/filepath" ) -func zip(w io.Writer, path string) error { +func Zip(w io.Writer, path string) error { basePath := path addFile := func(w *zipper.Writer, path string, stat os.FileInfo) error { if stat.IsDir() { diff --git a/main.go b/main.go deleted file mode 100644 index 87cfef0..0000000 --- a/main.go +++ /dev/null @@ -1,145 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "io/ioutil" - "log" - "net" - "net/http" - "os" - "path/filepath" - "strconv" -) - -const ( - addrEnvVarName = "ADDR" - allowUploadsEnvVarName = "UPLOADS" - defaultAddr = ":8080" - portEnvVarName = "PORT" - quietEnvVarName = "QUIET" - rootRoute = "/" - sslCertificateEnvVarName = "SSL_CERTIFICATE" - sslKeyEnvVarName = "SSL_KEY" -) - -var ( - addrFlag = os.Getenv(addrEnvVarName) - allowUploadsFlag = os.Getenv(allowUploadsEnvVarName) == "true" - portFlag64, _ = strconv.ParseInt(os.Getenv(portEnvVarName), 10, 64) - portFlag = int(portFlag64) - quietFlag = os.Getenv(quietEnvVarName) == "true" - routesFlag routes - sslCertificate = os.Getenv(sslCertificateEnvVarName) - sslKey = os.Getenv(sslKeyEnvVarName) -) - -func init() { - log.SetFlags(log.LUTC | log.Ldate | log.Ltime) - log.SetOutput(os.Stderr) - if addrFlag == "" { - addrFlag = defaultAddr - } - flag.StringVar(&addrFlag, "addr", addrFlag, fmt.Sprintf("address to listen on (environment variable %q)", addrEnvVarName)) - flag.StringVar(&addrFlag, "a", addrFlag, "(alias for -addr)") - flag.IntVar(&portFlag, "port", portFlag, fmt.Sprintf("port to listen on (overrides -addr port) (environment variable %q)", portEnvVarName)) - flag.IntVar(&portFlag, "p", portFlag, "(alias for -port)") - flag.BoolVar(&quietFlag, "quiet", quietFlag, fmt.Sprintf("disable all log output (environment variable %q)", quietEnvVarName)) - flag.BoolVar(&quietFlag, "q", quietFlag, "(alias for -quiet)") - flag.BoolVar(&allowUploadsFlag, "uploads", allowUploadsFlag, fmt.Sprintf("allow uploads (environment variable %q)", allowUploadsEnvVarName)) - flag.BoolVar(&allowUploadsFlag, "u", allowUploadsFlag, "(alias for -uploads)") - flag.Var(&routesFlag, "route", routesFlag.help()) - flag.Var(&routesFlag, "r", "(alias for -route)") - flag.StringVar(&sslCertificate, "ssl-cert", sslCertificate, fmt.Sprintf("path to SSL server certificate (environment variable %q)", sslCertificateEnvVarName)) - flag.StringVar(&sslKey, "ssl-key", sslKey, fmt.Sprintf("path to SSL private key (environment variable %q)", sslKeyEnvVarName)) - flag.Parse() - if quietFlag { - log.SetOutput(ioutil.Discard) - } - for i := 0; i < flag.NArg(); i++ { - arg := flag.Arg(i) - err := routesFlag.Set(arg) - if err != nil { - log.Fatalf("%q: %v", arg, err) - } - } -} - -func main() { - addr, err := addr() - if err != nil { - log.Fatalf("address/port: %v", err) - } - err = server(addr, routesFlag) - if err != nil { - log.Fatalf("start server: %v", err) - } -} - -func server(addr string, routes routes) error { - mux := http.DefaultServeMux - handlers := make(map[string]http.Handler) - paths := make(map[string]string) - - if len(routes.Values) == 0 { - _ = routes.Set(".") - } - - for _, route := range routes.Values { - handlers[route.Route] = &fileHandler{ - route: route.Route, - path: route.Path, - allowUpload: allowUploadsFlag, - } - paths[route.Route] = route.Path - } - - for route, path := range paths { - mux.Handle(route, handlers[route]) - log.Printf("serving local path %q on %q", path, route) - } - - _, rootRouteTaken := handlers[rootRoute] - if !rootRouteTaken { - route := routes.Values[0].Route - mux.Handle(rootRoute, http.RedirectHandler(route, http.StatusTemporaryRedirect)) - log.Printf("redirecting to %q from %q", route, rootRoute) - } - - binaryPath, _ := os.Executable() - if binaryPath == "" { - binaryPath = "server" - } - if sslCertificate != "" && sslKey != "" { - log.Printf("%s (HTTPS) listening on %q", filepath.Base(binaryPath), addr) - return http.ListenAndServeTLS(addr, sslCertificate, sslKey, mux) - } - log.Printf("%s listening on %q", filepath.Base(binaryPath), addr) - return http.ListenAndServe(addr, mux) -} - -func addr() (string, error) { - portSet := portFlag != 0 - addrSet := addrFlag != "" - switch { - case portSet && addrSet: - a, err := net.ResolveTCPAddr("tcp", addrFlag) - if err != nil { - return "", err - } - a.Port = portFlag - return a.String(), nil - case !portSet && addrSet: - a, err := net.ResolveTCPAddr("tcp", addrFlag) - if err != nil { - return "", err - } - return a.String(), nil - case portSet && !addrSet: - return fmt.Sprintf(":%d", portFlag), nil - case !portSet && !addrSet: - fallthrough - default: - return defaultAddr, nil - } -} diff --git a/server.go b/server.go index c3c3823..c793a7c 100644 --- a/server.go +++ b/server.go @@ -1,4 +1,4 @@ -package main +package httpfileserver import ( "fmt" @@ -13,6 +13,9 @@ import ( "path/filepath" "sort" "strings" + + "github.com/sgreben/httpfileserver/internal/targz" + "github.com/sgreben/httpfileserver/internal/zip" ) const ( @@ -29,6 +32,7 @@ const ( const directoryListingTemplateText = ` +