Import project

This commit is contained in:
nxshock 2021-08-31 19:07:19 +05:00
parent f0e39831c2
commit 1b0cbbf5cd
13 changed files with 588 additions and 0 deletions

32
PKGBUILD Normal file
View File

@ -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"
}

47
config.go Normal file
View File

@ -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
}

5
consts_linux.go Normal file
View File

@ -0,0 +1,5 @@
package main
const (
defaultConfigFilePath = "/etc/simplefileshare.conf"
)

5
consts_windows.go Normal file
View File

@ -0,0 +1,5 @@
package main
const (
defaultConfigFilePath = "simplefileshare.conf"
)

117
handlers.go Normal file
View File

@ -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))
}

178
index.htm Normal file
View File

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>File Storage</title>
<style>
* {
font-family: Verdana;
font-size: 16px;
color: #444;
margin: .5em;
padding: 0;
}
html {
margin: 0;
padding: 0;
height: 100%
}
body {
background-color: #fafafa;
display: flex;
justify-content: start;
flex-wrap: nowrap;
flex-direction: column;
height: 100%;
margin: 0;
padding: 0;
}
a {
text-decoration: none;
color: #07a;
margin: 0;
}
svg, img {
vertical-align: middle;
width: 1em;
height: 1em;
}
header {
background-color: #eee;
border-bottom: 1px solid #ddd;
padding: .5em;
margin: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
footer {
background-color: #eee;
border-top: 1px solid #ddd;
padding: .5em;
margin: 0;
}
h1 {
margin-top: 1em;
font-size: 150%;
}
table {
border-collapse: collapse;
width: 100%;
margin: 0;
}
tr:hover {
background-color: #dde;
}
th, td {
border: 1px solid #777;
padding: 0.5em;
}
td:nth-child(2), td:nth-child(3) {
width: 10em;
text-align: right;
}
th {
background-color: #eee;
}
form {
width: calc(100% - 1em);
}
form > input {
width: calc(100% - 2em);
padding: .5em;
}
button {
padding: .5em;
margin: 0;
}
pre {
margin: 0;
font-family: monospace;
}
input[type="file"] {
display: none;
}
label {
margin: 0;
padding: .5em;
border: 1px solid #ccc;
cursor: pointer;
}
</style>
</head>
<body>
<header>
<span>File Storage</span>
<label>
<input id="file-uploader" type="file" id="upload-button">
Загрузить файл
</label>
</header>
<main>
<table>
<col width="*">
<col width="0">
<col width="0">
<tr>
<th>Имя</th>
<th>Размер</th>
<th>Дата</th>
</tr>
{{range .}} <tr>
<td><a href="/download?filename={{.Name}}">{{.Name}}</a></td>
<td><pre>{{.Size}}</pre></td>
<td>{{.Date}}</td>
</tr>
{{end}} </table>
</main>
</body>
</html>
<script type="text/javascript">
function myProgressHandler(event) {
var p = Math.floor(event.loaded/event.total*100);
document.querySelector("label").innerHTML = 'Загрузка: ' + p + '%...';
}
function myOnLoadHandler(event) {
const response = event.currentTarget;
if (response.status != 200) {
alert('Ошибка при загрузке файла:\n' + response.responseText);
}
document.querySelector("label").innerHTML = 'Загрузка завершена.';
location.reload();
}
document.getElementById("file-uploader").addEventListener('change', (e) => {
var file = document.getElementById("file-uploader").files[0];
var formData = new FormData;
formData.append('file', file);
var ajax = new XMLHttpRequest;
ajax.upload.addEventListener("progress", myProgressHandler, false);
ajax.addEventListener('load', myOnLoadHandler, false);
ajax.open('POST', '/upload', true);
ajax.send(formData);
});
</script>

53
main.go Normal file
View File

@ -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.")
}

10
simplefileshare.conf Normal file
View File

@ -0,0 +1,10 @@
# HTTP-server listen address
ListenAddress = ":8000"
# File storage path
StoragePath = "files"
# File removing period (hours)
RemoveFilePeriod = 1
LogLevel = "debug"

41
simplefileshare.service Normal file
View File

@ -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

1
simplefileshare.sysusers Normal file
View File

@ -0,0 +1 @@
u simplefileshare - "simplefileshare user"

27
templates.go Normal file
View File

@ -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
}

28
utils.go Normal file
View File

@ -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)
}

44
walker.go Normal file
View File

@ -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
})
}