mirror of
https://github.com/nxshock/simplefileshare.git
synced 2024-11-28 03:21:00 +05:00
Import project
This commit is contained in:
parent
f0e39831c2
commit
1b0cbbf5cd
32
PKGBUILD
Normal file
32
PKGBUILD
Normal 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
47
config.go
Normal 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
5
consts_linux.go
Normal file
@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
const (
|
||||
defaultConfigFilePath = "/etc/simplefileshare.conf"
|
||||
)
|
5
consts_windows.go
Normal file
5
consts_windows.go
Normal file
@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
const (
|
||||
defaultConfigFilePath = "simplefileshare.conf"
|
||||
)
|
117
handlers.go
Normal file
117
handlers.go
Normal 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
178
index.htm
Normal 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
53
main.go
Normal 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
10
simplefileshare.conf
Normal 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
41
simplefileshare.service
Normal 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
1
simplefileshare.sysusers
Normal file
@ -0,0 +1 @@
|
||||
u simplefileshare - "simplefileshare user"
|
27
templates.go
Normal file
27
templates.go
Normal 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
28
utils.go
Normal 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
44
walker.go
Normal 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
|
||||
})
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user