Dernière activité 1764478533

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

Révision 2a833512d5df1e612167578dfce5c2d42319801a

fedya-server.sh Brut
1#!/bin/bash
2#
3# Автор: Федя Серафиев / urocibg.eu
4
5RED='\033[0;31m'
6GREEN='\033[0;32m'
7YELLOW='\033[1;33m'
8NC='\033[0m'
9
10log() { echo -e "${GREEN}[ИНФО]${NC} $1"; }
11warn() { echo -e "${YELLOW}[ПРЕДУПРЕЖДЕНИЕ]${NC} $1"; }
12error() { echo -e "${RED}[ГРЕШКА]${NC} $1"; exit 1; }
13
14[ "$EUID" -ne 0 ] && error "Стартирай със sudo!"
15
16log "Fedya's File Server – Професионална версия с красив дизайн"
17
18read -p "Директория за файловете [/var/www/files]: " file_dir
19file_dir=${file_dir:-"/var/www/files"}
20mkdir -p "$file_dir"
21
22read -p "Порт [8080]: " port
23port=${port:-"8080"}
24
25read -p "Име на сървъра [Fedya File Server]: " server_name
26server_name=${server_name:-"Fedya File Server"}
27
28read -p "Стартиране като услуга? [Y/n]: " svc
29[[ "$svc" =~ ^[Nn]$ ]] && auto_start=false || auto_start=true
30
31log "Инсталиране на Go..."
32command -v go >/dev/null || { apt update && apt install -y golang-go; }
33
34tmp=$(mktemp -d)
35cd "$tmp"
36
37go mod init fedya-files >/dev/null 2>&1
38
39cat > main.go <<'EOF'
40package main
41
42import (
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
60var (
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
67func getEnv(key, defaultValue string) string {
68 if value := os.Getenv(key); value != "" {
69 return value
70 }
71 return defaultValue
72}
73
74const 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>
586document.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
624type File struct {
625 Name string
626 Size string
627 ModTime string
628 IsDir bool
629 Thumbnail string
630}
631
632type 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
643func 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
654func 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
694func 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
723func 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
807func 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
824func 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
868func 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
880func 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}
956EOF
957
958log "Компилиране на професионалната версия..."
959CGO_ENABLED=0 go build -ldflags="-s -w" -o fedya-server || error "Грешка при компилация!"
960
961install -m 755 fedya-server /usr/local/bin/fedya-server
962
963# Спиране на старите услуги
964systemctl stop file-server sabork-server fedya-server 2>/dev/null || true
965systemctl disable file-server sabork-server 2>/dev/null || true
966
967if $auto_start; then
968 log "Конфигуриране на systemd услуга..."
969
970 cat > /etc/systemd/system/fedya-server.service <<EOF
971[Unit]
972Description=$server_name - Professional File Server
973After=network.target
974StartLimitIntervalSec=0
975
976[Service]
977Type=simple
978Environment=FILE_DIR=$file_dir
979Environment=PORT=$port
980Environment=SERVER_NAME="$server_name"
981ExecStart=/usr/local/bin/fedya-server
982WorkingDirectory=$file_dir
983Restart=always
984RestartSec=3
985
986[Install]
987WantedBy=multi-user.target
988EOF
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
1001else
1002 log "Ръчно стартиране:"
1003 echo "FILE_DIR='$file_dir' PORT='$port' SERVER_NAME='$server_name' /usr/local/bin/fedya-server"
1004fi
1005
1006rm -rf "$tmp"
1007IP=$(hostname -I | awk '{print $1}')
1008log "ГОТОВО! Отвори: http://$IP:$port"
1009log "🎉 Професионален дизайн • Цветно лого • Фавикон • Статистики"
1010
1011if $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"
1017fi
1018
1019echo
1020log "💼 Сървърът е готов за колегите!"
1021echo " 🌐 Може да го споделиш на: http://$IP:$port"
1022echo " 📊 Има статистики за файлове, папки и картинки"
1023echo " 🎨 Професионален дизайн с анимации и ефекти"
1024echo " 🔒 Безопасен и стабилен за корпоративна среда"