#!/bin/bash
#
# Автор: Федя Серафиев / urocibg.eu
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[ИНФО]${NC} $1"; }
warn() { echo -e "${YELLOW}[ПРЕДУПРЕЖДЕНИЕ]${NC} $1"; }
error() { echo -e "${RED}[ГРЕШКА]${NC} $1"; exit 1; }
[ "$EUID" -ne 0 ] && error "Стартирай със sudo!"
log "Fedya's File Server – Професионална версия с красив дизайн"
read -p "Директория за файловете [/var/www/files]: " file_dir
file_dir=${file_dir:-"/var/www/files"}
mkdir -p "$file_dir"
read -p "Порт [8080]: " port
port=${port:-"8080"}
read -p "Име на сървъра [Fedya File Server]: " server_name
server_name=${server_name:-"Fedya File Server"}
read -p "Стартиране като услуга? [Y/n]: " svc
[[ "$svc" =~ ^[Nn]$ ]] && auto_start=false || auto_start=true
log "Инсталиране на Go..."
command -v go >/dev/null || { apt update && apt install -y golang-go; }
tmp=$(mktemp -d)
cd "$tmp"
go mod init fedya-files >/dev/null 2>&1
cat > main.go <<'EOF'
package main
import (
"archive/zip"
"fmt"
"html/template"
"image"
"image/jpeg"
_ "image/png"
_ "image/gif"
"io"
"log"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
var (
fileDir = getEnv("FILE_DIR", "/var/www/files")
port = getEnv("PORT", "8080")
serverName = getEnv("SERVER_NAME", "Fedya File Server")
tmpl = template.Must(template.New("index").Parse(htmlTemplate))
)
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
const htmlTemplate = `
{{.ServerName}}
📊 Общо файлове: {{.FileCount}}
📁 Папки: {{.DirCount}}
🖼️ Картинки: {{.ImageCount}}
Път: /{{.Current}}
| Име |
Размер |
Дата |
{{if ne .Current "/"}}
|
⬆️ Назад
|
{{end}}
{{range .Files}}
{{if .IsDir}}
{{else}}
{{if .Thumbnail}}

{{end}}
{{end}}
|
{{.Size}} |
{{.ModTime}} |
{{end}}
`
type File struct {
Name string
Size string
ModTime string
IsDir bool
Thumbnail string
}
type PageData struct {
Files []File
Current string
CurrentPath string
ServerName string
FileCount int
DirCount int
ImageCount int
CurrentTime string
}
func main() {
log.Printf("Стартиране на %s на порт %s за директория %s", serverName, port, fileDir)
http.HandleFunc("/", handler)
http.HandleFunc("/zip", zipHandler)
http.HandleFunc("/thumb/", thumbHandler)
log.Printf("Сървърът е готов на http://0.0.0.0:%s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/zip") {
zipHandler(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/thumb/") {
thumbHandler(w, r)
return
}
reqPath := filepath.Clean(r.URL.Path)
if reqPath == "/" || reqPath == "." {
reqPath = ""
}
fullPath := filepath.Join(fileDir, reqPath)
if !strings.HasPrefix(fullPath, fileDir) {
http.Error(w, "Достъпът е отказан", http.StatusForbidden)
return
}
fi, err := os.Stat(fullPath)
if err != nil {
http.NotFound(w, r)
return
}
if fi.IsDir() {
if r.Method == "POST" {
upload(w, r, fullPath)
return
}
listDir(w, reqPath, fullPath)
return
}
http.ServeFile(w, r, fullPath)
}
func upload(w http.ResponseWriter, r *http.Request, dir string) {
if err := r.ParseMultipartForm(500 << 20); err != nil {
http.Error(w, "Файлът е твърде голям (максимум 500MB)", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("f")
if err != nil {
http.Error(w, "Грешка при качване", http.StatusBadRequest)
return
}
defer file.Close()
filePath := filepath.Join(dir, header.Filename)
dst, err := os.Create(filePath)
if err != nil {
http.Error(w, "Грешка при създаване на файл", http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, "Грешка при запис на файл", http.StatusInternalServerError)
return
}
http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
}
func listDir(w http.ResponseWriter, relPath, fullPath string) {
entries, err := os.ReadDir(fullPath)
if err != nil {
http.Error(w, "Грешка при четене на директория", http.StatusInternalServerError)
return
}
var files []File
fileCount := 0
dirCount := 0
imageCount := 0
for _, e := range entries {
info, err := e.Info()
if err != nil {
continue
}
size := "📁 папка"
if !e.IsDir() {
fileCount++
s := info.Size()
switch {
case s < 1024:
size = fmt.Sprintf("%d Б", s)
case s < 1048576:
size = fmt.Sprintf("%.1f КБ", float64(s)/1024)
case s < 1073741824:
size = fmt.Sprintf("%.1f МБ", float64(s)/1048576)
default:
size = fmt.Sprintf("%.1f ГБ", float64(s)/1073741824)
}
} else {
dirCount++
}
thumbnail := ""
if !e.IsDir() && isImageFile(e.Name()) {
imageCount++
thumbnail = fmt.Sprintf("/thumb%s/%s", relPath, e.Name())
}
files = append(files, File{
Name: e.Name(),
Size: size,
ModTime: info.ModTime().Format("02.01.2006 15:04"),
IsDir: e.IsDir(),
Thumbnail: thumbnail,
})
}
sort.Slice(files, func(i, j int) bool {
if files[i].IsDir != files[j].IsDir {
return files[i].IsDir
}
return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
})
current := relPath
if current == "" {
current = "/"
}
currentPath := relPath
if currentPath != "" && !strings.HasPrefix(currentPath, "/") {
currentPath = "/" + currentPath
}
data := PageData{
Files: files,
Current: current,
CurrentPath: currentPath,
ServerName: serverName,
FileCount: fileCount,
DirCount: dirCount,
ImageCount: imageCount,
CurrentTime: time.Now().Format("02.01.2006 15:04:05"),
}
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, "Грешка при генериране на страница", http.StatusInternalServerError)
}
}
func thumbHandler(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/thumb")
fullPath := filepath.Join(fileDir, filepath.Clean(path))
if !strings.HasPrefix(fullPath, fileDir) {
http.Error(w, "Достъпът е отказан", http.StatusForbidden)
return
}
if _, err := os.Stat(fullPath); os.IsNotExist(err) || !isImageFile(fullPath) {
http.NotFound(w, r)
return
}
generateThumbnail(w, fullPath)
}
func generateThumbnail(w http.ResponseWriter, filePath string) {
file, err := os.Open(filePath)
if err != nil {
http.Error(w, "Грешка при отваряне на файл", http.StatusInternalServerError)
return
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
http.Error(w, "Невалидно изображение", http.StatusInternalServerError)
return
}
bounds := img.Bounds()
width := bounds.Dx()
height := bounds.Dy()
newWidth := 90
newHeight := 70
if width > height {
newHeight = int(float64(height) * float64(newWidth) / float64(width))
} else {
newWidth = int(float64(width) * float64(newHeight) / float64(height))
}
dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
for y := 0; y < newHeight; y++ {
for x := 0; x < newWidth; x++ {
srcX := x * width / newWidth
srcY := y * height / newHeight
dst.Set(x, y, img.At(srcX, srcY))
}
}
w.Header().Set("Content-Type", "image/jpeg")
if err := jpeg.Encode(w, dst, &jpeg.Options{Quality: 85}); err != nil {
http.Error(w, "Грешка при кодиране на миниатюра", http.StatusInternalServerError)
return
}
}
func isImageFile(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
imageExts := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".svg"}
for _, imgExt := range imageExts {
if ext == imgExt {
return true
}
}
return false
}
func zipHandler(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/zip")
if path == "" {
path = "/"
}
fullPath := filepath.Join(fileDir, filepath.Clean(path))
if !strings.HasPrefix(fullPath, fileDir) {
http.Error(w, "Достъпът е отказан", http.StatusForbidden)
return
}
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/zip")
filename := filepath.Base(fullPath)
if filename == "" || filename == "." || filename == "/" {
filename = "files"
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", filename))
zw := zip.NewWriter(w)
defer zw.Close()
filepath.Walk(fullPath, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(fullPath, filePath)
if err != nil {
return err
}
if relPath == "." {
return nil
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = relPath
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
writer, err := zw.CreateHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
if err != nil {
return err
}
}
return nil
})
}
EOF
log "Компилиране на професионалната версия..."
CGO_ENABLED=0 go build -ldflags="-s -w" -o fedya-server || error "Грешка при компилация!"
install -m 755 fedya-server /usr/local/bin/fedya-server
# Спиране на старите услуги
systemctl stop file-server sabork-server fedya-server 2>/dev/null || true
systemctl disable file-server sabork-server 2>/dev/null || true
if $auto_start; then
log "Конфигуриране на systemd услуга..."
cat > /etc/systemd/system/fedya-server.service </dev/null 2>&1
if systemctl start fedya-server; then
log "Услугата $server_name е стартирана успешно!"
sleep 2
log "Статус на услугата:"
systemctl status fedya-server --no-pager -l
else
error "Неуспешно стартиране на услугата. Провери: journalctl -u fedya-server -f"
fi
else
log "Ръчно стартиране:"
echo "FILE_DIR='$file_dir' PORT='$port' SERVER_NAME='$server_name' /usr/local/bin/fedya-server"
fi
rm -rf "$tmp"
IP=$(hostname -I | awk '{print $1}')
log "ГОТОВО! Отвори: http://$IP:$port"
log "🎉 Професионален дизайн • Цветно лого • Фавикон • Статистики"
if $auto_start; then
echo
log "Команди за управление:"
echo " systemctl status fedya-server"
echo " journalctl -u fedya-server -f"
echo " systemctl restart fedya-server"
fi
echo
log "💼 Сървърът е готов за колегите!"
echo " 🌐 Може да го споделиш на: http://$IP:$port"
echo " 📊 Има статистики за файлове, папки и картинки"
echo " 🎨 Професионален дизайн с анимации и ефекти"
echo " 🔒 Безопасен и стабилен за корпоративна среда"