diff --git a/PKGBUILD b/PKGBUILD
new file mode 100644
index 0000000..ce3d7ed
--- /dev/null
+++ b/PKGBUILD
@@ -0,0 +1,32 @@
+pkgname=simplefileshare
+pkgver=0.1.0
+pkgrel=0
+pkgdesc="Simple file share"
+arch=('x86_64' 'aarch64')
+license=('GPL')
+url="https://github.com/nxshock/$pkgname"
+makedepends=('go' 'git')
+options=('!strip')
+backup=("etc/$pkgname.toml")
+source=("git+https://github.com/nxshock/$pkgname.git")
+sha256sums=('SKIP')
+
+build() {
+ cd "$srcdir/$pkgname"
+
+ export CGO_CPPFLAGS="${CPPFLAGS}"
+ export CGO_CFLAGS="${CFLAGS}"
+ export CGO_CXXFLAGS="${CXXFLAGS}"
+ export CGO_LDFLAGS="${LDFLAGS}"
+
+ go build -o $pkgname -buildmode=pie -trimpath -ldflags="-linkmode=external -s -w"
+}
+
+package() {
+ cd "$srcdir/$pkgname"
+
+ install -Dm755 "$pkgname" "$pkgdir/usr/bin/$pkgname"
+ install -Dm644 "$pkgname.conf" "$pkgdir/etc/$pkgname.conf"
+ install -Dm644 "$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service"
+ install -Dm644 "$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
+}
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..c48a027
--- /dev/null
+++ b/config.go
@@ -0,0 +1,47 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "github.com/BurntSushi/toml"
+ log "github.com/sirupsen/logrus"
+)
+
+var config *Config
+
+type Config struct {
+ ListenAddress string
+ StoragePath string
+ RemoveFilePeriod uint // hours
+ LogLevel log.Level
+}
+
+func initConfig() error {
+ log.Debugln("Сonfig initialization started.")
+ defer log.Debugln("Сonfig initialization finished.")
+
+ var configFilePath string
+
+ if len(os.Args) < 2 {
+ configFilePath = defaultConfigFilePath
+ } else {
+ configFilePath = os.Args[1]
+ }
+
+ _, err := toml.DecodeFile(configFilePath, &config)
+ if err != nil {
+ return err
+ }
+
+ stat, err := os.Stat(config.StoragePath)
+ if err != nil {
+ return fmt.Errorf("os.Stat(config.StoragePath): %v", err)
+ }
+ if !stat.IsDir() {
+ return errors.New("StoragePath is not a dir")
+ }
+
+ return nil
+}
diff --git a/consts_linux.go b/consts_linux.go
new file mode 100644
index 0000000..352839e
--- /dev/null
+++ b/consts_linux.go
@@ -0,0 +1,5 @@
+package main
+
+const (
+ defaultConfigFilePath = "/etc/simplefileshare.conf"
+)
diff --git a/consts_windows.go b/consts_windows.go
new file mode 100644
index 0000000..bf14874
--- /dev/null
+++ b/consts_windows.go
@@ -0,0 +1,5 @@
+package main
+
+const (
+ defaultConfigFilePath = "simplefileshare.conf"
+)
diff --git a/handlers.go b/handlers.go
new file mode 100644
index 0000000..9383ee4
--- /dev/null
+++ b/handlers.go
@@ -0,0 +1,117 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "mime"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strconv"
+)
+
+func HandleRoot(w http.ResponseWriter, r *http.Request) {
+ if r.RequestURI != "/" {
+ http.Error(w, "", http.StatusNotFound)
+ return
+ }
+
+ type FileInfo struct {
+ Name string
+ Size string
+ Date string
+ }
+
+ var data []FileInfo
+
+ err := filepath.Walk(config.StoragePath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ return nil
+ }
+ data = append(data, FileInfo{filepath.Base(path), sizeToApproxHuman(info.Size()), info.ModTime().Format("02.01.2006 15:04")})
+
+ return nil
+ })
+
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ err = templates.ExecuteTemplate(w, "index.htm", data)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func HandleUpload(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ http.Error(w, "wrong method", http.StatusBadRequest)
+ return
+ }
+
+ err := r.ParseMultipartForm(32 << 20)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ file, header, err := r.FormFile("file")
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ defer file.Close()
+
+ filePath := filepath.Join(config.StoragePath, header.Filename)
+ if _, err := os.Stat(filePath); !os.IsNotExist(err) {
+ http.Error(w, "файл с таким именем уже существует", http.StatusBadRequest)
+ return
+ }
+
+ f, err := os.Create(filePath)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer f.Close()
+
+ _, err = io.Copy(f, file)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+func HandleDownload(w http.ResponseWriter, r *http.Request) {
+ filename := filepath.Base(r.FormValue("filename"))
+
+ if filename == "" {
+ http.Error(w, `"filename" field can not be empty`, http.StatusBadRequest)
+ return
+ }
+
+ f, err := os.Open(filepath.Join(config.StoragePath, filename))
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer f.Close()
+
+ fileStat, err := f.Stat()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
+ w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(filename)))
+ w.Header().Set("Accept-Ranges", "none")
+ w.Header().Set("Content-Length", strconv.Itoa(int(fileStat.Size())))
+
+ io.CopyBuffer(w, f, make([]byte, 4096))
+}
diff --git a/index.htm b/index.htm
new file mode 100644
index 0000000..6e632fb
--- /dev/null
+++ b/index.htm
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+ File Storage
+
+
+
+
+
+
+
+
+
+
+
+ Имя |
+ Размер |
+ Дата |
+
+{{range .}}
+ {{.Name}} |
+ {{.Size}} |
+ {{.Date}} |
+
+{{end}}
+
+
+
+
+
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..cb166ec
--- /dev/null
+++ b/main.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ log "github.com/sirupsen/logrus"
+)
+
+func init() {
+ log.SetOutput(os.Stderr)
+ log.SetFormatter(&logrus.TextFormatter{ForceColors: true, DisableTimestamp: true})
+ log.SetLevel(log.ErrorLevel)
+
+ err := initConfig()
+ if err != nil {
+ log.Fatalln("initConfig:", err)
+ }
+
+ log.SetLevel(config.LogLevel)
+
+ err = initTemplates()
+ if err != nil {
+ log.Fatalln("initTemplates:", err)
+ }
+
+ if config.RemoveFilePeriod > 0 {
+ go removeOldFilesThread(config.StoragePath, time.Duration(config.RemoveFilePeriod)*time.Hour)
+ }
+
+ http.HandleFunc("/", HandleRoot)
+ http.HandleFunc("/upload", HandleUpload)
+ http.HandleFunc("/download", HandleDownload)
+}
+
+func main() {
+ go func() {
+ err := http.ListenAndServe(config.ListenAddress, nil)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ }()
+
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+
+ <-c
+ log.Debugln("Stop signal received.")
+}
diff --git a/simplefileshare.conf b/simplefileshare.conf
new file mode 100644
index 0000000..2684d49
--- /dev/null
+++ b/simplefileshare.conf
@@ -0,0 +1,10 @@
+# HTTP-server listen address
+ListenAddress = ":8000"
+
+# File storage path
+StoragePath = "files"
+
+# File removing period (hours)
+RemoveFilePeriod = 1
+
+LogLevel = "debug"
diff --git a/simplefileshare.service b/simplefileshare.service
new file mode 100644
index 0000000..18f7b69
--- /dev/null
+++ b/simplefileshare.service
@@ -0,0 +1,41 @@
+[Unit]
+Description=simplefileshare service
+
+[Service]
+Type=simple
+User=simplefileshare
+ExecStart=/usr/bin/simplefileshare
+Restart=on-failure
+RestartSec=10s
+
+SecureBits=keep-caps
+CapabilityBoundingSet=CAP_NET_BIND_SERVICE
+AmbientCapabilities=CAP_NET_BIND_SERVICE
+DevicePolicy=closed
+IPAccounting=true
+LockPersonality=true
+MemoryDenyWriteExecute=true
+NoNewPrivileges=true
+PrivateDevices=true
+PrivateTmp=true
+ProtectClock=true
+ProtectControlGroups=true
+ProtectControlGroups=true
+ProtectHome=true
+ProtectHostname=true
+ProtectKernelLogs=true
+ProtectKernelModules=true
+ProtectKernelTunables=true
+ProtectSystem=strict
+ReadWritePaths=
+RemoveIPC=true
+RestrictNamespaces=true
+RestrictRealtime=true
+RestrictSUIDSGID=true
+SystemCallArchitectures=native
+SystemCallFilter=@system-service
+SystemCallFilter=~@resources
+UMask=0027
+
+[Install]
+WantedBy=multi-user.target
diff --git a/simplefileshare.sysusers b/simplefileshare.sysusers
new file mode 100644
index 0000000..ee78e1f
--- /dev/null
+++ b/simplefileshare.sysusers
@@ -0,0 +1 @@
+u simplefileshare - "simplefileshare user"
diff --git a/templates.go b/templates.go
new file mode 100644
index 0000000..43da7a2
--- /dev/null
+++ b/templates.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "embed"
+ "html/template"
+
+ log "github.com/sirupsen/logrus"
+)
+
+//go:embed index.htm
+var templatesFS embed.FS
+
+var templates *template.Template
+
+func initTemplates() error {
+ log.Debugln("Templates initialization started.")
+ defer log.Debugln("Templates initialization finished.")
+
+ var err error
+ templates, err = template.ParseFS(templatesFS, "*.htm")
+ if err != nil {
+ return err
+ }
+ templatesFS = embed.FS{} // free memory
+
+ return nil
+}
diff --git a/utils.go b/utils.go
new file mode 100644
index 0000000..25983f5
--- /dev/null
+++ b/utils.go
@@ -0,0 +1,28 @@
+package main
+
+import "fmt"
+
+func sizeToApproxHuman(s int64) string {
+ t := []struct {
+ Name string
+ Val int64
+ }{
+ {"EiB", 1 << 60},
+ {"PiB", 1 << 50},
+ {"TiB", 1 << 40},
+ {"GiB", 1 << 30},
+ {"MiB", 1 << 20},
+ {"KiB", 1 << 10}}
+
+ var v float64
+ for i := 0; i < len(t); i++ {
+ v = float64(s) / float64(t[i].Val)
+ if v < 1.0 {
+ continue
+ }
+
+ return fmt.Sprintf("%.1f %s", v, t[i].Name)
+ }
+
+ return fmt.Sprintf("%.1f KiB", v)
+}
diff --git a/walker.go b/walker.go
new file mode 100644
index 0000000..c11c383
--- /dev/null
+++ b/walker.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "os"
+ "path/filepath"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+func removeOldFilesThread(path string, olderThan time.Duration) {
+ ticker := time.NewTicker(olderThan)
+
+ for _ = range ticker.C {
+ log.Debugln("Removing old files...")
+ err := removeOldFiles(path, olderThan)
+ if err != nil {
+ log.Println(err)
+ }
+ log.Debugln("Removing old files completed.")
+ }
+}
+
+func removeOldFiles(path string, olderThan time.Duration) error {
+ return filepath.Walk(config.StoragePath, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ return nil
+ }
+
+ if info.ModTime().Add(olderThan).Before(time.Now()) {
+ log.WithField("filepath", path).Debugln("Removing file...")
+ err := os.Remove(path)
+ if err != nil {
+ log.Println(err)
+ }
+ }
+
+ return nil
+ })
+
+}