From d4852ee6dd752b438f3d9c9c8b2e90d02033f9d4 Mon Sep 17 00:00:00 2001 From: "Alexander \"Arav\" Andreev" Date: Sun, 6 Feb 2022 02:22:23 +0400 Subject: [PATCH] Initial commit. --- LICENSE | 7 ++ Makefile | 46 ++++++++++ cmd/dwelling-upload-clear/main.go | 41 +++++++++ cmd/dwelling-upload/main.go | 35 ++++++++ configs/config.yaml | 7 ++ go.mod | 9 ++ init/systemd/dwelling-upload-clear.service | 9 ++ init/systemd/dwelling-upload-clear.timer | 10 +++ init/systemd/dwelling-upload.service | 11 +++ internal/configuration/configuration.go | 43 ++++++++++ pkg/server/http.go | 97 ++++++++++++++++++++++ 11 files changed, 315 insertions(+) create mode 100644 LICENSE create mode 100755 Makefile create mode 100644 cmd/dwelling-upload-clear/main.go create mode 100644 cmd/dwelling-upload/main.go create mode 100644 configs/config.yaml create mode 100644 go.mod create mode 100755 init/systemd/dwelling-upload-clear.service create mode 100755 init/systemd/dwelling-upload-clear.timer create mode 100755 init/systemd/dwelling-upload.service create mode 100644 internal/configuration/configuration.go create mode 100644 pkg/server/http.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07970f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2021,2022 Alexander "Arav" Andreev + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice, this permission notice and the word "NIGGER" shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..e8b8080 --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +TARGET=dwelling-upload + +SYSCTL=${shell which systemctl} +SYSDDIR_=${shell pkg-config systemd --variable=systemdsystemunitdir} +SYSDDIR=${SYSDDIR_:/%=%} +DESTDIR=/ + +LDFLAGS=-ldflags "-s -w" + +SOURCES := ${wildcard *.go} + +all: ${TARGET} + +.PHONY: ${TARGET} + +${TARGET}: + go build -o bin/$@ ${LDFLAGS} cmd/$@/main.go + go build -o bin/$@-clear ${LDFLAGS} cmd/$@-clear/main.go + +run: + bin/${TARGET} -config configs/config.yaml + +install: + install -Dm 0755 ${TARGET} ${DESTDIR}usr/bin/${TARGET} + install -Dm 0755 ${TARGET}-clean ${DESTDIR}usr/bin/${TARGET}-clean + install -Dm 0644 LICENSE ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE + + install -Dm 0644 init/systemd/${TARGET}.service ${DESTDIR}${SYSDDIR}/${TARGET}.service + install -Dm 0644 init/systemd/${TARGET}-clear.timer ${DESTDIR}${SYSDDIR}/${TARGET}-clear.timer + install -Dm 0644 init/systemd/${TARGET}-clear.service ${DESTDIR}${SYSDDIR}/${TARGET}-clear.service + +uninstall: + rm ${DESTDIR}usr/bin/${TARGET} + rm ${DESTDIR}usr/bin/${TARGET}-clear + rm ${DESTDIR}usr/share/licenses/${TARGET}/LICENSE + + ${SYSCTL} stop ${TARGET}.service + ${SYSCTL} stop ${TARGET}-clear.timer + ${SYSCTL} disable ${TARGET}.service + ${SYSCTL} disable ${TARGET}.timer + rm ${DESTDIR}${SYSDDIR}/${TARGET}.service + rm ${DESTDIR}${SYSDDIR}/${TARGET}-clear.timer + rm ${DESTDIR}${SYSDDIR}/${TARGET}-clear.service + +clean: + go clean diff --git a/cmd/dwelling-upload-clear/main.go b/cmd/dwelling-upload-clear/main.go new file mode 100644 index 0000000..f3298d8 --- /dev/null +++ b/cmd/dwelling-upload-clear/main.go @@ -0,0 +1,41 @@ +package main + +import ( + "dwelling-upload/internal/configuration" + "flag" + "io/ioutil" + "log" + "os" + "path" + "time" +) + +var configPath string = *flag.String("conf", "config.yaml", "path to configuration file") + +func main() { + config, err := configuration.LoadConfiguration(configPath) + if err != nil { + log.Fatalln(err) + } + _ = config + + uploads_dir, err := ioutil.ReadDir(config.Uploads.Directory) + if err != nil { + log.Fatalf("failed to open directory %s: %s\n", config.Uploads.Directory, err) + } + + var deleted_count int64 = 0 + var deteted_size int64 = 0 + + for _, entry := range uploads_dir { + if time.Duration(entry.ModTime().UTC().Sub(time.Now().UTC()).Hours()) >= 24*time.Hour { + if err := os.Remove(path.Join(config.Uploads.Directory, entry.Name())); err != nil { + log.Fatalln("failed to remove file ", entry.Name(), ": ", err) + } + deteted_size += entry.Size() + deleted_count++ + } + } + + log.Println(deleted_count, " file(s) in total of ", deteted_size/1024/1024, " MiB was removed during this run.") +} diff --git a/cmd/dwelling-upload/main.go b/cmd/dwelling-upload/main.go new file mode 100644 index 0000000..fea2de5 --- /dev/null +++ b/cmd/dwelling-upload/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "dwelling-upload/internal/configuration" + "dwelling-upload/pkg/server" + "flag" + "log" + "os" + "os/signal" + "syscall" +) + +var configPath string = *flag.String("conf", "config.yaml", "path to configuration file") + +func main() { + config, err := configuration.LoadConfiguration(configPath) + if err != nil { + log.Fatalln(err) + } + + srv := server.NewHttpServer() + + if err := srv.Start(config.SplitNetworkAddress()); err != nil { + log.Fatalln(err) + } + + doneSignal := make(chan os.Signal, 1) + signal.Notify(doneSignal, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + <-doneSignal + + if err := srv.Stop(); err != nil { + log.Fatalln(err) + } +} diff --git a/configs/config.yaml b/configs/config.yaml new file mode 100644 index 0000000..4adaf7e --- /dev/null +++ b/configs/config.yaml @@ -0,0 +1,7 @@ +listen_on: "tcp :29101" +uploads: + directory: "/srv/uploads" + limits: + file_size: "128M" + keep_for_hours: 48 + storage: "200G" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bc7b8cb --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module dwelling-upload + +go 1.17 + +require ( + github.com/julienschmidt/httprouter v1.3.0 + github.com/pkg/errors v0.9.1 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b +) diff --git a/init/systemd/dwelling-upload-clear.service b/init/systemd/dwelling-upload-clear.service new file mode 100755 index 0000000..ad03277 --- /dev/null +++ b/init/systemd/dwelling-upload-clear.service @@ -0,0 +1,9 @@ +[Unit] +Description=dwelling-upload-clear + +[Service] +Type=oneshot +ExecStart=/usr/bin/dwelling-upload-clear -config /etc/dwelling-upload/config.yaml + +[Install] +WantedBy=multi-user.target diff --git a/init/systemd/dwelling-upload-clear.timer b/init/systemd/dwelling-upload-clear.timer new file mode 100755 index 0000000..75a69e2 --- /dev/null +++ b/init/systemd/dwelling-upload-clear.timer @@ -0,0 +1,10 @@ +[Unit] +Description=dwelling-upload-clear + +[Timer] +OnCalendar=hourly +AccuracySec=1h +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/init/systemd/dwelling-upload.service b/init/systemd/dwelling-upload.service new file mode 100755 index 0000000..45fd9dd --- /dev/null +++ b/init/systemd/dwelling-upload.service @@ -0,0 +1,11 @@ +[Unit] +Description=dwelling-upload +After=network.target + +[Service] +Type=simple +Restart=on-failure +ExecStart=/usr/bin/dwelling-upload -config /etc/dwelling-upload/config.yaml + +[Install] +WantedBy=multi-user.target diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go new file mode 100644 index 0000000..d910748 --- /dev/null +++ b/internal/configuration/configuration.go @@ -0,0 +1,43 @@ +package configuration + +import ( + "os" + "strings" + + "github.com/pkg/errors" + "gopkg.in/yaml.v3" +) + +// Configuration holds a list of process names to be tracked and a listen address. +type Configuration struct { + ListenOn string `yaml:"listen_on"` + Uploads struct { + Directory string `yaml:"directory"` + Limits struct { + FileSize string `yaml:"file_size"` + KeepForHours int `yaml:"keep_for_hours"` + Storage string `yaml:"storage"` + } `yaml:"limits"` + } `yaml:"uploads"` +} + +func LoadConfiguration(path string) (*Configuration, error) { + config_file, err := os.Open(path) + if err != nil { + return nil, errors.Wrap(err, "failed to open configuration file") + } + + config := &Configuration{} + + if err := yaml.NewDecoder(config_file).Decode(config); err != nil { + return nil, errors.Wrap(err, "failed to decode configuration file") + } + + return config, nil +} + +func (c *Configuration) SplitNetworkAddress() (n string, a string) { + s := strings.Split(c.ListenOn, " ") + n, a = s[0], s[1] + return n, a +} diff --git a/pkg/server/http.go b/pkg/server/http.go new file mode 100644 index 0000000..3ed56a7 --- /dev/null +++ b/pkg/server/http.go @@ -0,0 +1,97 @@ +package server + +import ( + "context" + "crypto/subtle" + "encoding/json" + "log" + "net" + "net/http" + "time" + + "github.com/julienschmidt/httprouter" +) + +type HttpServer struct { + server *http.Server + router *httprouter.Router +} + +func NewHttpServer() *HttpServer { + r := httprouter.New() + return &HttpServer{ + server: &http.Server{ + ReadTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, + Handler: r, + }, + router: r, + } +} + +func (s *HttpServer) GET(path string, handler http.HandlerFunc) { + s.router.Handler(http.MethodGet, path, handler) +} + +func (s *HttpServer) POST(path string, handler http.HandlerFunc) { + s.router.Handler(http.MethodPost, path, handler) +} + +func (s *HttpServer) PATCH(path string, handler http.HandlerFunc) { + s.router.Handler(http.MethodPatch, path, handler) +} + +func (s *HttpServer) PUT(path string, handler http.HandlerFunc) { + s.router.Handler(http.MethodPut, path, handler) +} + +func (s *HttpServer) DELETE(path string, handler http.HandlerFunc) { + s.router.Handler(http.MethodDelete, path, handler) +} + +// GetURLParams wrapper around underlying router for getting URL parameters. +func GetURLParams(r *http.Request) httprouter.Params { + return httprouter.ParamsFromContext(r.Context()) +} + +func (s *HttpServer) Start(network, address string) error { + listener, err := net.Listen(network, address) + if err != nil { + return err + } + + go func() { + if err = s.server.Serve(listener); err != nil && err != http.ErrServerClosed { + log.Fatalln(err) + } + }() + + return nil +} + +func (s *HttpServer) Stop() error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := s.server.Shutdown(ctx); err != nil { + return err + } + + return nil +} + +// AuthWithToken middleware that authenticates user by token (that is effectively just a password) +// supplied in an 'Authentication-Token' HTTP header. +func AuthWithToken(handler http.HandlerFunc, token string) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authentication-Token")), []byte(token)) == 1 { + handler(rw, r) + } else { + rw.WriteHeader(http.StatusUnauthorized) + rw.Header().Add("Content-Type", "application/json") + response := make(map[string]string) + response["message"] = "Unauthorized" + json.NewEncoder(rw).Encode(response) + } + } +}