This repository has been archived on 2026-07-15. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
drive/backend/main.go
Elijah 71ba029fbc
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m18s
feat: implement trash auto-purge, settings layout update, larger folder icons, and legacy version cleanup
2026-05-22 18:41:16 -07:00

214 lines
6.7 KiB
Go

package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"os"
"time"
"git.elijahkuntz.com/Elijah/drive/config"
"git.elijahkuntz.com/Elijah/drive/database"
"git.elijahkuntz.com/Elijah/drive/handlers"
"git.elijahkuntz.com/Elijah/drive/middleware"
"git.elijahkuntz.com/Elijah/drive/workers"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/helmet"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
)
func main() {
cfg := config.Load()
// Auto-generate JWT secret if not set
if cfg.JWTSecret == "" {
secret, err := generateSecret(32)
if err != nil {
log.Fatal("Failed to generate JWT secret:", err)
}
cfg.JWTSecret = secret
log.Println("Warning: JWT_SECRET not set, generated a random one. Set JWT_SECRET env var for persistence across restarts.")
}
// Ensure required directories exist
dirs := []string{
cfg.DataDir,
cfg.StorageDir,
cfg.ThumbnailDir,
cfg.TrashDir,
cfg.VersionsDir,
cfg.BackupDir,
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
log.Fatalf("Failed to create directory %s: %v", dir, err)
}
}
// Initialize database
db, err := database.New(cfg.DBPath)
if err != nil {
log.Fatal("Failed to initialize database:", err)
}
defer db.Close()
// Initialize brute force guard
guard := middleware.NewBruteForceGuard(cfg.MaxLoginAttempts, cfg.LockoutSeconds)
// Initialize handlers
authHandler := &handlers.AuthHandler{DB: db, Config: cfg, Guard: guard}
fsHandler := &handlers.FSHandler{DB: db, Config: cfg}
healthHandler := &handlers.HealthHandler{DB: db, Config: cfg}
settingsHandler := &handlers.SettingsHandler{DB: db}
tusHandler := handlers.NewTusHandler(db, cfg)
webdavHandler := handlers.NewWebDAVHandler(db, cfg)
archiveHandler := &handlers.ArchiveHandler{DB: db, Config: cfg}
shareHandler := &handlers.ShareHandler{DB: db, Config: cfg}
// Initialize background workers
workers.InitTaskManager(cfg)
workers.InitThumbnailManager(cfg, db)
workers.StartBackupWorker(cfg, db)
workers.StartTrashWorker(cfg, db)
// Create Fiber app
app := fiber.New(fiber.Config{
BodyLimit: 100 * 1024 * 1024 * 1024, // 100 GB (for chunked uploads, each chunk is smaller)
StreamRequestBody: true,
DisableStartupMessage: false,
AppName: "Drive",
})
// Global middleware
app.Use(recover.New())
app.Use(logger.New(logger.Config{
Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path}\n",
TimeFormat: "2006-01-02 15:04:05",
}))
app.Use(func(c *fiber.Ctx) error {
c.Set("Cache-Control", "no-store, no-cache, must-revalidate")
return c.Next()
})
app.Use(helmet.New())
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "Origin, Content-Type, Accept, Authorization, Upload-Length, Upload-Offset, Upload-Metadata, Tus-Resumable",
AllowMethods: "GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD",
ExposeHeaders: "Location, Upload-Offset, Upload-Length, Tus-Resumable, Tus-Version, Tus-Extension, Tus-Max-Size",
}))
// === PUBLIC ROUTES (no auth) ===
// Health check
app.Get("/health", healthHandler.Health)
// Setup routes (first-run only)
app.Get("/api/setup/status", authHandler.CheckSetup)
app.Post("/api/setup", authHandler.Setup)
// Public Share Endpoints (Rate Limited)
public := app.Group("/api/public")
public.Use(limiter.New(limiter.Config{
Max: 20,
Expiration: 1 * time.Minute,
}))
public.Post("/share/:id", shareHandler.GetPublicShare)
public.Get("/download/:id", shareHandler.DownloadPublicShare)
// Auth routes (with brute force protection)
auth := app.Group("/api/auth")
auth.Post("/login", guard.Check(), authHandler.Login)
// === PROTECTED ROUTES (require JWT) ===
protected := app.Group("/api", middleware.AuthMiddleware(cfg.JWTSecret))
// Token refresh
protected.Post("/auth/refresh", authHandler.Refresh)
// Password management
protected.Put("/auth/password", authHandler.ChangePassword)
// 2FA management
protected.Post("/auth/2fa/enable", authHandler.Enable2FA)
protected.Post("/auth/2fa/confirm", authHandler.Confirm2FA)
protected.Post("/auth/2fa/disable", authHandler.Disable2FA)
// Settings & Audit
protected.Get("/settings", settingsHandler.GetSettings)
protected.Put("/settings", settingsHandler.UpdateSettings)
protected.Get("/audit", settingsHandler.GetAuditLog)
// Search
protected.Get("/search", fsHandler.Search)
// Pinned items
protected.Get("/files/pinned", fsHandler.ListPinned)
protected.Post("/files/pin", fsHandler.TogglePin)
// Trash
protected.Get("/trash", fsHandler.ListTrash)
protected.Post("/trash/restore", fsHandler.RestoreFromTrash)
protected.Delete("/trash", fsHandler.EmptyTrash)
// Storage dashboard
protected.Get("/storage/dashboard", fsHandler.StorageDashboard)
// File operations
protected.Post("/files/mkdir", fsHandler.CreateFolder)
protected.Post("/files/rename", fsHandler.Rename)
protected.Post("/files/move", fsHandler.Move)
protected.Post("/files/copy", fsHandler.Copy)
protected.Get("/files/download-folder", fsHandler.DownloadFolder)
protected.Post("/files/upload", fsHandler.Upload)
// File listing and download (with path jail)
files := protected.Group("/files", middleware.PathJail(cfg.StorageDir))
files.Get("/thumbnail/:checksum", fsHandler.ServeThumbnail)
files.Get("/download/*", fsHandler.Download)
files.Post("/zip", archiveHandler.Zip)
files.Post("/unzip", archiveHandler.Unzip)
files.Delete("/*", fsHandler.Delete)
files.Get("/*", fsHandler.ListDirectory)
// Tus resumable uploads
tus := protected.Group("/tus")
tus.Options("/", tusHandler.Options)
tus.Post("/", tusHandler.Create)
tus.Head("/:id", tusHandler.Head)
tus.Patch("/:id", tusHandler.Patch)
tus.Delete("/:id", tusHandler.Delete)
// Background Tasks
protected.Get("/tasks", archiveHandler.Tasks)
// Sharing
protected.Post("/share", shareHandler.CreateShare)
protected.Get("/share", shareHandler.ListShares)
protected.Delete("/share/:id", shareHandler.RevokeShare)
// WebDAV Endpoint (accepts basic auth OR bearer)
// We use All() because WebDAV uses custom HTTP methods (PROPFIND, MKCOL, LOCK, etc.)
app.All("/webdav/*", webdavHandler.Handle)
// Start server
addr := fmt.Sprintf(":%s", cfg.Port)
log.Printf("🚀 Drive server starting on %s", addr)
log.Printf("📁 Storage: %s", cfg.StorageDir)
log.Printf("💾 Data: %s", cfg.DataDir)
if err := app.Listen(addr); err != nil {
log.Fatal("Failed to start server:", err)
}
}
func generateSecret(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}