Last active 1764478533

Fedya's File Server - професионален файлов сървър на Go, който може да се инсталира за минути.

urocibg revised this gist 1764478533. Go to revision

1 file changed, 3 insertions

fedya-server.sh

@@ -1,4 +1,7 @@
1 1 #!/bin/bash
2 + #
3 + # Автор: Федя Серафиев / urocibg.eu
4 +
2 5 clear
3 6
4 7 echo -e "\e[1;36m"

urocibg revised this gist 1764478455. Go to revision

1 file changed, 1025 insertions, 706 deletions

fedya-server.sh

@@ -1,56 +1,70 @@
1 1 #!/bin/bash
2 - #
3 - # Автор: Федя Серафиев / urocibg.eu
4 -
5 - RED='\033[0;31m'
6 - GREEN='\033[0;32m'
7 - YELLOW='\033[1;33m'
8 - NC='\033[0m'
2 + clear
3 +
4 + echo -e "\e[1;36m"
5 + cat << "EOF"
6 + ███████╗███████╗██████╗ ██╗ ██╗ █████╗ ██╗ ██╗██████╗ ██╗ ████████╗██████╗ █████╗
7 + ██╔════╝██╔════╝██╔══██╗╚██╗ ██╔╝██╔══██╗ ██║ ██║██╔══██╗██║ ╚══██╔══╝██╔══██╗██╔══██╗
8 + █████╗ █████╗ ██║ ██║ ╚████╔╝ ███████║ ██║ ██║██████╔╝██║ ██║ ██████╔╝███████║
9 + ██╔══╝ ██╔══╝ ██║ ██║ ╚██╔╝ ██╔══██║ ██║ ██║██╔══██╗██║ ██║ ██╔══██╗██╔══██║
10 + ██║ ███████╗██████╔╝ ██║ ██║ ██║ ╚██████╔╝██║ ██║███████╗██║ ██║ ██║██║ ██║
11 + ╚═╝ ╚══════╝╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝
12 + EOF
13 + echo -e "\e[0m"
9 14
10 - log() { echo -e "${GREEN}[ИНФО]${NC} $1"; }
11 - warn() { echo -e "${YELLOW}[ПРЕДУПРЕЖДЕНИЕ]${NC} $1"; }
12 - error() { echo -e "${RED}[ГРЕШКА]${NC} $1"; exit 1; }
15 + echo -e "\e[1;35mFEDYA ULTRA PRO SERVER v4.9 — MULTILANGUAGE\e[0m"
16 + echo -e "\e[1;34m══════════════════════════════════════════════════════════════════════\e[0m"
17 + echo
13 18
14 - [ "$EUID" -ne 0 ] && error "Стартирай със sudo!"
19 + # Проверка за root права
20 + [ "$EUID" -ne 0 ] && { echo -e "\e[1;31m❌ sudo required!\e[0m"; exit 1; }
15 21
16 - log "Fedya's File Server – Професионална версия с красив дизайн"
22 + # Събиране на потребителски настройки
23 + echo -e "\e[1;33m⚙️ Server Configuration:\e[0m"
24 + read -p "$(echo -e '\e[1;36m📁 Files directory\e[0m [\e[1;32m/var/www/files\e[0m]: ')" DIR
25 + DIR=${DIR:-"/var/www/files"}
26 + mkdir -p "$DIR"
17 27
18 - read -p "Директория за файловете [/var/www/files]: " file_dir
19 - file_dir=${file_dir:-"/var/www/files"}
20 - mkdir -p "$file_dir"
28 + read -p "$(echo -e '\e[1;36m🌐 Port\e[0m [\e[1;32m8080\e[0m]: ')" PORT
29 + PORT=${PORT:-"8080"}
21 30
22 - read -p "Порт [8080]: " port
23 - port=${port:-"8080"}
31 + read -p "$(echo -e '\e[1;36m🏷️ Server name\e[0m [\e[1;32mFedya'\''s File Server\e[0m]: ')" NAME
32 + NAME=${NAME:-"Fedya's File Server"}
24 33
25 - read -p "Име на сървъра [Fedya File Server]: " server_name
26 - server_name=${server_name:-"Fedya File Server"}
34 + read -p "$(echo -e '\e[1;36m🌍 Language (bg/en)\e[0m [\e[1;32men\e[0m]: ')" LANG
35 + LANG=${LANG:-"en"}
27 36
28 - read -p "Стартиране като услуга? [Y/n]: " svc
29 - [[ "$svc" =~ ^[Nn]$ ]] && auto_start=false || auto_start=true
37 + read -p "$(echo -e '\e[1;36m🔧 Install as systemd service?\e[0m [\e[1;32mY\e[0m/n]: ')" SVC
38 + [[ "$SVC" =~ ^[Nn]$ ]] && SERVICE=false || SERVICE=true
30 39
31 - log "Инсталиране на Go..."
32 - command -v go >/dev/null || { apt update && apt install -y golang-go; }
40 + echo -e "\n\e[1;33m📦 Installing Go...\e[0m"
41 + apt update -qq && apt install -y golang-go &>/dev/null || true
33 42
34 - tmp=$(mktemp -d)
35 - cd "$tmp"
43 + # Подготовка на временна директория
44 + TMP=$(mktemp -d)
45 + cd "$TMP"
36 46
37 - go mod init fedya-files >/dev/null 2>&1
47 + echo -e "\e[1;33m🔨 Creating Go server...\e[0m"
38 48
49 + # Създаване на Go код с поддръжка на два езика
39 50 cat > main.go <<'EOF'
40 51 package main
41 52
42 53 import (
43 54 "archive/zip"
55 + "crypto/rand"
56 + "encoding/hex"
44 57 "fmt"
45 58 "html/template"
46 59 "image"
47 60 "image/jpeg"
48 - _ "image/png"
49 61 _ "image/gif"
62 + _ "image/png"
50 63 "io"
51 64 "log"
52 65 "net/http"
53 66 "os"
67 + "path"
54 68 "path/filepath"
55 69 "sort"
56 70 "strings"
@@ -58,50 +72,83 @@ import (
58 72 )
59 73
60 74 var (
61 - fileDir = getEnv("FILE_DIR", "/var/www/files")
62 - port = getEnv("PORT", "8080")
63 - serverName = getEnv("SERVER_NAME", "Fedya File Server")
64 - tmpl = template.Must(template.New("index").Parse(htmlTemplate))
75 + fileDir string
76 + absFileDir string
77 + port string
78 + serverName string
79 + lang string
80 + tmpl *template.Template
81 + links = make(map[string]struct {
82 + Path string
83 + Expiry time.Time
84 + OneTime bool
85 + Used bool
86 + })
65 87 )
66 88
67 - func getEnv(key, defaultValue string) string {
68 - if value := os.Getenv(key); value != "" {
69 - return value
89 + func init() {
90 + fileDir = os.Getenv("FILE_DIR")
91 + if fileDir == "" {
92 + fileDir = "/var/www/files"
93 + }
94 + port = os.Getenv("PORT")
95 + if port == "" {
96 + port = "8080"
97 + }
98 + serverName = os.Getenv("SERVER_NAME")
99 + if serverName == "" {
100 + serverName = "Fedya's File Server"
101 + }
102 + lang = os.Getenv("LANG")
103 + if lang == "" {
104 + lang = "en"
105 + }
106 +
107 + var err error
108 + fileDir, err = filepath.Abs(fileDir)
109 + if err != nil {
110 + log.Fatalf("Error getting absolute path: %v", err)
111 + }
112 + absFileDir = fileDir
113 +
114 + // Create base directories
115 + os.MkdirAll(filepath.Join(fileDir, "pdf"), 0755)
116 + os.MkdirAll(filepath.Join(fileDir, "images"), 0755)
117 +
118 + // Choose template based on language
119 + if lang == "bg" {
120 + tmpl = template.Must(template.New("index").Parse(htmlBG))
121 + } else {
122 + tmpl = template.Must(template.New("index").Parse(htmlEN))
70 123 }
71 - return defaultValue
72 124 }
73 125
74 - const htmlTemplate = `<!DOCTYPE html>
126 + const htmlBG = `<!DOCTYPE html>
75 127 <html lang="bg">
76 128 <head>
77 129 <meta charset="UTF-8">
78 - <meta name="viewport" content="width=device-width, initial-scale=1.0">
79 - <title>{{.ServerName}}</title>
80 - <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><defs><linearGradient id='grad' x1='0%' y1='0%' x2='100%' y2='100%'><stop offset='0%' style='stop-color:%233b82f6;stop-opacity:1' /><stop offset='100%' style='stop-color:%231e40af;stop-opacity:1' /></linearGradient></defs><circle cx='50' cy='50' r='45' fill='url(%23grad)'/><rect x='30' y='30' width='40' height='30' rx='5' fill='white'/><rect x='35' y='45' width='30' height='2' fill='%233b82f6'/><rect x='35' y='50' width='25' height='2' fill='%233b82f6'/><rect x='35' y='55' width='20' height='2' fill='%233b82f6'/></svg>">
130 + <meta name="viewport" content="width=device-width,initial-scale=1">
131 + <title>{{.Name}}</title>
132 + <!-- ПРОФЕСИОНАЛЕН ФАВИКОН -->
133 + <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHJ4PSIxMiIgZmlsbD0idXJsKCNncmFkKSIvPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3R5bGU9InN0b3AtY29sb3I6IzRGNjBFNTtzdG9wLW9wYWNpdHk6MSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiMwNkI2RDQ7c3RvcC1vcGFjaXR5OjEiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgPC9kZWZzPgogIDxwYXRoIGQ9Ik0yMCAyMEMyMCAxNy43OTA5IDE3Ljc5MDkgMTYgMTUgMTZDMTIuMjA5MSAxNiAxMCAxNy43OTA5IDEwIDIwQzEwIDIyLjIwOTEgMTIuMjA5MSAyNCAxNSAyNEMxNy43OTA5IDI0IDIwIDIyLjIwOTEgMjAgMjBaIiBmaWxsPSJ3aGl0ZSIvPgogIDxwYXRoIGQ9Ik0zOCAyMEMzOCAxNy43OTA5IDM1Ljc5MDkgMTYgMzMgMTZDMzAuMjA5MSAxNiAyOCAxNy43OTA5IDI4IDIwQzI4IDIyLjIwOTEgMzAuMjA5MSAyNCAzMyAyNEMzNS43OTA5IDI0IDM4IDIyLjIwOTEgMzggMjBaIiBmaWxsPSJ3aGl0ZSIvPgogIDxwYXRoIGQ9Ik01NCAyMEM1NCAxNy43OTA5IDUxLjc5MDkgMTYgNDkgMTZDNDYuMjA5MSAxNiA0NCAxNy43OTA5IDQ0IDIwQzQ0IDIyLjIwOTEgNDYuMjA5MSAyNCA0OSAyNEM1MS43OTA5IDI0IDU0IDIyLjIwOTEgNTQgMjBaIiBmaWxsPSJ3aGl0ZSIvPgogIDxwYXRoIGQ9Ik0yMCA0MEMyMCAzNy43OTA5IDE3Ljc5MDkgMzYgMTUgMzZDMTIuMjA5MSAzNiAxMCAzNy43OTA5IDEwIDQwQzEwIDQyLjIwOTEgMTIuMjA5MSA0NCAxNSA0NEMxNy43OTA5IDQ0IDIwIDQyLjIwOTEgMjAgNDBaIiBmaWxsPSJ3aGl0ZSIvPgogIDxwYXRoIGQ9Ik0zOCA0MEMzOCAzNy43OTA5IDM1Ljc5MDkgMzYgMzMgMzZDMzAuMjA5MSAzNiAyOCAzNy43OTA5IDI4IDQwQzI4IDQyLjIwOTEgMzAuMjA5MSA0NCAzMyA0NEMzNS43OTA5IDQ0IDM4IDQyLjIwOTEgMzggNDBaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K"/>
81 134 <style>
82 135 :root {
83 - --primary: #3b82f6;
84 - --primary-dark: #1d4ed8;
85 - --primary-light: #60a5fa;
86 - --secondary: #8b5cf6;
87 - --success: #10b981;
88 - --warning: #f59e0b;
89 - --danger: #ef4444;
90 - --dark: #1e293b;
91 - --light: #f8fafc;
92 - --gray: #64748b;
93 - --border: #e2e8f0;
94 - --shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
95 - }
96 -
97 - * {
98 - margin: 0;
99 - padding: 0;
100 - box-sizing: border-box;
136 + --primary: #4F46E5;
137 + --primary-dark: #4338CA;
138 + --secondary: #06B6D4;
139 + --success: #10B981;
140 + --danger: #EF4444;
141 + --warning: #F59E0B;
142 + --dark: #1E293B;
143 + --light: #F8FAFC;
144 + --gray: #64748B;
145 + --border: #E2E8F0;
101 146 }
102 147
148 + * { margin: 0; padding: 0; box-sizing: border-box; }
149 +
103 150 body {
104 - font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
151 + font-family: 'Segoe UI', system-ui, sans-serif;
105 152 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
106 153 color: var(--dark);
107 154 min-height: 100vh;
@@ -110,16 +157,15 @@ const htmlTemplate = `<!DOCTYPE html>
110 157 }
111 158
112 159 .container {
113 - max-width: 1400px;
160 + max-width: 1600px;
114 161 margin: 0 auto;
115 162 background: white;
116 - border-radius: 24px;
117 - box-shadow: var(--shadow), 0 20px 80px rgba(0,0,0,0.15);
163 + border-radius: 20px;
164 + box-shadow: 0 25px 80px rgba(0,0,0,0.15);
118 165 overflow: hidden;
119 - backdrop-filter: blur(10px);
120 166 }
121 167
122 - header {
168 + .header {
123 169 background: linear-gradient(135deg, var(--primary), var(--secondary));
124 170 color: white;
125 171 padding: 3rem 2rem;
@@ -128,7 +174,7 @@ const htmlTemplate = `<!DOCTYPE html>
128 174 overflow: hidden;
129 175 }
130 176
131 - header::before {
177 + .header::before {
132 178 content: '';
133 179 position: absolute;
134 180 top: 0;
@@ -136,889 +182,1162 @@ const htmlTemplate = `<!DOCTYPE html>
136 182 right: 0;
137 183 bottom: 0;
138 184 background: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23ffffff' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E");
139 - animation: float 20s infinite linear;
140 - }
141 -
142 - @keyframes float {
143 - 0% { transform: translateX(0) translateY(0); }
144 - 100% { transform: translateX(-100px) translateY(-100px); }
145 - }
146 -
147 - .logo-container {
148 - position: relative;
149 - z-index: 2;
150 185 }
151 186
152 187 .logo {
153 - font-size: 5rem;
154 - margin-bottom: 1rem;
155 - filter: drop-shadow(0 8px 16px rgba(0,0,0,0.3));
156 - background: linear-gradient(135deg, #ffffff, #e0f2fe);
188 + font-size: 3.5rem;
189 + font-weight: 900;
190 + background: linear-gradient(135deg, #FFF, #E0F2FE);
157 191 -webkit-background-clip: text;
158 192 -webkit-text-fill-color: transparent;
159 - background-clip: text;
160 - }
161 -
162 - h1 {
163 - margin: 0;
164 - font-size: 3rem;
165 - font-weight: 800;
166 - letter-spacing: -1px;
167 - text-shadow: 0 4px 8px rgba(0,0,0,0.2);
193 + margin-bottom: 0.5rem;
194 + text-shadow: 0 2px 4px rgba(0,0,0,0.1);
168 195 }
169 196
170 197 .subtitle {
171 - opacity: 0.95;
172 - margin-top: 0.75rem;
173 - font-weight: 400;
174 198 font-size: 1.3rem;
175 - max-width: 600px;
176 - margin-left: auto;
177 - margin-right: auto;
199 + opacity: 0.9;
200 + font-weight: 300;
178 201 }
179 202
180 - .path-bar {
181 - background: linear-gradient(90deg, #f1f5f9, #f8fafc);
182 - padding: 1.25rem 2rem;
183 - font-family: 'JetBrains Mono', 'Monaco', 'Consolas', monospace;
184 - font-size: 1.1rem;
185 - color: var(--primary-dark);
186 - border-bottom: 1px solid var(--border);
203 + .stats {
204 + padding: 1.5rem;
205 + background: var(--light);
206 + text-align: center;
207 + font-weight: 600;
208 + display: flex;
209 + justify-content: center;
210 + gap: 2rem;
211 + flex-wrap: wrap;
212 + }
213 +
214 + .stat-item {
187 215 display: flex;
188 216 align-items: center;
189 - gap: 12px;
217 + gap: 0.5rem;
218 + color: var(--gray);
190 219 }
191 220
192 - .path-bar::before {
193 - content: '📍';
194 - font-size: 1.2rem;
221 + .path {
222 + padding: 1.2rem 2rem;
223 + background: var(--light);
224 + border-bottom: 1px solid var(--border);
225 + font-weight: 600;
226 + color: var(--primary);
227 + display: flex;
228 + align-items: center;
229 + gap: 0.5rem;
195 230 }
196 231
197 232 .actions {
198 233 padding: 2rem;
199 - background: #f8fafc;
234 + background: var(--light);
200 235 display: flex;
201 - gap: 1.5rem;
202 236 flex-wrap: wrap;
237 + gap: 1.5rem;
203 238 align-items: center;
204 - justify-content: space-between;
205 239 border-bottom: 1px solid var(--border);
206 240 }
207 241
208 242 .upload-box {
209 243 background: white;
210 - padding: 1.75rem;
211 - border-radius: 20px;
212 - box-shadow: var(--shadow);
244 + padding: 2rem;
245 + border-radius: 16px;
246 + box-shadow: 0 8px 25px rgba(139,92,246,0.1);
247 + border: 2px dashed #DDD6FE;
213 248 flex: 1;
214 - min-width: 350px;
215 - border: 2px dashed var(--primary-light);
249 + min-width: 360px;
216 250 transition: all 0.3s ease;
217 251 }
218 252
219 253 .upload-box:hover {
220 254 border-color: var(--primary);
221 - transform: translateY(-2px);
222 - box-shadow: 0 15px 40px rgba(59, 130, 246, 0.15);
223 - }
224 -
225 - .upload-form {
226 - display: flex;
227 - gap: 15px;
228 - width: 100%;
229 - align-items: center;
230 - }
231 -
232 - .file-input {
233 - flex: 1;
234 - padding: 14px 18px;
235 - border: 2px solid #e2e8f0;
236 - border-radius: 12px;
237 - background: white;
238 - font-size: 1rem;
239 - transition: all 0.3s ease;
240 - font-family: inherit;
241 - }
242 -
243 - .file-input:focus {
244 - outline: none;
245 - border-color: var(--primary);
246 - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
247 - }
248 -
249 - .file-input:hover {
250 - border-color: var(--primary-light);
255 + transform: translateY(-3px);
256 + box-shadow: 0 15px 35px rgba(139,92,246,0.15);
251 257 }
252 258
253 259 button, .btn {
254 - background: linear-gradient(135deg, var(--primary), var(--primary-dark));
255 - color: white;
260 + padding: 14px 28px;
256 261 border: none;
257 - padding: 14px 32px;
258 - border-radius: 14px;
262 + border-radius: 12px;
263 + color: white;
264 + background: linear-gradient(135deg, var(--primary), var(--primary-dark));
259 265 cursor: pointer;
260 266 font-weight: 600;
261 - font-size: 1.05rem;
262 - text-decoration: none;
267 + transition: all 0.3s ease;
263 268 display: inline-flex;
264 269 align-items: center;
265 - gap: 10px;
266 - transition: all 0.3s ease;
267 - font-family: inherit;
268 - position: relative;
269 - overflow: hidden;
270 + gap: 0.5rem;
271 + text-decoration: none;
270 272 }
271 273
272 - button::before, .btn::before {
273 - content: '';
274 - position: absolute;
275 - top: 0;
276 - left: -100%;
277 - width: 100%;
278 - height: 100%;
279 - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
280 - transition: left 0.5s;
274 + button:hover, .btn:hover {
275 + transform: translateY(-2px);
276 + box-shadow: 0 10px 25px rgba(79,70,229,0.3);
281 277 }
282 278
283 - button:hover::before, .btn:hover::before {
284 - left: 100%;
279 + .btn-success {
280 + background: linear-gradient(135deg, var(--success), #059669);
285 281 }
286 282
287 - button:hover, .btn:hover {
288 - transform: translateY(-3px);
289 - box-shadow: 0 10px 25px rgba(59, 130, 246, 0.4);
283 + .btn-warning {
284 + background: linear-gradient(135deg, var(--warning), #D97706);
290 285 }
291 286
292 - button:active, .btn:active {
293 - transform: translateY(-1px);
287 + .modal {
288 + display: none;
289 + position: fixed;
290 + top: 0;
291 + left: 0;
292 + right: 0;
293 + bottom: 0;
294 + background: rgba(0,0,0,0.7);
295 + align-items: center;
296 + justify-content: center;
297 + z-index: 1000;
294 298 }
295 299
296 - .btn-success {
297 - background: linear-gradient(135deg, var(--success), #059669);
300 + .modal-content {
301 + background: white;
302 + padding: 2.5rem;
303 + border-radius: 20px;
304 + max-width: 500px;
305 + width: 90%;
306 + box-shadow: 0 25px 50px rgba(0,0,0,0.25);
298 307 }
299 308
300 - .btn-success:hover {
301 - box-shadow: 0 10px 25px rgba(16, 185, 129, 0.4);
309 + .close {
310 + float: right;
311 + font-size: 2rem;
312 + cursor: pointer;
313 + color: var(--gray);
302 314 }
303 315
304 - .file-table {
316 + table {
305 317 width: 100%;
306 318 border-collapse: collapse;
307 - margin: 0;
319 + background: white;
308 320 }
309 321
310 322 th {
311 - background: linear-gradient(135deg, #f8fafc, #f1f5f9);
312 - padding: 1.5rem 2rem;
323 + background: var(--light);
324 + padding: 1.5rem;
313 325 text-align: left;
314 - color: var(--gray);
315 326 font-weight: 700;
327 + color: var(--gray);
316 328 border-bottom: 3px solid var(--border);
317 - font-size: 1.1rem;
318 329 }
319 330
320 331 td {
321 - padding: 1.5rem 2rem;
332 + padding: 1.2rem 1.5rem;
322 333 border-bottom: 1px solid var(--border);
323 - transition: all 0.3s ease;
324 - vertical-align: top;
325 334 }
326 335
327 - tr {
328 - transition: all 0.3s ease;
336 + tr:hover {
337 + background: #F0F9FF;
329 338 }
330 339
331 - tr:hover {
332 - background: linear-gradient(90deg, #f0f9ff, #f8fafc);
333 - transform: translateX(8px);
340 + .thumbnail {
341 + width: 80px;
342 + height: 60px;
343 + object-fit: cover;
344 + border-radius: 8px;
345 + border: 2px solid var(--border);
346 + cursor: pointer;
334 347 }
335 348
336 - .size-col {
337 - text-align: right;
338 - color: var(--gray);
339 - font-variant-numeric: tabular-nums;
340 - font-weight: 600;
349 + .thumbnail:hover {
350 + transform: scale(1.05);
351 + border-color: var(--primary);
341 352 }
342 353
343 - .date-col {
344 - color: #94a3b8;
345 - font-size: 0.95rem;
346 - font-weight: 500;
354 + .file-icon {
355 + font-size: 1.8rem;
356 + margin-right: 10px;
347 357 }
348 358
349 - .folder {
350 - font-weight: 700;
351 - color: var(--primary);
352 - font-size: 1.1rem;
359 + .folder-icon { color: var(--warning); }
360 + .pdf-icon { color: var(--danger); }
361 + .image-icon { color: var(--success); }
362 + .file-icon-default { color: var(--primary); }
363 +
364 + .link-result {
365 + background: #ECFDF5;
366 + padding: 1.5rem;
367 + border-radius: 12px;
368 + margin: 1.5rem 0;
369 + border: 2px solid var(--success);
353 370 }
354 371
355 - .file-link {
356 - color: var(--dark);
357 - text-decoration: none;
358 - transition: all 0.3s ease;
359 - display: flex;
360 - align-items: center;
361 - gap: 12px;
362 - font-weight: 500;
372 + .footer {
373 + padding: 2.5rem;
374 + text-align: center;
375 + color: var(--gray);
376 + background: var(--light);
363 377 }
378 + </style>
379 + </head>
380 + <body>
381 + <div class="container">
382 + <div class="header">
383 + <div class="logo">{{.Name}}</div>
384 + <div class="subtitle">Временни линкове • PDF & Снимки в папки • ZIP • Миниатюри</div>
385 + </div>
364 386
365 - .file-link:hover {
366 - color: var(--primary);
367 - transform: translateX(5px);
387 + <div class="stats">
388 + <div class="stat-item">📊 Файлове: <b>{{.Files}}</b></div>
389 + <div class="stat-item">📁 Папки: <b>{{.Dirs}}</b></div>
390 + <div class="stat-item">🖼️ Снимки: <b>{{.Imgs}}</b></div>
391 + </div>
392 +
393 + <div class="path">
394 + <span>📂</span>
395 + <span>Път: <b>/{{.Path}}</b></span>
396 + </div>
397 +
398 + <div class="actions">
399 + <div class="upload-box">
400 + <form method="post" enctype="multipart/form-data">
401 + <div style="display:flex;gap:15px;flex-wrap:wrap;align-items:center">
402 + <input type="file" name="f" multiple required
403 + style="flex:1;min-width:250px;padding:14px;border:2px solid var(--border);border-radius:12px">
404 + <button type="submit">📤 Качи</button>
405 + </div>
406 + </form>
407 + </div>
408 +
409 + <button onclick="document.getElementById('modalFolder').style.display='flex'">📁 Нова папка</button>
410 + <a href="/zip{{.Zip}}" class="btn btn-success">📦 ZIP</a>
411 + </div>
412 +
413 + <table>
414 + <thead>
415 + <tr>
416 + <th>Име</th>
417 + <th>Размер</th>
418 + <th>Дата</th>
419 + <th>Действия</th>
420 + </tr>
421 + </thead>
422 + <tbody>
423 + {{if ne .Path "/"}}
424 + <tr>
425 + <td colspan="4">
426 + <a href=".." style="color:var(--primary);font-weight:700;text-decoration:none">⬆️ Назад</a>
427 + </td>
428 + </tr>
429 + {{end}}
430 +
431 + {{range .Items}}
432 + <tr>
433 + <td style="display:flex;align-items:center;gap:15px">
434 + {{if .Thumb}}
435 + <img src="{{.Thumb}}" class="thumbnail" onclick="window.open('{{$.Cur}}/{{.Name}}','_blank')">
436 + {{else}}
437 + <div class="file-icon {{if .Dir}}folder-icon{{else if eq .Ext ".pdf"}}pdf-icon{{else if .Img}}image-icon{{else}}file-icon-default{{end}}">
438 + {{if .Dir}}📁{{else if eq .Ext ".pdf"}}📄{{else if .Img}}🖼️{{else}}📄{{end}}
439 + </div>
440 + {{end}}
441 +
442 + <a href="{{if .Dir}}{{.Name}}/{{else}}{{.Name}}{{end}}"
443 + style="color:var(--dark);text-decoration:none;font-weight:500">
444 + <span style="font-weight:600">{{.Name}}</span>
445 + </a>
446 + </td>
447 + <td>{{.Size}}</td>
448 + <td>{{.Date}}</td>
449 + <td>
450 + {{if not .Dir}}
451 + <button onclick="generateTempLink('{{$.Cur}}/{{.Name}}')">🔗 Временен линк</button>
452 + {{else}}
453 + <span style="color:var(--gray)">—</span>
454 + {{end}}
455 + </td>
456 + </tr>
457 + {{end}}
458 + </tbody>
459 + </table>
460 +
461 + <div class="footer">
462 + © 2025 {{.Name}} | Файлов сървър с пълна функционалност
463 + </div>
464 + </div>
465 +
466 + <div id="modalFolder" class="modal">
467 + <div class="modal-content">
468 + <span class="close" onclick="document.getElementById('modalFolder').style.display='none'">×</span>
469 + <h2>📁 Нова папка</h2>
470 + <form method="post">
471 + <input type="hidden" name="mkdir" value="1">
472 + <input name="name" placeholder="Име на папката" required
473 + style="width:100%;padding:14px;border:2px solid var(--border);border-radius:12px;margin:1rem 0">
474 + <button type="submit">✅ Създай</button>
475 + </form>
476 + </div>
477 + </div>
478 +
479 + <div id="modalTempLink" class="modal">
480 + <div class="modal-content">
481 + <span class="close" onclick="document.getElementById('modalTempLink').style.display='none'">×</span>
482 + <h2>🔗 Временен линк</h2>
483 + <div id="linkResult"></div>
484 + <div style="margin-top:1.5rem;display:flex;gap:12px;flex-wrap:wrap">
485 + <button onclick="setExpiry('1h')" class="btn-success">1 час</button>
486 + <button onclick="setExpiry('24h')">24 часа</button>
487 + <button onclick="setExpiry('7d')" class="btn-warning">7 дни</button>
488 + <button onclick="setExpiry('once')">Еднократно</button>
489 + </div>
490 + </div>
491 + </div>
492 +
493 + <script>
494 + let currentPath = "";
495 +
496 + function generateTempLink(path) {
497 + currentPath = path;
498 + document.getElementById('modalTempLink').style.display = 'flex';
499 + document.getElementById('linkResult').innerHTML = '<p style="text-align:center;color:var(--gray)">Изберете време за валидност...</p>';
368 500 }
369 501
370 - .icon {
371 - font-size: 1.4rem;
372 - transition: transform 0.3s ease;
502 + function setExpiry(time) {
503 + fetch('/t', {
504 + method: 'POST',
505 + body: 'p=' + encodeURIComponent(currentPath) + '&e=' + time,
506 + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
507 + })
508 + .then(r => r.text())
509 + .then(html => {
510 + document.getElementById('linkResult').innerHTML = html;
511 + });
373 512 }
374 513
375 - .file-link:hover .icon {
376 - transform: scale(1.2);
514 + window.onclick = function(event) {
515 + if (event.target.classList.contains('modal')) {
516 + event.target.style.display = 'none';
517 + }
518 + }
519 + </script>
520 + </body>
521 + </html>`
522 +
523 + const htmlEN = `<!DOCTYPE html>
524 + <html lang="en">
525 + <head>
526 + <meta charset="UTF-8">
527 + <meta name="viewport" content="width=device-width,initial-scale=1">
528 + <title>{{.Name}}</title>
529 + <!-- PROFESSIONAL FAVICON -->
530 + <link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHJ4PSIxMiIgZmlsbD0idXJsKCNncmFkKSIvPgogIDxkZWZzPgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3R5bGU9InN0b3AtY29sb3I6IzRGNjBFNTszdG9wLW9wYWNpdHk6MSIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0eWxlPSJzdG9wLWNvbG9yOiMwNkI2RDQ7c3RvcC1vcGFjaXR5OjEiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgPC9kZWZzPgogIDxwYXRoIGQ9Ik0yMCAyMEMyMCAxNy43OTA5IDE3Ljc5MDkgMTYgMTUgMTZDMTIuMjA5MSAxNiAxMCAxNy43OTA5IDEwIDIwQzEwIDIyLjIwOTEgMTIuMjA5MSAyNCAxNSAyNEMxNy43OTA5IDI0IDIwIDIyLjIwOTEgMjAgMjBaIiBmaWxsPSJ3aGl0ZSIvPgogIDxwYXRoIGQ9Ik0zOCAyMEMzOCAxNy43OTA5IDM1Ljc5MDkgMTYgMzMgMTZDMzAuMjA5MSAxNiAyOCAxNy43OTA5IDI4IDIwQzI4IDIyLjIwOTEgMzAuMjA5MSAyNCAzMyAyNEMzNS43OTA5IDI0IDM4IDIyLjIwOTEgMzggMjBaIiBmaWxsPSJ3aGl0ZSIvPgogIDxwYXRoIGQ9Ik01NCAyMEM1NCAxNy43OTA5IDUxLjc5MDkgMTYgNDkgMTZDNDYuMjA5MSAxNiA0NCAxNy43OTA5IDQ0IDIwQzQ0IDIyLjIwOTEgNDYuMjA5MSAyNCA0OSAyNEM1MS43OTA5IDI0IDU0IDIyLjIwOTEgNTQgMjBaIiBmaWxsPSJ3aGl0ZSIvPgogIDxwYXRoIGQ9Ik0yMCA0MEMyMCAzNy43OTA5IDE3Ljc5MDkgMzYgMTUgMzZDMTIuMjA5MSAzNiAxMCAzNy43OTA5IDEwIDQwQzEwIDQyLjIwOTEgMTIuMjA5MSA0NCAxNSA0NEMxNy43OTA5IDQ0IDIwIDQyLjIwOTEgMjAgNDBaIiBmaWxsPSJ3aGl0ZSIvPgogIDxwYXRoIGQ9Ik0zOCA0MEMzOCAzNy43OTA5IDM1Ljc5MDkgMzYgMzMgMzZDMzAuMjA5MSAzNiAyOCAzNy43OTA5IDI4IDQwQzI4IDQyLjIwOTEgMzAuMjA5MSA0NCAzMyA0NEMzNS43OTA5IDQ0IDM4IDQyLjIwOTEgMzggNDBaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K"/>
531 + <style>
532 + :root {
533 + --primary: #4F46E5;
534 + --primary-dark: #4338CA;
535 + --secondary: #06B6D4;
536 + --success: #10B981;
537 + --danger: #EF4444;
538 + --warning: #F59E0B;
539 + --dark: #1E293B;
540 + --light: #F8FAFC;
541 + --gray: #64748B;
542 + --border: #E2E8F0;
377 543 }
378 544
379 - .back-link {
380 - color: var(--primary);
381 - text-decoration: none;
382 - font-weight: 700;
383 - display: flex;
384 - align-items: center;
385 - gap: 10px;
386 - padding: 12px 0;
387 - font-size: 1.1rem;
388 - transition: all 0.3s ease;
545 + * { margin: 0; padding: 0; box-sizing: border-box; }
546 +
547 + body {
548 + font-family: 'Segoe UI', system-ui, sans-serif;
549 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
550 + color: var(--dark);
551 + min-height: 100vh;
552 + padding: 20px;
553 + line-height: 1.6;
389 554 }
390 555
391 - .back-link:hover {
392 - transform: translateX(-5px);
556 + .container {
557 + max-width: 1600px;
558 + margin: 0 auto;
559 + background: white;
560 + border-radius: 20px;
561 + box-shadow: 0 25px 80px rgba(0,0,0,0.15);
562 + overflow: hidden;
393 563 }
394 564
395 - .thumbnail {
396 - width: 90px;
397 - height: 70px;
398 - object-fit: cover;
399 - border-radius: 10px;
400 - border: 3px solid #e2e8f0;
401 - margin-right: 15px;
402 - transition: all 0.3s ease;
403 - box-shadow: 0 4px 12px rgba(0,0,0,0.1);
565 + .header {
566 + background: linear-gradient(135deg, var(--primary), var(--secondary));
567 + color: white;
568 + padding: 3rem 2rem;
569 + text-align: center;
570 + position: relative;
571 + overflow: hidden;
404 572 }
405 573
406 - .thumbnail:hover {
407 - transform: scale(1.05);
408 - border-color: var(--primary);
409 - box-shadow: 0 8px 20px rgba(0,0,0,0.15);
574 + .header::before {
575 + content: '';
576 + position: absolute;
577 + top: 0;
578 + left: 0;
579 + right: 0;
580 + bottom: 0;
581 + background: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23ffffff' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E");
410 582 }
411 583
412 - .file-info {
413 - display: flex;
414 - align-items: center;
584 + .logo {
585 + font-size: 3.5rem;
586 + font-weight: 900;
587 + background: linear-gradient(135deg, #FFF, #E0F2FE);
588 + -webkit-background-clip: text;
589 + -webkit-text-fill-color: transparent;
590 + margin-bottom: 0.5rem;
591 + text-shadow: 0 2px 4px rgba(0,0,0,0.1);
415 592 }
416 593
417 - .file-name {
418 - flex: 1;
594 + .subtitle {
595 + font-size: 1.3rem;
596 + opacity: 0.9;
597 + font-weight: 300;
419 598 }
420 599
421 600 .stats {
601 + padding: 1.5rem;
602 + background: var(--light);
603 + text-align: center;
604 + font-weight: 600;
422 605 display: flex;
606 + justify-content: center;
423 607 gap: 2rem;
424 - padding: 1.5rem 2rem;
425 - background: #f8fafc;
426 - border-bottom: 1px solid var(--border);
427 - font-size: 0.95rem;
428 - color: var(--gray);
608 + flex-wrap: wrap;
429 609 }
430 610
431 611 .stat-item {
432 612 display: flex;
433 613 align-items: center;
434 - gap: 8px;
614 + gap: 0.5rem;
615 + color: var(--gray);
435 616 }
436 617
437 - footer {
438 - text-align: center;
439 - padding: 2.5rem;
440 - color: #94a3b8;
441 - font-size: 1rem;
442 - border-top: 1px solid var(--border);
443 - background: #f8fafc;
618 + .path {
619 + padding: 1.2rem 2rem;
620 + background: var(--light);
621 + border-bottom: 1px solid var(--border);
622 + font-weight: 600;
623 + color: var(--primary);
624 + display: flex;
625 + align-items: center;
626 + gap: 0.5rem;
444 627 }
445 628
446 - .footer-content {
629 + .actions {
630 + padding: 2rem;
631 + background: var(--light);
447 632 display: flex;
448 - justify-content: space-between;
633 + flex-wrap: wrap;
634 + gap: 1.5rem;
449 635 align-items: center;
450 - max-width: 1000px;
451 - margin: 0 auto;
636 + border-bottom: 1px solid var(--border);
452 637 }
453 638
454 - @media (max-width: 768px) {
455 - .actions {
456 - flex-direction: column;
457 - align-items: stretch;
458 - }
459 -
460 - .upload-form {
461 - flex-direction: column;
462 - }
463 -
464 - th, td {
465 - padding: 1.25rem;
466 - }
467 -
468 - .thumbnail {
469 - width: 70px;
470 - height: 55px;
471 - }
472 -
473 - h1 {
474 - font-size: 2.2rem;
475 - }
476 -
477 - .footer-content {
478 - flex-direction: column;
479 - gap: 1rem;
480 - }
639 + .upload-box {
640 + background: white;
641 + padding: 2rem;
642 + border-radius: 16px;
643 + box-shadow: 0 8px 25px rgba(139,92,246,0.1);
644 + border: 2px dashed #DDD6FE;
645 + flex: 1;
646 + min-width: 360px;
647 + transition: all 0.3s ease;
481 648 }
482 649
483 - .loading {
484 - opacity: 0.7;
485 - pointer-events: none;
650 + .upload-box:hover {
651 + border-color: var(--primary);
652 + transform: translateY(-3px);
653 + box-shadow: 0 15px 35px rgba(139,92,246,0.15);
486 654 }
487 655
488 - .pulse {
489 - animation: pulse 2s infinite;
656 + button, .btn {
657 + padding: 14px 28px;
658 + border: none;
659 + border-radius: 12px;
660 + color: white;
661 + background: linear-gradient(135deg, var(--primary), var(--primary-dark));
662 + cursor: pointer;
663 + font-weight: 600;
664 + transition: all 0.3s ease;
665 + display: inline-flex;
666 + align-items: center;
667 + gap: 0.5rem;
668 + text-decoration: none;
490 669 }
491 670
492 - @keyframes pulse {
493 - 0% { opacity: 1; }
494 - 50% { opacity: 0.7; }
495 - 100% { opacity: 1; }
671 + button:hover, .btn:hover {
672 + transform: translateY(-2px);
673 + box-shadow: 0 10px 25px rgba(79,70,229,0.3);
674 + }
675 +
676 + .btn-success {
677 + background: linear-gradient(135deg, var(--success), #059669);
678 + }
679 +
680 + .btn-warning {
681 + background: linear-gradient(135deg, var(--warning), #D97706);
682 + }
683 +
684 + .modal {
685 + display: none;
686 + position: fixed;
687 + top: 0;
688 + left: 0;
689 + right: 0;
690 + bottom: 0;
691 + background: rgba(0,0,0,0.7);
692 + align-items: center;
693 + justify-content: center;
694 + z-index: 1000;
695 + }
696 +
697 + .modal-content {
698 + background: white;
699 + padding: 2.5rem;
700 + border-radius: 20px;
701 + max-width: 500px;
702 + width: 90%;
703 + box-shadow: 0 25px 50px rgba(0,0,0,0.25);
704 + }
705 +
706 + .close {
707 + float: right;
708 + font-size: 2rem;
709 + cursor: pointer;
710 + color: var(--gray);
711 + }
712 +
713 + table {
714 + width: 100%;
715 + border-collapse: collapse;
716 + background: white;
717 + }
718 +
719 + th {
720 + background: var(--light);
721 + padding: 1.5rem;
722 + text-align: left;
723 + font-weight: 700;
724 + color: var(--gray);
725 + border-bottom: 3px solid var(--border);
726 + }
727 +
728 + td {
729 + padding: 1.2rem 1.5rem;
730 + border-bottom: 1px solid var(--border);
731 + }
732 +
733 + tr:hover {
734 + background: #F0F9FF;
735 + }
736 +
737 + .thumbnail {
738 + width: 80px;
739 + height: 60px;
740 + object-fit: cover;
741 + border-radius: 8px;
742 + border: 2px solid var(--border);
743 + cursor: pointer;
744 + }
745 +
746 + .thumbnail:hover {
747 + transform: scale(1.05);
748 + border-color: var(--primary);
749 + }
750 +
751 + .file-icon {
752 + font-size: 1.8rem;
753 + margin-right: 10px;
754 + }
755 +
756 + .folder-icon { color: var(--warning); }
757 + .pdf-icon { color: var(--danger); }
758 + .image-icon { color: var(--success); }
759 + .file-icon-default { color: var(--primary); }
760 +
761 + .link-result {
762 + background: #ECFDF5;
763 + padding: 1.5rem;
764 + border-radius: 12px;
765 + margin: 1.5rem 0;
766 + border: 2px solid var(--success);
767 + }
768 +
769 + .footer {
770 + padding: 2.5rem;
771 + text-align: center;
772 + color: var(--gray);
773 + background: var(--light);
496 774 }
497 775 </style>
498 776 </head>
499 777 <body>
500 - <div class="container">
501 - <header>
502 - <div class="logo-container">
503 - <div class="logo">📁</div>
504 - <h1>{{.ServerName}}</h1>
505 - <div class="subtitle">Професионално споделяне на файлове с миниатюри</div>
778 + <div class="container">
779 + <div class="header">
780 + <div class="logo">{{.Name}}</div>
781 + <div class="subtitle">Temporary links • PDF & Images in folders • ZIP • Thumbnails</div>
782 + </div>
783 +
784 + <div class="stats">
785 + <div class="stat-item">📊 Files: <b>{{.Files}}</b></div>
786 + <div class="stat-item">📁 Folders: <b>{{.Dirs}}</b></div>
787 + <div class="stat-item">🖼️ Images: <b>{{.Imgs}}</b></div>
788 + </div>
789 +
790 + <div class="path">
791 + <span>📂</span>
792 + <span>Path: <b>/{{.Path}}</b></span>
793 + </div>
794 +
795 + <div class="actions">
796 + <div class="upload-box">
797 + <form method="post" enctype="multipart/form-data">
798 + <div style="display:flex;gap:15px;flex-wrap:wrap;align-items:center">
799 + <input type="file" name="f" multiple required
800 + style="flex:1;min-width:250px;padding:14px;border:2px solid var(--border);border-radius:12px">
801 + <button type="submit">📤 Upload</button>
802 + </div>
803 + </form>
804 + </div>
805 +
806 + <button onclick="document.getElementById('modalFolder').style.display='flex'">📁 New Folder</button>
807 + <a href="/zip{{.Zip}}" class="btn btn-success">📦 ZIP</a>
808 + </div>
809 +
810 + <table>
811 + <thead>
812 + <tr>
813 + <th>Name</th>
814 + <th>Size</th>
815 + <th>Date</th>
816 + <th>Actions</th>
817 + </tr>
818 + </thead>
819 + <tbody>
820 + {{if ne .Path "/"}}
821 + <tr>
822 + <td colspan="4">
823 + <a href=".." style="color:var(--primary);font-weight:700;text-decoration:none">⬆️ Back</a>
824 + </td>
825 + </tr>
826 + {{end}}
827 +
828 + {{range .Items}}
829 + <tr>
830 + <td style="display:flex;align-items:center;gap:15px">
831 + {{if .Thumb}}
832 + <img src="{{.Thumb}}" class="thumbnail" onclick="window.open('{{$.Cur}}/{{.Name}}','_blank')">
833 + {{else}}
834 + <div class="file-icon {{if .Dir}}folder-icon{{else if eq .Ext ".pdf"}}pdf-icon{{else if .Img}}image-icon{{else}}file-icon-default{{end}}">
835 + {{if .Dir}}📁{{else if eq .Ext ".pdf"}}📄{{else if .Img}}🖼️{{else}}📄{{end}}
836 + </div>
837 + {{end}}
838 +
839 + <a href="{{if .Dir}}{{.Name}}/{{else}}{{.Name}}{{end}}"
840 + style="color:var(--dark);text-decoration:none;font-weight:500">
841 + <span style="font-weight:600">{{.Name}}</span>
842 + </a>
843 + </td>
844 + <td>{{.Size}}</td>
845 + <td>{{.Date}}</td>
846 + <td>
847 + {{if not .Dir}}
848 + <button onclick="generateTempLink('{{$.Cur}}/{{.Name}}')">🔗 Temporary Link</button>
849 + {{else}}
850 + <span style="color:var(--gray)">—</span>
851 + {{end}}
852 + </td>
853 + </tr>
854 + {{end}}
855 + </tbody>
856 + </table>
857 +
858 + <div class="footer">
859 + © 2025 {{.Name}} | Full-featured file server
506 860 </div>
507 - </header>
508 -
509 - <div class="stats">
510 - <div class="stat-item">📊 Общо файлове: <strong>{{.FileCount}}</strong></div>
511 - <div class="stat-item">📁 Папки: <strong>{{.DirCount}}</strong></div>
512 - <div class="stat-item">🖼️ Картинки: <strong>{{.ImageCount}}</strong></div>
513 - </div>
514 -
515 - <div class="path-bar">
516 - Път: <strong>/{{.Current}}</strong>
517 861 </div>
518 -
519 - <div class="actions">
520 - <div class="upload-box">
521 - <form method="post" enctype="multipart/form-data" class="upload-form" id="uploadForm">
522 - <input type="file" name="f" required class="file-input" id="fileInput">
523 - <button type="submit" id="uploadBtn">📤 Качи файл</button>
862 +
863 + <div id="modalFolder" class="modal">
864 + <div class="modal-content">
865 + <span class="close" onclick="document.getElementById('modalFolder').style.display='none'">×</span>
866 + <h2>📁 New Folder</h2>
867 + <form method="post">
868 + <input type="hidden" name="mkdir" value="1">
869 + <input name="name" placeholder="Folder name" required
870 + style="width:100%;padding:14px;border:2px solid var(--border);border-radius:12px;margin:1rem 0">
871 + <button type="submit">✅ Create</button>
524 872 </form>
525 873 </div>
526 - <a href="/zip{{.CurrentPath}}" class="btn btn-success">📦 Изтегли като ZIP</a>
527 874 </div>
528 -
529 - <table class="file-table">
530 - <thead>
531 - <tr>
532 - <th>Име</th>
533 - <th class="size-col">Размер</th>
534 - <th class="date-col">Дата</th>
535 - </tr>
536 - </thead>
537 - <tbody>
538 - {{if ne .Current "/"}}
539 - <tr>
540 - <td colspan="3">
541 - <a href=".." class="back-link">⬆️ Назад</a>
542 - </td>
543 - </tr>
544 - {{end}}
545 - {{range .Files}}
546 - <tr>
547 - <td>
548 - {{if .IsDir}}
549 - <div class="file-info">
550 - <span class="folder">📁 {{.Name}}</span>
551 - <a href="{{.Name}}/" class="file-link">[Отвори]</a>
552 - </div>
553 - {{else}}
554 - <div class="file-info">
555 - {{if .Thumbnail}}
556 - <img src="{{.Thumbnail}}" alt="{{.Name}}" class="thumbnail"
557 - onerror="this.style.display='none'"
558 - onclick="window.open('{{$.CurrentPath}}/{{.Name}}', '_blank')"
559 - style="cursor: zoom-in;">
560 - {{end}}
561 - <div class="file-name">
562 - <a href="{{.Name}}" class="file-link">
563 - <span class="icon">{{if .Thumbnail}}🖼️{{else}}📄{{end}}</span>
564 - {{.Name}}
565 - </a>
566 - </div>
567 - </div>
568 - {{end}}
569 - </td>
570 - <td class="size-col">{{.Size}}</td>
571 - <td class="date-col">{{.ModTime}}</td>
572 - </tr>
573 - {{end}}
574 - </tbody>
575 - </table>
576 -
577 - <footer>
578 - <div class="footer-content">
579 - <div>© 2024 {{.ServerName}} - Професионален файлов сървър</div>
580 - <div>🔄 {{.CurrentTime}}</div>
875 +
876 + <div id="modalTempLink" class="modal">
877 + <div class="modal-content">
878 + <span class="close" onclick="document.getElementById('modalTempLink').style.display='none'">×</span>
879 + <h2>🔗 Temporary Link</h2>
880 + <div id="linkResult"></div>
881 + <div style="margin-top:1.5rem;display:flex;gap:12px;flex-wrap:wrap">
882 + <button onclick="setExpiry('1h')" class="btn-success">1 hour</button>
883 + <button onclick="setExpiry('24h')">24 hours</button>
884 + <button onclick="setExpiry('7d')" class="btn-warning">7 days</button>
885 + <button onclick="setExpiry('once')">One-time</button>
886 + </div>
581 887 </div>
582 - </footer>
583 - </div>
888 + </div>
584 889
585 - <script>
586 - document.addEventListener('DOMContentLoaded', function() {
587 - const thumbnails = document.querySelectorAll('.thumbnail');
588 - thumbnails.forEach(thumb => {
589 - thumb.addEventListener('error', function() {
590 - this.style.display = 'none';
591 - });
592 - });
593 -
594 - const fileInput = document.querySelector('#fileInput');
595 - const uploadForm = document.querySelector('#uploadForm');
596 - const uploadBtn = document.querySelector('#uploadBtn');
597 -
598 - fileInput.addEventListener('change', function() {
599 - if (this.files[0] && this.files[0].size > 100 * 1024 * 1024) {
600 - if (!confirm('Файлът е голям (' + (this.files[0].size / (1024*1024)).toFixed(1) + 'MB). Сигурни ли сте, че искате да го качите?')) {
601 - this.value = '';
890 + <script>
891 + let currentPath = "";
892 +
893 + function generateTempLink(path) {
894 + currentPath = path;
895 + document.getElementById('modalTempLink').style.display = 'flex';
896 + document.getElementById('linkResult').innerHTML = '<p style="text-align:center;color:var(--gray)">Select expiration time...</p>';
897 + }
898 +
899 + function setExpiry(time) {
900 + fetch('/t', {
901 + method: 'POST',
902 + body: 'p=' + encodeURIComponent(currentPath) + '&e=' + time,
903 + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
904 + })
905 + .then(r => r.text())
906 + .then(html => {
907 + document.getElementById('linkResult').innerHTML = html;
908 + });
909 + }
910 +
911 + window.onclick = function(event) {
912 + if (event.target.classList.contains('modal')) {
913 + event.target.style.display = 'none';
602 914 }
603 915 }
604 - });
605 -
606 - uploadForm.addEventListener('submit', function(e) {
607 - uploadBtn.innerHTML = '⏳ Качване...';
608 - uploadBtn.classList.add('loading', 'pulse');
609 - uploadBtn.disabled = true;
610 - });
611 -
612 - // Add some interactive effects
613 - const rows = document.querySelectorAll('tr');
614 - rows.forEach(row => {
615 - row.addEventListener('mouseenter', function() {
616 - this.style.transition = 'all 0.3s ease';
617 - });
618 - });
619 - });
620 - </script>
916 + </script>
621 917 </body>
622 918 </html>`
623 919
624 - type File struct {
625 - Name string
626 - Size string
627 - ModTime string
628 - IsDir bool
629 - Thumbnail string
920 + type Item struct {
921 + Name, Size, Date, Ext, Thumb string
922 + Dir, Img bool
630 923 }
631 924
632 - type PageData struct {
633 - Files []File
634 - Current string
635 - CurrentPath string
636 - ServerName string
637 - FileCount int
638 - DirCount int
639 - ImageCount int
640 - CurrentTime string
925 + type Data struct {
926 + Name, Path, Cur, Zip string
927 + Items []Item
928 + Files, Dirs, Imgs int
929 + }
930 +
931 + func isPathSafe(p string) bool {
932 + cleanedPath := filepath.Clean(p)
933 + return strings.HasPrefix(cleanedPath, absFileDir)
641 934 }
642 935
643 936 func main() {
644 - log.Printf("Стартиране на %s на порт %s за директория %s", serverName, port, fileDir)
645 -
937 + // Clean expired links
938 + go func() {
939 + for range time.Tick(10 * time.Minute) {
940 + now := time.Now()
941 + for k, v := range links {
942 + if now.After(v.Expiry) {
943 + delete(links, k)
944 + }
945 + }
946 + }
947 + }()
948 +
646 949 http.HandleFunc("/", handler)
647 - http.HandleFunc("/zip", zipHandler)
648 - http.HandleFunc("/thumb/", thumbHandler)
950 + http.HandleFunc("/t", tempHandler)
951 + http.Handle("/thumb/", http.StripPrefix("/thumb/", http.HandlerFunc(thumb)))
649 952
650 - log.Printf("Сървърът е готов на http://0.0.0.0:%s", port)
953 + log.Printf("🚀 Server '%s' starting on port %s", serverName, port)
954 + log.Printf("📁 Directory: %s", absFileDir)
651 955 log.Fatal(http.ListenAndServe(":"+port, nil))
652 956 }
653 957
654 958 func handler(w http.ResponseWriter, r *http.Request) {
655 - if strings.HasPrefix(r.URL.Path, "/zip") {
656 - zipHandler(w, r)
959 + if strings.HasPrefix(r.URL.Path, "/temp/") {
960 + serveTemp(w, r)
657 961 return
658 962 }
659 -
660 963 if strings.HasPrefix(r.URL.Path, "/thumb/") {
661 - thumbHandler(w, r)
964 + thumb(w, r)
662 965 return
663 966 }
664 -
665 - reqPath := filepath.Clean(r.URL.Path)
666 - if reqPath == "/" || reqPath == "." {
967 +
968 + reqPath := path.Clean(r.URL.Path)
969 + if reqPath == "/" {
667 970 reqPath = ""
668 971 }
669 - fullPath := filepath.Join(fileDir, reqPath)
972 +
973 + full := filepath.Join(fileDir, filepath.FromSlash(reqPath))
670 974
671 - if !strings.HasPrefix(fullPath, fileDir) {
672 - http.Error(w, "Достъпът е отказан", http.StatusForbidden)
975 + if !isPathSafe(full) {
976 + http.Error(w, "Forbidden: Path Traversal", 403)
673 977 return
674 978 }
675 979
676 - fi, err := os.Stat(fullPath)
677 - if err != nil {
678 - http.NotFound(w, r)
679 - return
680 - }
681 -
682 - if fi.IsDir() {
683 - if r.Method == "POST" {
684 - upload(w, r, fullPath)
980 + if r.Method == "POST" {
981 + if r.FormValue("mkdir") != "" {
982 + name := strings.TrimSpace(r.FormValue("name"))
983 + if name != "" && !strings.ContainsAny(name, "/\\") {
984 + os.Mkdir(filepath.Join(full, name), 0755)
985 + }
986 + http.Redirect(w, r, r.URL.Path, 303)
685 987 return
686 988 }
687 - listDir(w, reqPath, fullPath)
688 - return
689 - }
690 989
691 - http.ServeFile(w, r, fullPath)
692 - }
693 -
694 - func upload(w http.ResponseWriter, r *http.Request, dir string) {
695 - if err := r.ParseMultipartForm(500 << 20); err != nil {
696 - http.Error(w, "Файлът е твърде голям (максимум 500MB)", http.StatusBadRequest)
990 + if err := r.ParseMultipartForm(500 << 20); err != nil {
991 + http.Error(w, "Upload error: "+err.Error(), 500)
992 + return
993 + }
994 +
995 + for _, h := range r.MultipartForm.File["f"] {
996 + f, err := h.Open()
997 + if err != nil {
998 + continue
999 + }
1000 + defer f.Close()
1001 +
1002 + ext := strings.ToLower(filepath.Ext(h.Filename))
1003 + dest := full
1004 +
1005 + // Auto-sorting
1006 + if ext == ".pdf" {
1007 + dest = filepath.Join(fileDir, "pdf")
1008 + os.MkdirAll(dest, 0755)
1009 + } else if strings.Contains(".jpg.jpeg.png.gif.webp", ext) {
1010 + dest = filepath.Join(fileDir, "images")
1011 + os.MkdirAll(dest, 0755)
1012 + }
1013 +
1014 + out, err := os.Create(filepath.Join(dest, h.Filename))
1015 + if err != nil {
1016 + continue
1017 + }
1018 + defer out.Close()
1019 +
1020 + io.Copy(out, f)
1021 + }
1022 + http.Redirect(w, r, r.URL.Path, 303)
697 1023 return
698 1024 }
699 1025
700 - file, header, err := r.FormFile("f")
1026 + // Show files
1027 + entries, err := os.ReadDir(full)
701 1028 if err != nil {
702 - http.Error(w, "Грешка при качване", http.StatusBadRequest)
1029 + if os.IsNotExist(err) {
1030 + http.ServeFile(w, r, full)
1031 + } else {
1032 + http.Error(w, "Read error", 500)
1033 + }
703 1034 return
704 1035 }
705 - defer file.Close()
706 1036
707 - filePath := filepath.Join(dir, header.Filename)
708 - dst, err := os.Create(filePath)
709 - if err != nil {
710 - http.Error(w, "Грешка при създаване на файл", http.StatusInternalServerError)
711 - return
712 - }
713 - defer dst.Close()
1037 + var items []Item
1038 + files, dirs, imgs := 0, 0, 0
714 1039
715 - if _, err := io.Copy(dst, file); err != nil {
716 - http.Error(w, "Грешка при запис на файл", http.StatusInternalServerError)
717 - return
718 - }
1040 + // ZIP functionality
1041 + if strings.HasPrefix(reqPath, "/zip/") {
1042 + zipPath := filepath.Join(fileDir, filepath.FromSlash(strings.TrimPrefix(reqPath, "/zip")))
1043 + if !isPathSafe(zipPath) {
1044 + http.Error(w, "Forbidden", 403)
1045 + return
1046 + }
1047 +
1048 + w.Header().Set("Content-Type", "application/zip")
1049 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", filepath.Base(zipPath)))
719 1050
720 - http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
721 - }
1051 + archive := zip.NewWriter(w)
1052 + defer archive.Close()
722 1053
723 - func listDir(w http.ResponseWriter, relPath, fullPath string) {
724 - entries, err := os.ReadDir(fullPath)
725 - if err != nil {
726 - http.Error(w, "Грешка при четене на директория", http.StatusInternalServerError)
1054 + filepath.Walk(zipPath, func(p string, info os.FileInfo, err error) error {
1055 + if err != nil {
1056 + return err
1057 + }
1058 +
1059 + if p == zipPath {
1060 + return nil
1061 + }
1062 +
1063 + relPath, err := filepath.Rel(zipPath, p)
1064 + if err != nil {
1065 + return err
1066 + }
1067 +
1068 + if info.IsDir() {
1069 + _, err = archive.Create(relPath + "/")
1070 + return err
1071 + }
1072 +
1073 + file, err := os.Open(p)
1074 + if err != nil {
1075 + return err
1076 + }
1077 + defer file.Close()
1078 +
1079 + writer, err := archive.Create(relPath)
1080 + if err != nil {
1081 + return err
1082 + }
1083 +
1084 + _, err = io.Copy(writer, file)
1085 + return err
1086 + })
727 1087 return
728 1088 }
729 1089
730 - var files []File
731 - fileCount := 0
732 - dirCount := 0
733 - imageCount := 0
734 -
735 1090 for _, e := range entries {
736 1091 info, err := e.Info()
737 1092 if err != nil {
738 1093 continue
739 1094 }
740 -
741 - size := "📁 папка"
742 - if !e.IsDir() {
743 - fileCount++
744 - s := info.Size()
1095 +
1096 + item := Item{Name: e.Name(), Date: info.ModTime().Format("02.01.2006 15:04"), Dir: e.IsDir()}
1097 + if e.IsDir() {
1098 + dirs++
1099 + } else {
1100 + files++
1101 + size := info.Size()
745 1102 switch {
746 - case s < 1024:
747 - size = fmt.Sprintf("%d Б", s)
748 - case s < 1048576:
749 - size = fmt.Sprintf("%.1f КБ", float64(s)/1024)
750 - case s < 1073741824:
751 - size = fmt.Sprintf("%.1f МБ", float64(s)/1048576)
1103 + case size < 1024:
1104 + item.Size = fmt.Sprintf("%d B", size)
1105 + case size < 1024*1024:
1106 + item.Size = fmt.Sprintf("%.1f KB", float64(size)/1024)
1107 + case size < 1024*1024*1024:
1108 + item.Size = fmt.Sprintf("%.1f MB", float64(size)/(1024*1024))
752 1109 default:
753 - size = fmt.Sprintf("%.1f ГБ", float64(s)/1073741824)
1110 + item.Size = fmt.Sprintf("%.1f GB", float64(size)/(1024*1024*1024))
1111 + }
1112 + ext := strings.ToLower(filepath.Ext(e.Name()))
1113 + item.Ext = ext
1114 + if strings.Contains(".jpg.jpeg.png.gif.webp", ext) {
1115 + imgs++
1116 + item.Img = true
1117 + item.Thumb = "/thumb" + path.Join("/", reqPath, e.Name())
754 1118 }
755 - } else {
756 - dirCount++
757 - }
758 -
759 - thumbnail := ""
760 - if !e.IsDir() && isImageFile(e.Name()) {
761 - imageCount++
762 - thumbnail = fmt.Sprintf("/thumb%s/%s", relPath, e.Name())
763 1119 }
764 -
765 - files = append(files, File{
766 - Name: e.Name(),
767 - Size: size,
768 - ModTime: info.ModTime().Format("02.01.2006 15:04"),
769 - IsDir: e.IsDir(),
770 - Thumbnail: thumbnail,
771 - })
1120 + items = append(items, item)
772 1121 }
773 1122
774 - sort.Slice(files, func(i, j int) bool {
775 - if files[i].IsDir != files[j].IsDir {
776 - return files[i].IsDir
1123 + sort.Slice(items, func(i, j int) bool {
1124 + if items[i].Dir != items[j].Dir {
1125 + return items[i].Dir
777 1126 }
778 - return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
1127 + return strings.ToLower(items[i].Name) < strings.ToLower(items[j].Name)
779 1128 })
780 1129
781 - current := relPath
782 - if current == "" {
783 - current = "/"
1130 + data := Data{
1131 + Name: serverName,
1132 + Path: reqPath,
1133 + Cur: "/" + reqPath,
1134 + Zip: reqPath,
1135 + Items: items,
1136 + Files: files,
1137 + Dirs: dirs,
1138 + Imgs: imgs,
784 1139 }
785 1140
786 - currentPath := relPath
787 - if currentPath != "" && !strings.HasPrefix(currentPath, "/") {
788 - currentPath = "/" + currentPath
789 - }
790 -
791 - data := PageData{
792 - Files: files,
793 - Current: current,
794 - CurrentPath: currentPath,
795 - ServerName: serverName,
796 - FileCount: fileCount,
797 - DirCount: dirCount,
798 - ImageCount: imageCount,
799 - CurrentTime: time.Now().Format("02.01.2006 15:04:05"),
800 - }
801 -
802 - if err := tmpl.Execute(w, data); err != nil {
803 - http.Error(w, "Грешка при генериране на страница", http.StatusInternalServerError)
804 - }
1141 + tmpl.Execute(w, data)
805 1142 }
806 1143
807 - func thumbHandler(w http.ResponseWriter, r *http.Request) {
808 - path := strings.TrimPrefix(r.URL.Path, "/thumb")
809 - fullPath := filepath.Join(fileDir, filepath.Clean(path))
810 -
811 - if !strings.HasPrefix(fullPath, fileDir) {
812 - http.Error(w, "Достъпът е отказан", http.StatusForbidden)
813 - return
814 - }
815 -
816 - if _, err := os.Stat(fullPath); os.IsNotExist(err) || !isImageFile(fullPath) {
817 - http.NotFound(w, r)
1144 + func thumb(w http.ResponseWriter, r *http.Request) {
1145 + p := filepath.Join(fileDir, filepath.FromSlash(strings.TrimPrefix(r.URL.Path, "/thumb")))
1146 +
1147 + if !isPathSafe(p) {
818 1148 return
819 1149 }
820 -
821 - generateThumbnail(w, fullPath)
822 - }
823 -
824 - func generateThumbnail(w http.ResponseWriter, filePath string) {
825 - file, err := os.Open(filePath)
1150 +
1151 + f, err := os.Open(p)
826 1152 if err != nil {
827 - http.Error(w, "Грешка при отваряне на файл", http.StatusInternalServerError)
828 1153 return
829 1154 }
830 - defer file.Close()
831 -
832 - img, _, err := image.Decode(file)
1155 + defer f.Close()
1156 +
1157 + img, _, err := image.Decode(f)
833 1158 if err != nil {
834 - http.Error(w, "Невалидно изображение", http.StatusInternalServerError)
835 1159 return
836 1160 }
837 -
838 - bounds := img.Bounds()
839 - width := bounds.Dx()
840 - height := bounds.Dy()
841 -
842 - newWidth := 90
843 - newHeight := 70
844 -
845 - if width > height {
846 - newHeight = int(float64(height) * float64(newWidth) / float64(width))
1161 +
1162 + b := img.Bounds()
1163 + nw, nh := 100, 80
1164 + if b.Dx() > b.Dy() {
1165 + nh = b.Dy() * 100 / b.Dx()
847 1166 } else {
848 - newWidth = int(float64(width) * float64(newHeight) / float64(height))
1167 + nw = b.Dx() * 80 / b.Dy()
849 1168 }
850 -
851 - dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
852 -
853 - for y := 0; y < newHeight; y++ {
854 - for x := 0; x < newWidth; x++ {
855 - srcX := x * width / newWidth
856 - srcY := y * height / newHeight
857 - dst.Set(x, y, img.At(srcX, srcY))
1169 + resized := image.NewRGBA(image.Rect(0, 0, nw, nh))
1170 + for y := 0; y < nh; y++ {
1171 + for x := 0; x < nw; x++ {
1172 + resized.Set(x, y, img.At(x*b.Dx()/nw, y*b.Dy()/nh))
858 1173 }
859 1174 }
860 -
1175 +
861 1176 w.Header().Set("Content-Type", "image/jpeg")
862 - if err := jpeg.Encode(w, dst, &jpeg.Options{Quality: 85}); err != nil {
863 - http.Error(w, "Грешка при кодиране на миниатюра", http.StatusInternalServerError)
1177 + jpeg.Encode(w, resized, &jpeg.Options{Quality: 85})
1178 + }
1179 +
1180 + func tempHandler(w http.ResponseWriter, r *http.Request) {
1181 + if r.Method != "POST" {
1182 + http.Error(w, "Method not allowed", 405)
1183 + return
1184 + }
1185 +
1186 + if err := r.ParseForm(); err != nil {
1187 + http.Error(w, "Parse error", 500)
1188 + return
1189 + }
1190 +
1191 + p := r.FormValue("p")
1192 + e := r.FormValue("e")
1193 +
1194 + cleanedPath := filepath.Clean(filepath.Join(fileDir, filepath.FromSlash(strings.TrimPrefix(p, "/"))))
1195 +
1196 + if !isPathSafe(cleanedPath) {
1197 + http.Error(w, "Invalid path", 400)
864 1198 return
865 1199 }
866 - }
867 1200
868 - func isImageFile(filename string) bool {
869 - ext := strings.ToLower(filepath.Ext(filename))
870 - imageExts := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".svg"}
871 -
872 - for _, imgExt := range imageExts {
873 - if ext == imgExt {
874 - return true
875 - }
1201 + var exp time.Time
1202 + once := false
1203 + switch e {
1204 + case "1h":
1205 + exp = time.Now().Add(1 * time.Hour)
1206 + case "24h":
1207 + exp = time.Now().Add(24 * time.Hour)
1208 + case "7d":
1209 + exp = time.Now().Add(7 * 24 * time.Hour)
1210 + case "once":
1211 + once = true
1212 + exp = time.Now().Add(30 * 24 * time.Hour)
1213 + default:
1214 + exp = time.Now().Add(24 * time.Hour)
1215 + }
1216 +
1217 + tok := make([]byte, 16)
1218 + rand.Read(tok)
1219 + token := hex.EncodeToString(tok)
1220 +
1221 + links[token] = struct {
1222 + Path string
1223 + Expiry time.Time
1224 + OneTime bool
1225 + Used bool
1226 + }{
1227 + Path: cleanedPath,
1228 + Expiry: exp,
1229 + OneTime: once,
876 1230 }
877 - return false
1231 +
1232 + link := fmt.Sprintf("http://%s/temp/%s", r.Host, token)
1233 + fmt.Fprintf(w, `<div class="link-result">Done! Valid until <b>%s</b><br><br>
1234 + <input value="%s" readonly style="width:100%%;padding:12px;border-radius:8px;border:1px solid #10B981" onclick="this.select()">
1235 + <br><small>Ctrl+C to copy</small></div>`, exp.Format("02.01.2006 15:04"), link)
878 1236 }
879 1237
880 - func zipHandler(w http.ResponseWriter, r *http.Request) {
881 - path := strings.TrimPrefix(r.URL.Path, "/zip")
882 - if path == "" {
883 - path = "/"
884 - }
1238 + func serveTemp(w http.ResponseWriter, r *http.Request) {
1239 + token := strings.TrimPrefix(r.URL.Path, "/temp/")
1240 + l, ok := links[token]
885 1241
886 - fullPath := filepath.Join(fileDir, filepath.Clean(path))
887 -
888 - if !strings.HasPrefix(fullPath, fileDir) {
889 - http.Error(w, "Достъпът е отказан", http.StatusForbidden)
1242 + if !ok || time.Now().After(l.Expiry) || (l.OneTime && l.Used) {
1243 + http.Error(w, "Link expired", 410)
890 1244 return
891 1245 }
892 1246
893 - if _, err := os.Stat(fullPath); os.IsNotExist(err) {
894 - http.NotFound(w, r)
1247 + if !isPathSafe(l.Path) {
1248 + http.Error(w, "Invalid link", 403)
895 1249 return
896 1250 }
897 -
898 - w.Header().Set("Content-Type", "application/zip")
899 - filename := filepath.Base(fullPath)
900 - if filename == "" || filename == "." || filename == "/" {
901 - filename = "files"
902 - }
903 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", filename))
904 -
905 - zw := zip.NewWriter(w)
906 - defer zw.Close()
907 -
908 - filepath.Walk(fullPath, func(filePath string, info os.FileInfo, err error) error {
909 - if err != nil {
910 - return err
911 - }
912 -
913 - relPath, err := filepath.Rel(fullPath, filePath)
914 - if err != nil {
915 - return err
916 - }
917 -
918 - if relPath == "." {
919 - return nil
920 - }
921 -
922 - header, err := zip.FileInfoHeader(info)
923 - if err != nil {
924 - return err
925 - }
926 -
927 - header.Name = relPath
928 -
929 - if info.IsDir() {
930 - header.Name += "/"
931 - } else {
932 - header.Method = zip.Deflate
933 - }
934 -
935 - writer, err := zw.CreateHeader(header)
936 - if err != nil {
937 - return err
938 - }
939 -
940 - if !info.IsDir() {
941 - file, err := os.Open(filePath)
942 - if err != nil {
943 - return err
944 - }
945 - defer file.Close()
946 -
947 - _, err = io.Copy(writer, file)
948 - if err != nil {
949 - return err
950 - }
1251 +
1252 + if l.OneTime {
1253 + links[token] = struct {
1254 + Path string
1255 + Expiry time.Time
1256 + OneTime bool
1257 + Used bool
1258 + }{
1259 + Path: l.Path,
1260 + Expiry: l.Expiry,
1261 + OneTime: true,
1262 + Used: true,
951 1263 }
952 -
953 - return nil
954 - })
1264 + }
1265 +
1266 + http.ServeFile(w, r, l.Path)
955 1267 }
956 1268 EOF
957 1269
958 - log "Компилиране на професионалната версия..."
959 - CGO_ENABLED=0 go build -ldflags="-s -w" -o fedya-server || error "Грешка при компилация!"
1270 + # Създаване на Go модул и компилиране
1271 + echo 'module fedya' > go.mod
1272 + echo -e "\e[1;33m🔨 Compiling server...\e[0m"
960 1273
961 - install -m 755 fedya-server /usr/local/bin/fedya-server
1274 + if ! CGO_ENABLED=0 go build -ldflags="-s -w" -o fedya-pro 2>&1; then
1275 + echo -e "\e[1;31m❌ Compilation error!\e[0m"
1276 + exit 1
1277 + fi
962 1278
963 - # Спиране на старите услуги
964 - systemctl stop file-server sabork-server fedya-server 2>/dev/null || true
965 - systemctl disable file-server sabork-server 2>/dev/null || true
1279 + # Инсталиране на изпълнимия файл
1280 + install -m 755 fedya-pro /usr/local/bin/fedya-pro
966 1281
967 - if $auto_start; then
968 - log "Конфигуриране на systemd услуга..."
969 -
970 - cat > /etc/systemd/system/fedya-server.service <<EOF
1282 + # Спиране на старата услуга
1283 + systemctl stop fedya-pro 2>/dev/null || true
1284 +
1285 + if $SERVICE; then
1286 + # Създаване на systemd услуга
1287 + cat > /etc/systemd/system/fedya-pro.service <<EOF
971 1288 [Unit]
972 - Description=$server_name - Professional File Server
1289 + Description=$NAME - Full-featured file server
973 1290 After=network.target
974 - StartLimitIntervalSec=0
975 1291
976 1292 [Service]
977 1293 Type=simple
978 - Environment=FILE_DIR=$file_dir
979 - Environment=PORT=$port
980 - Environment=SERVER_NAME="$server_name"
981 - ExecStart=/usr/local/bin/fedya-server
982 - WorkingDirectory=$file_dir
1294 + Environment=FILE_DIR=$DIR
1295 + Environment=PORT=$PORT
1296 + Environment=SERVER_NAME=$NAME
1297 + Environment=LANG=$LANG
1298 + ExecStart=/usr/local/bin/fedya-pro
1299 + WorkingDirectory=$DIR
983 1300 Restart=always
984 1301 RestartSec=3
1302 + User=root
985 1303
986 1304 [Install]
987 1305 WantedBy=multi-user.target
988 1306 EOF
989 1307
990 1308 systemctl daemon-reload
991 - systemctl enable fedya-server >/dev/null 2>&1
1309 + systemctl enable fedya-pro
1310 + systemctl start fedya-pro
992 1311
993 - if systemctl start fedya-server; then
994 - log "Услугата $server_name е стартирана успешно!"
995 - sleep 2
996 - log "Статус на услугата:"
997 - systemctl status fedya-server --no-pager -l
998 - else
999 - error "Неуспешно стартиране на услугата. Провери: journalctl -u fedya-server -f"
1000 - fi
1312 + echo -e "\e[1;32m✅ Service is active!\e[0m"
1313 + sleep 2
1314 + echo -e "\e[1;33m📊 Service status:\e[0m"
1315 + systemctl status fedya-pro --no-pager -l
1001 1316 else
1002 - log "Ръчно стартиране:"
1003 - echo "FILE_DIR='$file_dir' PORT='$port' SERVER_NAME='$server_name' /usr/local/bin/fedya-server"
1317 + echo -e "\n\e[1;33m📝 Manual start:\e[0m"
1318 + echo -e "\e[1;36mFILE_DIR='$DIR' PORT='$PORT' SERVER_NAME='$NAME' LANG='$LANG' /usr/local/bin/fedya-pro\e[0m"
1004 1319 fi
1005 1320
1006 - rm -rf "$tmp"
1321 + # Финална информация
1007 1322 IP=$(hostname -I | awk '{print $1}')
1008 - log "ГОТОВО! Отвори: http://$IP:$port"
1009 - log "🎉 Професионален дизайн • Цветно лого • Фавикон • Статистики"
1010 -
1011 - if $auto_start; then
1012 - echo
1013 - log "Команди за управление:"
1014 - echo " systemctl status fedya-server"
1015 - echo " journalctl -u fedya-server -f"
1016 - echo " systemctl restart fedya-server"
1017 - fi
1018 -
1019 1323 echo
1020 - log "💼 Сървърът е готов за колегите!"
1021 - echo " 🌐 Може да го споделиш на: http://$IP:$port"
1022 - echo " 📊 Има статистики за файлове, папки и картинки"
1023 - echo " 🎨 Професионален дизайн с анимации и ефекти"
1024 - echo " 🔒 Безопасен и стабилен за корпоративна среда"
1324 + echo -e "\e[1;32m🎉 FEDYA FILE SERVER IS READY!\e[0m"
1325 + echo
1326 + echo -e "\e[1;36m🌐 Access the server:\e[0m"
1327 + echo -e " \e[1;33mhttp://$IP:$PORT\e[0m"
1328 + echo
1329 + echo -e "\e[1;35m✅ Now you have:\e[0m"
1330 + echo -e " 🎯 Professional favicon"
1331 + echo -e " 📤 File upload"
1332 + echo -e " 📂 Auto-sorting (PDF/images)"
1333 + echo -e " 🖼️ Image thumbnails"
1334 + echo -e " 🔗 Temporary links"
1335 + echo -e " 📦 ZIP archiving"
1336 + echo -e " 📁 Folder creation"
1337 + echo -e " 🎨 Premium design"
1338 + echo -e " 🌍 Multi-language support ($LANG)"
1339 + echo
1340 + echo -e "\e[1;33m👍 All features working properly!\e[0m"
1341 +
1342 + # Почистване
1343 + rm -rf "$TMP"

urocibg revised this gist 1764441555. Go to revision

1 file changed, 1024 insertions

fedya-server.sh(file created)

@@ -0,0 +1,1024 @@
1 + #!/bin/bash
2 + #
3 + # Автор: Федя Серафиев / urocibg.eu
4 +
5 + RED='\033[0;31m'
6 + GREEN='\033[0;32m'
7 + YELLOW='\033[1;33m'
8 + NC='\033[0m'
9 +
10 + log() { echo -e "${GREEN}[ИНФО]${NC} $1"; }
11 + warn() { echo -e "${YELLOW}[ПРЕДУПРЕЖДЕНИЕ]${NC} $1"; }
12 + error() { echo -e "${RED}[ГРЕШКА]${NC} $1"; exit 1; }
13 +
14 + [ "$EUID" -ne 0 ] && error "Стартирай със sudo!"
15 +
16 + log "Fedya's File Server – Професионална версия с красив дизайн"
17 +
18 + read -p "Директория за файловете [/var/www/files]: " file_dir
19 + file_dir=${file_dir:-"/var/www/files"}
20 + mkdir -p "$file_dir"
21 +
22 + read -p "Порт [8080]: " port
23 + port=${port:-"8080"}
24 +
25 + read -p "Име на сървъра [Fedya File Server]: " server_name
26 + server_name=${server_name:-"Fedya File Server"}
27 +
28 + read -p "Стартиране като услуга? [Y/n]: " svc
29 + [[ "$svc" =~ ^[Nn]$ ]] && auto_start=false || auto_start=true
30 +
31 + log "Инсталиране на Go..."
32 + command -v go >/dev/null || { apt update && apt install -y golang-go; }
33 +
34 + tmp=$(mktemp -d)
35 + cd "$tmp"
36 +
37 + go mod init fedya-files >/dev/null 2>&1
38 +
39 + cat > main.go <<'EOF'
40 + package main
41 +
42 + import (
43 + "archive/zip"
44 + "fmt"
45 + "html/template"
46 + "image"
47 + "image/jpeg"
48 + _ "image/png"
49 + _ "image/gif"
50 + "io"
51 + "log"
52 + "net/http"
53 + "os"
54 + "path/filepath"
55 + "sort"
56 + "strings"
57 + "time"
58 + )
59 +
60 + var (
61 + fileDir = getEnv("FILE_DIR", "/var/www/files")
62 + port = getEnv("PORT", "8080")
63 + serverName = getEnv("SERVER_NAME", "Fedya File Server")
64 + tmpl = template.Must(template.New("index").Parse(htmlTemplate))
65 + )
66 +
67 + func getEnv(key, defaultValue string) string {
68 + if value := os.Getenv(key); value != "" {
69 + return value
70 + }
71 + return defaultValue
72 + }
73 +
74 + const htmlTemplate = `<!DOCTYPE html>
75 + <html lang="bg">
76 + <head>
77 + <meta charset="UTF-8">
78 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
79 + <title>{{.ServerName}}</title>
80 + <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><defs><linearGradient id='grad' x1='0%' y1='0%' x2='100%' y2='100%'><stop offset='0%' style='stop-color:%233b82f6;stop-opacity:1' /><stop offset='100%' style='stop-color:%231e40af;stop-opacity:1' /></linearGradient></defs><circle cx='50' cy='50' r='45' fill='url(%23grad)'/><rect x='30' y='30' width='40' height='30' rx='5' fill='white'/><rect x='35' y='45' width='30' height='2' fill='%233b82f6'/><rect x='35' y='50' width='25' height='2' fill='%233b82f6'/><rect x='35' y='55' width='20' height='2' fill='%233b82f6'/></svg>">
81 + <style>
82 + :root {
83 + --primary: #3b82f6;
84 + --primary-dark: #1d4ed8;
85 + --primary-light: #60a5fa;
86 + --secondary: #8b5cf6;
87 + --success: #10b981;
88 + --warning: #f59e0b;
89 + --danger: #ef4444;
90 + --dark: #1e293b;
91 + --light: #f8fafc;
92 + --gray: #64748b;
93 + --border: #e2e8f0;
94 + --shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
95 + }
96 +
97 + * {
98 + margin: 0;
99 + padding: 0;
100 + box-sizing: border-box;
101 + }
102 +
103 + body {
104 + font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
105 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
106 + color: var(--dark);
107 + min-height: 100vh;
108 + padding: 20px;
109 + line-height: 1.6;
110 + }
111 +
112 + .container {
113 + max-width: 1400px;
114 + margin: 0 auto;
115 + background: white;
116 + border-radius: 24px;
117 + box-shadow: var(--shadow), 0 20px 80px rgba(0,0,0,0.15);
118 + overflow: hidden;
119 + backdrop-filter: blur(10px);
120 + }
121 +
122 + header {
123 + background: linear-gradient(135deg, var(--primary), var(--secondary));
124 + color: white;
125 + padding: 3rem 2rem;
126 + text-align: center;
127 + position: relative;
128 + overflow: hidden;
129 + }
130 +
131 + header::before {
132 + content: '';
133 + position: absolute;
134 + top: 0;
135 + left: 0;
136 + right: 0;
137 + bottom: 0;
138 + background: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23ffffff' fill-opacity='0.1' fill-rule='evenodd'/%3E%3C/svg%3E");
139 + animation: float 20s infinite linear;
140 + }
141 +
142 + @keyframes float {
143 + 0% { transform: translateX(0) translateY(0); }
144 + 100% { transform: translateX(-100px) translateY(-100px); }
145 + }
146 +
147 + .logo-container {
148 + position: relative;
149 + z-index: 2;
150 + }
151 +
152 + .logo {
153 + font-size: 5rem;
154 + margin-bottom: 1rem;
155 + filter: drop-shadow(0 8px 16px rgba(0,0,0,0.3));
156 + background: linear-gradient(135deg, #ffffff, #e0f2fe);
157 + -webkit-background-clip: text;
158 + -webkit-text-fill-color: transparent;
159 + background-clip: text;
160 + }
161 +
162 + h1 {
163 + margin: 0;
164 + font-size: 3rem;
165 + font-weight: 800;
166 + letter-spacing: -1px;
167 + text-shadow: 0 4px 8px rgba(0,0,0,0.2);
168 + }
169 +
170 + .subtitle {
171 + opacity: 0.95;
172 + margin-top: 0.75rem;
173 + font-weight: 400;
174 + font-size: 1.3rem;
175 + max-width: 600px;
176 + margin-left: auto;
177 + margin-right: auto;
178 + }
179 +
180 + .path-bar {
181 + background: linear-gradient(90deg, #f1f5f9, #f8fafc);
182 + padding: 1.25rem 2rem;
183 + font-family: 'JetBrains Mono', 'Monaco', 'Consolas', monospace;
184 + font-size: 1.1rem;
185 + color: var(--primary-dark);
186 + border-bottom: 1px solid var(--border);
187 + display: flex;
188 + align-items: center;
189 + gap: 12px;
190 + }
191 +
192 + .path-bar::before {
193 + content: '📍';
194 + font-size: 1.2rem;
195 + }
196 +
197 + .actions {
198 + padding: 2rem;
199 + background: #f8fafc;
200 + display: flex;
201 + gap: 1.5rem;
202 + flex-wrap: wrap;
203 + align-items: center;
204 + justify-content: space-between;
205 + border-bottom: 1px solid var(--border);
206 + }
207 +
208 + .upload-box {
209 + background: white;
210 + padding: 1.75rem;
211 + border-radius: 20px;
212 + box-shadow: var(--shadow);
213 + flex: 1;
214 + min-width: 350px;
215 + border: 2px dashed var(--primary-light);
216 + transition: all 0.3s ease;
217 + }
218 +
219 + .upload-box:hover {
220 + border-color: var(--primary);
221 + transform: translateY(-2px);
222 + box-shadow: 0 15px 40px rgba(59, 130, 246, 0.15);
223 + }
224 +
225 + .upload-form {
226 + display: flex;
227 + gap: 15px;
228 + width: 100%;
229 + align-items: center;
230 + }
231 +
232 + .file-input {
233 + flex: 1;
234 + padding: 14px 18px;
235 + border: 2px solid #e2e8f0;
236 + border-radius: 12px;
237 + background: white;
238 + font-size: 1rem;
239 + transition: all 0.3s ease;
240 + font-family: inherit;
241 + }
242 +
243 + .file-input:focus {
244 + outline: none;
245 + border-color: var(--primary);
246 + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
247 + }
248 +
249 + .file-input:hover {
250 + border-color: var(--primary-light);
251 + }
252 +
253 + button, .btn {
254 + background: linear-gradient(135deg, var(--primary), var(--primary-dark));
255 + color: white;
256 + border: none;
257 + padding: 14px 32px;
258 + border-radius: 14px;
259 + cursor: pointer;
260 + font-weight: 600;
261 + font-size: 1.05rem;
262 + text-decoration: none;
263 + display: inline-flex;
264 + align-items: center;
265 + gap: 10px;
266 + transition: all 0.3s ease;
267 + font-family: inherit;
268 + position: relative;
269 + overflow: hidden;
270 + }
271 +
272 + button::before, .btn::before {
273 + content: '';
274 + position: absolute;
275 + top: 0;
276 + left: -100%;
277 + width: 100%;
278 + height: 100%;
279 + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
280 + transition: left 0.5s;
281 + }
282 +
283 + button:hover::before, .btn:hover::before {
284 + left: 100%;
285 + }
286 +
287 + button:hover, .btn:hover {
288 + transform: translateY(-3px);
289 + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.4);
290 + }
291 +
292 + button:active, .btn:active {
293 + transform: translateY(-1px);
294 + }
295 +
296 + .btn-success {
297 + background: linear-gradient(135deg, var(--success), #059669);
298 + }
299 +
300 + .btn-success:hover {
301 + box-shadow: 0 10px 25px rgba(16, 185, 129, 0.4);
302 + }
303 +
304 + .file-table {
305 + width: 100%;
306 + border-collapse: collapse;
307 + margin: 0;
308 + }
309 +
310 + th {
311 + background: linear-gradient(135deg, #f8fafc, #f1f5f9);
312 + padding: 1.5rem 2rem;
313 + text-align: left;
314 + color: var(--gray);
315 + font-weight: 700;
316 + border-bottom: 3px solid var(--border);
317 + font-size: 1.1rem;
318 + }
319 +
320 + td {
321 + padding: 1.5rem 2rem;
322 + border-bottom: 1px solid var(--border);
323 + transition: all 0.3s ease;
324 + vertical-align: top;
325 + }
326 +
327 + tr {
328 + transition: all 0.3s ease;
329 + }
330 +
331 + tr:hover {
332 + background: linear-gradient(90deg, #f0f9ff, #f8fafc);
333 + transform: translateX(8px);
334 + }
335 +
336 + .size-col {
337 + text-align: right;
338 + color: var(--gray);
339 + font-variant-numeric: tabular-nums;
340 + font-weight: 600;
341 + }
342 +
343 + .date-col {
344 + color: #94a3b8;
345 + font-size: 0.95rem;
346 + font-weight: 500;
347 + }
348 +
349 + .folder {
350 + font-weight: 700;
351 + color: var(--primary);
352 + font-size: 1.1rem;
353 + }
354 +
355 + .file-link {
356 + color: var(--dark);
357 + text-decoration: none;
358 + transition: all 0.3s ease;
359 + display: flex;
360 + align-items: center;
361 + gap: 12px;
362 + font-weight: 500;
363 + }
364 +
365 + .file-link:hover {
366 + color: var(--primary);
367 + transform: translateX(5px);
368 + }
369 +
370 + .icon {
371 + font-size: 1.4rem;
372 + transition: transform 0.3s ease;
373 + }
374 +
375 + .file-link:hover .icon {
376 + transform: scale(1.2);
377 + }
378 +
379 + .back-link {
380 + color: var(--primary);
381 + text-decoration: none;
382 + font-weight: 700;
383 + display: flex;
384 + align-items: center;
385 + gap: 10px;
386 + padding: 12px 0;
387 + font-size: 1.1rem;
388 + transition: all 0.3s ease;
389 + }
390 +
391 + .back-link:hover {
392 + transform: translateX(-5px);
393 + }
394 +
395 + .thumbnail {
396 + width: 90px;
397 + height: 70px;
398 + object-fit: cover;
399 + border-radius: 10px;
400 + border: 3px solid #e2e8f0;
401 + margin-right: 15px;
402 + transition: all 0.3s ease;
403 + box-shadow: 0 4px 12px rgba(0,0,0,0.1);
404 + }
405 +
406 + .thumbnail:hover {
407 + transform: scale(1.05);
408 + border-color: var(--primary);
409 + box-shadow: 0 8px 20px rgba(0,0,0,0.15);
410 + }
411 +
412 + .file-info {
413 + display: flex;
414 + align-items: center;
415 + }
416 +
417 + .file-name {
418 + flex: 1;
419 + }
420 +
421 + .stats {
422 + display: flex;
423 + gap: 2rem;
424 + padding: 1.5rem 2rem;
425 + background: #f8fafc;
426 + border-bottom: 1px solid var(--border);
427 + font-size: 0.95rem;
428 + color: var(--gray);
429 + }
430 +
431 + .stat-item {
432 + display: flex;
433 + align-items: center;
434 + gap: 8px;
435 + }
436 +
437 + footer {
438 + text-align: center;
439 + padding: 2.5rem;
440 + color: #94a3b8;
441 + font-size: 1rem;
442 + border-top: 1px solid var(--border);
443 + background: #f8fafc;
444 + }
445 +
446 + .footer-content {
447 + display: flex;
448 + justify-content: space-between;
449 + align-items: center;
450 + max-width: 1000px;
451 + margin: 0 auto;
452 + }
453 +
454 + @media (max-width: 768px) {
455 + .actions {
456 + flex-direction: column;
457 + align-items: stretch;
458 + }
459 +
460 + .upload-form {
461 + flex-direction: column;
462 + }
463 +
464 + th, td {
465 + padding: 1.25rem;
466 + }
467 +
468 + .thumbnail {
469 + width: 70px;
470 + height: 55px;
471 + }
472 +
473 + h1 {
474 + font-size: 2.2rem;
475 + }
476 +
477 + .footer-content {
478 + flex-direction: column;
479 + gap: 1rem;
480 + }
481 + }
482 +
483 + .loading {
484 + opacity: 0.7;
485 + pointer-events: none;
486 + }
487 +
488 + .pulse {
489 + animation: pulse 2s infinite;
490 + }
491 +
492 + @keyframes pulse {
493 + 0% { opacity: 1; }
494 + 50% { opacity: 0.7; }
495 + 100% { opacity: 1; }
496 + }
497 + </style>
498 + </head>
499 + <body>
500 + <div class="container">
501 + <header>
502 + <div class="logo-container">
503 + <div class="logo">📁</div>
504 + <h1>{{.ServerName}}</h1>
505 + <div class="subtitle">Професионално споделяне на файлове с миниатюри</div>
506 + </div>
507 + </header>
508 +
509 + <div class="stats">
510 + <div class="stat-item">📊 Общо файлове: <strong>{{.FileCount}}</strong></div>
511 + <div class="stat-item">📁 Папки: <strong>{{.DirCount}}</strong></div>
512 + <div class="stat-item">🖼️ Картинки: <strong>{{.ImageCount}}</strong></div>
513 + </div>
514 +
515 + <div class="path-bar">
516 + Път: <strong>/{{.Current}}</strong>
517 + </div>
518 +
519 + <div class="actions">
520 + <div class="upload-box">
521 + <form method="post" enctype="multipart/form-data" class="upload-form" id="uploadForm">
522 + <input type="file" name="f" required class="file-input" id="fileInput">
523 + <button type="submit" id="uploadBtn">📤 Качи файл</button>
524 + </form>
525 + </div>
526 + <a href="/zip{{.CurrentPath}}" class="btn btn-success">📦 Изтегли като ZIP</a>
527 + </div>
528 +
529 + <table class="file-table">
530 + <thead>
531 + <tr>
532 + <th>Име</th>
533 + <th class="size-col">Размер</th>
534 + <th class="date-col">Дата</th>
535 + </tr>
536 + </thead>
537 + <tbody>
538 + {{if ne .Current "/"}}
539 + <tr>
540 + <td colspan="3">
541 + <a href=".." class="back-link">⬆️ Назад</a>
542 + </td>
543 + </tr>
544 + {{end}}
545 + {{range .Files}}
546 + <tr>
547 + <td>
548 + {{if .IsDir}}
549 + <div class="file-info">
550 + <span class="folder">📁 {{.Name}}</span>
551 + <a href="{{.Name}}/" class="file-link">[Отвори]</a>
552 + </div>
553 + {{else}}
554 + <div class="file-info">
555 + {{if .Thumbnail}}
556 + <img src="{{.Thumbnail}}" alt="{{.Name}}" class="thumbnail"
557 + onerror="this.style.display='none'"
558 + onclick="window.open('{{$.CurrentPath}}/{{.Name}}', '_blank')"
559 + style="cursor: zoom-in;">
560 + {{end}}
561 + <div class="file-name">
562 + <a href="{{.Name}}" class="file-link">
563 + <span class="icon">{{if .Thumbnail}}🖼️{{else}}📄{{end}}</span>
564 + {{.Name}}
565 + </a>
566 + </div>
567 + </div>
568 + {{end}}
569 + </td>
570 + <td class="size-col">{{.Size}}</td>
571 + <td class="date-col">{{.ModTime}}</td>
572 + </tr>
573 + {{end}}
574 + </tbody>
575 + </table>
576 +
577 + <footer>
578 + <div class="footer-content">
579 + <div>© 2024 {{.ServerName}} - Професионален файлов сървър</div>
580 + <div>🔄 {{.CurrentTime}}</div>
581 + </div>
582 + </footer>
583 + </div>
584 +
585 + <script>
586 + document.addEventListener('DOMContentLoaded', function() {
587 + const thumbnails = document.querySelectorAll('.thumbnail');
588 + thumbnails.forEach(thumb => {
589 + thumb.addEventListener('error', function() {
590 + this.style.display = 'none';
591 + });
592 + });
593 +
594 + const fileInput = document.querySelector('#fileInput');
595 + const uploadForm = document.querySelector('#uploadForm');
596 + const uploadBtn = document.querySelector('#uploadBtn');
597 +
598 + fileInput.addEventListener('change', function() {
599 + if (this.files[0] && this.files[0].size > 100 * 1024 * 1024) {
600 + if (!confirm('Файлът е голям (' + (this.files[0].size / (1024*1024)).toFixed(1) + 'MB). Сигурни ли сте, че искате да го качите?')) {
601 + this.value = '';
602 + }
603 + }
604 + });
605 +
606 + uploadForm.addEventListener('submit', function(e) {
607 + uploadBtn.innerHTML = '⏳ Качване...';
608 + uploadBtn.classList.add('loading', 'pulse');
609 + uploadBtn.disabled = true;
610 + });
611 +
612 + // Add some interactive effects
613 + const rows = document.querySelectorAll('tr');
614 + rows.forEach(row => {
615 + row.addEventListener('mouseenter', function() {
616 + this.style.transition = 'all 0.3s ease';
617 + });
618 + });
619 + });
620 + </script>
621 + </body>
622 + </html>`
623 +
624 + type File struct {
625 + Name string
626 + Size string
627 + ModTime string
628 + IsDir bool
629 + Thumbnail string
630 + }
631 +
632 + type PageData struct {
633 + Files []File
634 + Current string
635 + CurrentPath string
636 + ServerName string
637 + FileCount int
638 + DirCount int
639 + ImageCount int
640 + CurrentTime string
641 + }
642 +
643 + func main() {
644 + log.Printf("Стартиране на %s на порт %s за директория %s", serverName, port, fileDir)
645 +
646 + http.HandleFunc("/", handler)
647 + http.HandleFunc("/zip", zipHandler)
648 + http.HandleFunc("/thumb/", thumbHandler)
649 +
650 + log.Printf("Сървърът е готов на http://0.0.0.0:%s", port)
651 + log.Fatal(http.ListenAndServe(":"+port, nil))
652 + }
653 +
654 + func handler(w http.ResponseWriter, r *http.Request) {
655 + if strings.HasPrefix(r.URL.Path, "/zip") {
656 + zipHandler(w, r)
657 + return
658 + }
659 +
660 + if strings.HasPrefix(r.URL.Path, "/thumb/") {
661 + thumbHandler(w, r)
662 + return
663 + }
664 +
665 + reqPath := filepath.Clean(r.URL.Path)
666 + if reqPath == "/" || reqPath == "." {
667 + reqPath = ""
668 + }
669 + fullPath := filepath.Join(fileDir, reqPath)
670 +
671 + if !strings.HasPrefix(fullPath, fileDir) {
672 + http.Error(w, "Достъпът е отказан", http.StatusForbidden)
673 + return
674 + }
675 +
676 + fi, err := os.Stat(fullPath)
677 + if err != nil {
678 + http.NotFound(w, r)
679 + return
680 + }
681 +
682 + if fi.IsDir() {
683 + if r.Method == "POST" {
684 + upload(w, r, fullPath)
685 + return
686 + }
687 + listDir(w, reqPath, fullPath)
688 + return
689 + }
690 +
691 + http.ServeFile(w, r, fullPath)
692 + }
693 +
694 + func upload(w http.ResponseWriter, r *http.Request, dir string) {
695 + if err := r.ParseMultipartForm(500 << 20); err != nil {
696 + http.Error(w, "Файлът е твърде голям (максимум 500MB)", http.StatusBadRequest)
697 + return
698 + }
699 +
700 + file, header, err := r.FormFile("f")
701 + if err != nil {
702 + http.Error(w, "Грешка при качване", http.StatusBadRequest)
703 + return
704 + }
705 + defer file.Close()
706 +
707 + filePath := filepath.Join(dir, header.Filename)
708 + dst, err := os.Create(filePath)
709 + if err != nil {
710 + http.Error(w, "Грешка при създаване на файл", http.StatusInternalServerError)
711 + return
712 + }
713 + defer dst.Close()
714 +
715 + if _, err := io.Copy(dst, file); err != nil {
716 + http.Error(w, "Грешка при запис на файл", http.StatusInternalServerError)
717 + return
718 + }
719 +
720 + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
721 + }
722 +
723 + func listDir(w http.ResponseWriter, relPath, fullPath string) {
724 + entries, err := os.ReadDir(fullPath)
725 + if err != nil {
726 + http.Error(w, "Грешка при четене на директория", http.StatusInternalServerError)
727 + return
728 + }
729 +
730 + var files []File
731 + fileCount := 0
732 + dirCount := 0
733 + imageCount := 0
734 +
735 + for _, e := range entries {
736 + info, err := e.Info()
737 + if err != nil {
738 + continue
739 + }
740 +
741 + size := "📁 папка"
742 + if !e.IsDir() {
743 + fileCount++
744 + s := info.Size()
745 + switch {
746 + case s < 1024:
747 + size = fmt.Sprintf("%d Б", s)
748 + case s < 1048576:
749 + size = fmt.Sprintf("%.1f КБ", float64(s)/1024)
750 + case s < 1073741824:
751 + size = fmt.Sprintf("%.1f МБ", float64(s)/1048576)
752 + default:
753 + size = fmt.Sprintf("%.1f ГБ", float64(s)/1073741824)
754 + }
755 + } else {
756 + dirCount++
757 + }
758 +
759 + thumbnail := ""
760 + if !e.IsDir() && isImageFile(e.Name()) {
761 + imageCount++
762 + thumbnail = fmt.Sprintf("/thumb%s/%s", relPath, e.Name())
763 + }
764 +
765 + files = append(files, File{
766 + Name: e.Name(),
767 + Size: size,
768 + ModTime: info.ModTime().Format("02.01.2006 15:04"),
769 + IsDir: e.IsDir(),
770 + Thumbnail: thumbnail,
771 + })
772 + }
773 +
774 + sort.Slice(files, func(i, j int) bool {
775 + if files[i].IsDir != files[j].IsDir {
776 + return files[i].IsDir
777 + }
778 + return strings.ToLower(files[i].Name) < strings.ToLower(files[j].Name)
779 + })
780 +
781 + current := relPath
782 + if current == "" {
783 + current = "/"
784 + }
785 +
786 + currentPath := relPath
787 + if currentPath != "" && !strings.HasPrefix(currentPath, "/") {
788 + currentPath = "/" + currentPath
789 + }
790 +
791 + data := PageData{
792 + Files: files,
793 + Current: current,
794 + CurrentPath: currentPath,
795 + ServerName: serverName,
796 + FileCount: fileCount,
797 + DirCount: dirCount,
798 + ImageCount: imageCount,
799 + CurrentTime: time.Now().Format("02.01.2006 15:04:05"),
800 + }
801 +
802 + if err := tmpl.Execute(w, data); err != nil {
803 + http.Error(w, "Грешка при генериране на страница", http.StatusInternalServerError)
804 + }
805 + }
806 +
807 + func thumbHandler(w http.ResponseWriter, r *http.Request) {
808 + path := strings.TrimPrefix(r.URL.Path, "/thumb")
809 + fullPath := filepath.Join(fileDir, filepath.Clean(path))
810 +
811 + if !strings.HasPrefix(fullPath, fileDir) {
812 + http.Error(w, "Достъпът е отказан", http.StatusForbidden)
813 + return
814 + }
815 +
816 + if _, err := os.Stat(fullPath); os.IsNotExist(err) || !isImageFile(fullPath) {
817 + http.NotFound(w, r)
818 + return
819 + }
820 +
821 + generateThumbnail(w, fullPath)
822 + }
823 +
824 + func generateThumbnail(w http.ResponseWriter, filePath string) {
825 + file, err := os.Open(filePath)
826 + if err != nil {
827 + http.Error(w, "Грешка при отваряне на файл", http.StatusInternalServerError)
828 + return
829 + }
830 + defer file.Close()
831 +
832 + img, _, err := image.Decode(file)
833 + if err != nil {
834 + http.Error(w, "Невалидно изображение", http.StatusInternalServerError)
835 + return
836 + }
837 +
838 + bounds := img.Bounds()
839 + width := bounds.Dx()
840 + height := bounds.Dy()
841 +
842 + newWidth := 90
843 + newHeight := 70
844 +
845 + if width > height {
846 + newHeight = int(float64(height) * float64(newWidth) / float64(width))
847 + } else {
848 + newWidth = int(float64(width) * float64(newHeight) / float64(height))
849 + }
850 +
851 + dst := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
852 +
853 + for y := 0; y < newHeight; y++ {
854 + for x := 0; x < newWidth; x++ {
855 + srcX := x * width / newWidth
856 + srcY := y * height / newHeight
857 + dst.Set(x, y, img.At(srcX, srcY))
858 + }
859 + }
860 +
861 + w.Header().Set("Content-Type", "image/jpeg")
862 + if err := jpeg.Encode(w, dst, &jpeg.Options{Quality: 85}); err != nil {
863 + http.Error(w, "Грешка при кодиране на миниатюра", http.StatusInternalServerError)
864 + return
865 + }
866 + }
867 +
868 + func isImageFile(filename string) bool {
869 + ext := strings.ToLower(filepath.Ext(filename))
870 + imageExts := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".svg"}
871 +
872 + for _, imgExt := range imageExts {
873 + if ext == imgExt {
874 + return true
875 + }
876 + }
877 + return false
878 + }
879 +
880 + func zipHandler(w http.ResponseWriter, r *http.Request) {
881 + path := strings.TrimPrefix(r.URL.Path, "/zip")
882 + if path == "" {
883 + path = "/"
884 + }
885 +
886 + fullPath := filepath.Join(fileDir, filepath.Clean(path))
887 +
888 + if !strings.HasPrefix(fullPath, fileDir) {
889 + http.Error(w, "Достъпът е отказан", http.StatusForbidden)
890 + return
891 + }
892 +
893 + if _, err := os.Stat(fullPath); os.IsNotExist(err) {
894 + http.NotFound(w, r)
895 + return
896 + }
897 +
898 + w.Header().Set("Content-Type", "application/zip")
899 + filename := filepath.Base(fullPath)
900 + if filename == "" || filename == "." || filename == "/" {
901 + filename = "files"
902 + }
903 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", filename))
904 +
905 + zw := zip.NewWriter(w)
906 + defer zw.Close()
907 +
908 + filepath.Walk(fullPath, func(filePath string, info os.FileInfo, err error) error {
909 + if err != nil {
910 + return err
911 + }
912 +
913 + relPath, err := filepath.Rel(fullPath, filePath)
914 + if err != nil {
915 + return err
916 + }
917 +
918 + if relPath == "." {
919 + return nil
920 + }
921 +
922 + header, err := zip.FileInfoHeader(info)
923 + if err != nil {
924 + return err
925 + }
926 +
927 + header.Name = relPath
928 +
929 + if info.IsDir() {
930 + header.Name += "/"
931 + } else {
932 + header.Method = zip.Deflate
933 + }
934 +
935 + writer, err := zw.CreateHeader(header)
936 + if err != nil {
937 + return err
938 + }
939 +
940 + if !info.IsDir() {
941 + file, err := os.Open(filePath)
942 + if err != nil {
943 + return err
944 + }
945 + defer file.Close()
946 +
947 + _, err = io.Copy(writer, file)
948 + if err != nil {
949 + return err
950 + }
951 + }
952 +
953 + return nil
954 + })
955 + }
956 + EOF
957 +
958 + log "Компилиране на професионалната версия..."
959 + CGO_ENABLED=0 go build -ldflags="-s -w" -o fedya-server || error "Грешка при компилация!"
960 +
961 + install -m 755 fedya-server /usr/local/bin/fedya-server
962 +
963 + # Спиране на старите услуги
964 + systemctl stop file-server sabork-server fedya-server 2>/dev/null || true
965 + systemctl disable file-server sabork-server 2>/dev/null || true
966 +
967 + if $auto_start; then
968 + log "Конфигуриране на systemd услуга..."
969 +
970 + cat > /etc/systemd/system/fedya-server.service <<EOF
971 + [Unit]
972 + Description=$server_name - Professional File Server
973 + After=network.target
974 + StartLimitIntervalSec=0
975 +
976 + [Service]
977 + Type=simple
978 + Environment=FILE_DIR=$file_dir
979 + Environment=PORT=$port
980 + Environment=SERVER_NAME="$server_name"
981 + ExecStart=/usr/local/bin/fedya-server
982 + WorkingDirectory=$file_dir
983 + Restart=always
984 + RestartSec=3
985 +
986 + [Install]
987 + WantedBy=multi-user.target
988 + EOF
989 +
990 + systemctl daemon-reload
991 + systemctl enable fedya-server >/dev/null 2>&1
992 +
993 + if systemctl start fedya-server; then
994 + log "Услугата $server_name е стартирана успешно!"
995 + sleep 2
996 + log "Статус на услугата:"
997 + systemctl status fedya-server --no-pager -l
998 + else
999 + error "Неуспешно стартиране на услугата. Провери: journalctl -u fedya-server -f"
1000 + fi
1001 + else
1002 + log "Ръчно стартиране:"
1003 + echo "FILE_DIR='$file_dir' PORT='$port' SERVER_NAME='$server_name' /usr/local/bin/fedya-server"
1004 + fi
1005 +
1006 + rm -rf "$tmp"
1007 + IP=$(hostname -I | awk '{print $1}')
1008 + log "ГОТОВО! Отвори: http://$IP:$port"
1009 + log "🎉 Професионален дизайн • Цветно лого • Фавикон • Статистики"
1010 +
1011 + if $auto_start; then
1012 + echo
1013 + log "Команди за управление:"
1014 + echo " systemctl status fedya-server"
1015 + echo " journalctl -u fedya-server -f"
1016 + echo " systemctl restart fedya-server"
1017 + fi
1018 +
1019 + echo
1020 + log "💼 Сървърът е готов за колегите!"
1021 + echo " 🌐 Може да го споделиш на: http://$IP:$port"
1022 + echo " 📊 Има статистики за файлове, папки и картинки"
1023 + echo " 🎨 Професионален дизайн с анимации и ефекти"
1024 + echo " 🔒 Безопасен и стабилен за корпоративна среда"
Newer Older