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 + + + + +
+ File Storage + +
+
+ + + + + + + + + +{{range .}} + + + + +{{end}}
ИмяРазмерДата
{{.Name}}
{{.Size}}
{{.Date}}
+
+ + + + 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 + }) + +}