-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.go
More file actions
615 lines (560 loc) · 23.1 KB
/
main.go
File metadata and controls
615 lines (560 loc) · 23.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
package main
import (
"activedebiansync/api"
"activedebiansync/cluster"
"activedebiansync/config"
"activedebiansync/cvescanner"
"activedebiansync/database"
"activedebiansync/gpg"
"activedebiansync/metrics"
pkgmanager "activedebiansync/package"
"activedebiansync/scheduler"
"activedebiansync/server"
"activedebiansync/sync"
"activedebiansync/utils"
"activedebiansync/webconsole"
"context"
"flag"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
const (
AppName = "ActiveDebianSync"
)
func main() {
if len(os.Args) > 1 {
switch os.Args[1] {
case "package":
handlePackageCommand()
return
case "gpg":
handleGPGCommand()
return
case "artica":
handleArticaCommand()
return
case "cve":
handleCVECommand()
return
case "-setup", "--setup", "setup":
runSetup()
return
case "-uninstall", "--uninstall", "uninstall":
runUninstall()
return
case "version", "-version", "--version":
fmt.Printf("%s v%s\n", AppName, version)
os.Exit(0)
case "help", "-help", "--help":
printHelp()
os.Exit(0)
}
}
startDaemon()
}
func startDaemon() {
configPath := flag.String("config", config.DefaultConfigPath, "Path to configuration file")
flag.Parse()
cfg, err := config.LoadConfig(*configPath)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
os.Exit(1)
}
// Initialiser le logger
if err := utils.InitLogger(cfg.Get().LogPath, cfg.Get().AccessLogPath); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Failed to initialize logger: %v\n", err)
os.Exit(1)
}
logger := utils.GetLogger()
defer func(logger *utils.Logger) {
_ = logger.Close()
}(logger)
logger.LogInfo("=== %s v%s Starting ===", AppName, version)
logger.LogInfo("Configuration loaded from: %s", *configPath)
cfgData := cfg.Get()
pidFile := utils.NewPIDFile(cfgData.PIDFile)
if err := pidFile.Write(); err != nil {
logger.LogError("Failed to create PID file: %v", err)
os.Exit(1)
}
defer func(pidFile *utils.PIDFile) {
err := pidFile.Remove()
if err != nil {
logger.LogError("Failed to remove PID file: %v", err)
}
}(pidFile)
logger.LogInfo("PID file created: %s (PID: %d)", pidFile.GetPath(), os.Getpid())
if err := utils.ValidateUserGroup(cfgData.RunAsUser, cfgData.RunAsGroup); err != nil {
logger.LogError("Invalid user/group configuration: %v", err)
os.Exit(1)
}
if err := os.MkdirAll(cfgData.RepositoryPath, 0755); err != nil {
logger.LogError("Failed to create repository directory: %v", err)
os.Exit(1)
}
if cfgData.RunAsUser != "" {
currentUser, currentGroup, _ := utils.GetCurrentUser()
logger.LogInfo("Current user: %s:%s", currentUser, currentGroup)
targetGroup := cfgData.RunAsGroup
if targetGroup == "" {
targetGroup = "(primary group)"
}
logger.LogInfo("Switching to user: %s:%s", cfgData.RunAsUser, targetGroup)
if err := utils.SwitchUser(cfgData.RunAsUser, cfgData.RunAsGroup); err != nil {
logger.LogError("Failed to switch user: %v", err)
os.Exit(1)
}
newUser, newGroup, _ := utils.GetCurrentUser()
logger.LogInfo("Successfully switched to user: %s:%s", newUser, newGroup)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
gpgManager := gpg.NewGPGManager(cfg, logger)
syncer := sync.NewSyncer(cfg, logger, gpgManager)
httpServer := server.NewHTTPServer(cfg, logger)
httpServer.SetSyncChecker(syncer)
httpServer.SetAnalytics(syncer.GetAnalytics())
// Initialize security database
dbPath := cfg.GetDatabasePath()
securityDB, err := database.NewSecurityDB(dbPath)
if err != nil {
logger.LogError("Failed to initialize security database: %v", err)
logger.LogError("Security rules will be disabled")
} else {
httpServer.SetSecurityDB(securityDB)
logger.LogInfo("Security database initialized with %d active rules", securityDB.GetCachedRuleCount())
defer func() {
if err := securityDB.Close(); err != nil {
logger.LogError("Failed to close security database: %v", err)
}
}()
}
pkgManager := pkgmanager.NewPackageManager(cfg, logger, gpgManager)
syncer.SetPackageIndexer(pkgManager) // Enable Artica packages to be added to repository
restAPI := api.NewRestAPI(cfg, logger, syncer, httpServer, pkgManager, gpgManager, syncer)
// Initialiser la base de données des mises à jour de packages
updatesDB, err := database.NewUpdatesDB(dbPath)
if err != nil {
logger.LogError("Failed to initialize package updates database: %v", err)
logger.LogError("Package update tracking will be disabled")
} else {
syncer.SetUpdatesDB(updatesDB)
if updatesDB.IsFirstSync() {
logger.LogInfo("First sync detected - package updates will not be recorded until first sync completes")
} else {
logger.LogInfo("Package updates database initialized: %s", updatesDB.GetDBPath())
}
defer func() {
if err := updatesDB.Close(); err != nil {
logger.LogError("Failed to close updates database: %v", err)
}
}()
}
// Initialiser la base de données de recherche de packages (si activée)
if cfgData.PackageSearchEnabled {
searchDB, err := database.NewPackageSearchDB(dbPath)
if err != nil {
logger.LogError("Failed to initialize package search database: %v", err)
logger.LogError("Package search will be disabled")
} else {
syncer.SetSearchDB(searchDB)
restAPI.SetSearchDB(searchDB)
logger.LogInfo("Package search database initialized: %s", searchDB.GetDBPath())
defer func() {
if err := searchDB.Close(); err != nil {
logger.LogError("Failed to close search database: %v", err)
}
}()
}
}
// Initialize events database
eventsDB, err := database.NewEventsDB(dbPath)
if err != nil {
logger.LogError("Failed to initialize events database: %v", err)
logger.LogError("Sync event tracking will be disabled")
} else {
syncer.SetEventsDB(eventsDB)
logger.LogInfo("Events database initialized")
defer func() {
if err := eventsDB.Close(); err != nil {
logger.LogError("Failed to close events database: %v", err)
}
}()
}
// Initialize clients database for tracking client access statistics
clientsDB, err := database.NewClientsDB(dbPath)
if err != nil {
logger.LogError("Failed to initialize clients database: %v", err)
logger.LogError("Client statistics tracking will be disabled")
} else {
httpServer.SetClientsDB(clientsDB)
// Cleanup old records (older than 30 days)
deleted, err := clientsDB.CleanupOldRecords(30)
if err != nil {
logger.LogError("Failed to cleanup old client records: %v", err)
} else if deleted > 0 {
logger.LogInfo("Cleaned up %d old client records", deleted)
}
logger.LogInfo("Clients database initialized")
defer func() {
if err := clientsDB.Close(); err != nil {
logger.LogError("Failed to close clients database: %v", err)
}
}()
}
// Initialize cluster replication (if enabled)
var clusterDB *database.ClusterDB
var replicationMgr *cluster.ReplicationManager
if cfgData.ClusterEnabled {
var err error
clusterDB, err = database.NewClusterDB(dbPath)
if err != nil {
logger.LogError("Failed to initialize cluster database: %v", err)
logger.LogError("Cluster replication will be disabled")
} else {
replicationMgr = cluster.NewReplicationManager(cfg, logger, clusterDB)
syncer.SetReplicationManager(replicationMgr)
logger.LogInfo("Cluster replication initialized (node: %s, port: %d)", cfgData.ClusterNodeName, cfgData.ClusterPort)
defer func() {
if err := clusterDB.Close(); err != nil {
logger.LogError("Failed to close cluster database: %v", err)
}
}()
}
}
// Initialiser le scanner CVE
cveScanner := cvescanner.NewCVEScanner(cfg, logger)
cveAdapter := cvescanner.NewCVEScannerAdapter(cveScanner)
restAPI.SetCVEScanner(cveScanner)
syncer.SetCVEScanner(cveAdapter)
logger.LogInfo("CVE scanner initialized")
// Initialiser la persistence des métriques
metricsPersistence := metrics.NewMetricsPersistence(*configPath)
// Charger les métriques persistées si elles existent
if metricsPersistence.Exists() {
persistedMetrics, err := metricsPersistence.Load()
if err != nil {
logger.LogError("Failed to load persisted metrics: %v", err)
} else if persistedMetrics != nil {
logger.LogInfo("Loading persisted metrics from %s (saved at %s)",
metricsPersistence.GetFilePath(), persistedMetrics.SavedAt.Format(time.RFC3339))
// Charger les stats de sync
syncer.LoadStats(
persistedMetrics.SyncTotalFiles,
persistedMetrics.SyncTotalBytes,
persistedMetrics.SyncFailedFiles,
persistedMetrics.SyncLastStart,
persistedMetrics.SyncLastEnd,
persistedMetrics.SyncLastDuration,
persistedMetrics.SyncLastError,
)
// Charger les stats du serveur HTTP
httpServer.LoadStats(
persistedMetrics.ServerTotalRequests,
persistedMetrics.ServerTotalBytesSent,
)
// Charger les clients
if len(persistedMetrics.Clients) > 0 {
var clients []server.ClientInfo
for _, c := range persistedMetrics.Clients {
clients = append(clients, server.ClientInfo{
IP: c.IP,
Hostname: c.Hostname,
FirstSeen: c.FirstSeen,
LastSeen: c.LastSeen,
RequestCount: c.RequestCount,
BytesReceived: c.BytesReceived,
})
}
httpServer.LoadClients(clients)
logger.LogInfo("Loaded %d persisted clients", len(clients))
}
logger.LogInfo("Persisted metrics loaded successfully")
}
}
if cfgData.GPGSigningEnabled {
newKeyGenerated, err := gpgManager.InitializeOrLoadKey()
if err != nil {
logger.LogError("Failed to initialize GPG key: %v", err)
logger.LogError("GPG signing will be disabled")
} else {
if newKeyGenerated {
logger.LogInfo("GPG signing enabled (new key pair generated automatically)")
} else {
logger.LogInfo("GPG signing enabled (existing key loaded)")
}
}
}
errChan := make(chan error, 3)
go func() {
syncer.Start(ctx)
}()
optScheduler := scheduler.NewScheduler(cfg, logger, syncer.GetOptimizer())
go optScheduler.Start(ctx)
// Démarrer le vérificateur d'index de recherche
// Ce scheduler vérifie périodiquement si les index existent et les crée si nécessaire
// (utile quand sync_contents passe de false à true)
go syncer.StartSearchIndexChecker(ctx)
go func() {
analytics := syncer.GetAnalytics()
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
if err := analytics.Save(); err != nil {
logger.LogError("Failed to save analytics on shutdown: %v", err)
} else {
logger.LogInfo("Analytics saved successfully on shutdown")
}
return
case <-ticker.C:
if err := analytics.Save(); err != nil {
logger.LogError("Failed to save analytics: %v", err)
}
}
}
}()
go func() {
if err := httpServer.Start(ctx); err != nil {
errChan <- fmt.Errorf("HTTP server error: %w", err)
}
}()
go func() {
if err := restAPI.Start(ctx); err != nil {
errChan <- fmt.Errorf("REST API error: %w", err)
}
}()
// Initialiser et démarrer la console web (si activée)
var webConsole *webconsole.WebConsole
if cfgData.WebConsoleEnabled {
var err error
webConsole, err = webconsole.NewWebConsole(cfg, *configPath, logger, version)
if err != nil {
logger.LogError("Failed to initialize web console: %v", err)
logger.LogError("Web console will be disabled")
} else {
// Set providers for the web console
webConsole.SetProviders(httpServer, syncer, pkgManager)
webConsole.SetCVEScanner(cveAdapter)
if eventsDB != nil {
webConsole.SetEventsDB(eventsDB)
}
if securityDB != nil {
webConsole.SetSecurityDB(securityDB)
}
if clientsDB != nil {
webConsole.SetClientsDB(clientsDB)
}
if clusterDB != nil {
webConsole.SetClusterDB(clusterDB)
}
if replicationMgr != nil {
webConsole.SetReplicationManager(replicationMgr)
}
go func() {
if err := webConsole.Start(ctx); err != nil {
errChan <- fmt.Errorf("Web Console error: %w", err)
}
}()
}
}
// Start cluster replication server (if enabled)
if replicationMgr != nil {
go func() {
if err := replicationMgr.Start(ctx); err != nil {
logger.LogError("Failed to start cluster replication server: %v", err)
}
}()
defer replicationMgr.Stop()
}
logger.LogInfo("All services started successfully")
logger.LogInfo("HTTP Server: %v (port %d)", cfgData.HTTPEnabled, cfgData.HTTPPort)
logger.LogInfo("HTTPS Server: %v (port %d)", cfgData.HTTPSEnabled, cfgData.HTTPSPort)
logger.LogInfo("REST API: %v (%s:%d)", cfgData.APIEnabled, cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo("Web Console: %v (%s:%d)", cfgData.WebConsoleEnabled, cfgData.WebConsoleListenAddr, cfgData.WebConsolePort)
logger.LogInfo("Cluster: %v (port %d)", cfgData.ClusterEnabled, cfgData.ClusterPort)
logger.LogInfo("Repository Path: %s", cfgData.RepositoryPath)
logger.LogInfo("Sync Interval: %d minutes", cfgData.SyncInterval)
if cfgData.APIEnabled {
logger.LogInfo("REST API Endpoints:")
logger.LogInfo(" - GET http://%s:%d/api/status", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/sync/stats", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - POST http://%s:%d/api/sync/trigger", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/server/stats", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/clients", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/disk", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/updates/check", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/health", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - POST http://%s:%d/api/packages/upload", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/packages/list", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - POST http://%s:%d/api/packages/remove", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - POST http://%s:%d/api/packages/regenerate", cfgData.APIListenAddr, cfgData.APIPort)
if cfgData.PackageSearchEnabled {
logger.LogInfo("Package Search API:")
logger.LogInfo(" - GET http://%s:%d/api/search?q=<query>", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/search/file?path=<path>", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/search/package?name=<name>", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/search/package-files?package=<name>", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/search/status", cfgData.APIListenAddr, cfgData.APIPort)
}
if cfgData.GPGSigningEnabled {
logger.LogInfo("GPG Signing API:")
logger.LogInfo(" - GET http://%s:%d/api/gpg/status", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/gpg/info", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - POST http://%s:%d/api/gpg/generate", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - POST http://%s:%d/api/gpg/sign", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/gpg/export", cfgData.APIListenAddr, cfgData.APIPort)
logger.LogInfo(" - GET http://%s:%d/api/gpg-key (public, no auth)", cfgData.APIListenAddr, cfgData.APIPort)
}
}
select {
case sig := <-sigChan:
logger.LogInfo("Received signal: %v", sig)
logger.LogInfo("Shutting down gracefully...")
cancel()
case err := <-errChan:
logger.LogError("Fatal error: %v", err)
cancel()
}
// Sauvegarder les métriques avant l'arrêt
logger.LogInfo("Saving metrics to %s...", metricsPersistence.GetFilePath())
syncStats := syncer.GetStats()
serverStats := httpServer.GetStats()
clientList := httpServer.GetClients()
persistedMetrics := &metrics.PersistedMetrics{
// Sync stats
SyncTotalFiles: syncStats.TotalFiles,
SyncTotalBytes: syncStats.TotalBytes,
SyncFailedFiles: syncStats.FailedFiles,
SyncLastStart: syncStats.LastSyncStart,
SyncLastEnd: syncStats.LastSyncEnd,
SyncLastDuration: syncStats.LastSyncDuration,
SyncLastError: syncStats.LastError,
// Server stats
ServerTotalRequests: serverStats.TotalRequests,
ServerTotalBytesSent: serverStats.TotalBytesSent,
}
// Convertir les clients
for _, c := range clientList {
persistedMetrics.Clients = append(persistedMetrics.Clients, metrics.PersistedClient{
IP: c.IP,
Hostname: c.Hostname,
FirstSeen: c.FirstSeen,
LastSeen: c.LastSeen,
RequestCount: c.RequestCount,
BytesReceived: c.BytesReceived,
})
}
if err := metricsPersistence.Save(persistedMetrics); err != nil {
logger.LogError("Failed to save metrics: %v", err)
} else {
logger.LogInfo("Metrics saved successfully (%d sync files, %d server requests, %d clients)",
syncStats.TotalFiles, serverStats.TotalRequests, len(clientList))
}
logger.LogInfo("Removing PID file: %s", pidFile.GetPath())
logger.LogInfo("=== %s Stopped ===", AppName)
}
func printHelp() {
fmt.Printf("%s v%s - Debian Repository Synchronization Daemon\n\n", AppName, version)
fmt.Println("Usage:")
fmt.Printf(" %s [command] [options]\n\n", os.Args[0])
fmt.Println("Commands:")
fmt.Println(" (none) Start daemon (default)")
fmt.Println(" -setup Interactive setup wizard (creates user, config, systemd service)")
fmt.Println(" -uninstall Remove service, config, and optionally data")
fmt.Println(" package Manage custom packages (see 'package help')")
fmt.Println(" artica Manage Artica packages (see 'artica help')")
fmt.Println(" gpg Manage GPG keys (see 'gpg help')")
fmt.Println(" cve CVE vulnerability scanner (see 'cve help')")
fmt.Println(" version Show version")
fmt.Println(" help Show this help")
fmt.Println("\nDaemon Options:")
fmt.Println(" -config <path> Path to configuration file (default: /etc/ActiveDebianSync/config.json)")
fmt.Println("\nConfiguration:")
fmt.Printf(" Default config path: %s\n", config.DefaultConfigPath)
fmt.Println(" Config format: JSON")
fmt.Println("\nFeatures:")
fmt.Println(" - Incremental Debian repository synchronization")
fmt.Println(" - HTTP/HTTPS server for APT repository")
fmt.Println(" - REST API for monitoring and statistics")
fmt.Println(" - Automatic resume on download errors")
fmt.Println(" - Disk space monitoring and limits")
fmt.Println(" - Client tracking and access logging")
fmt.Println(" - IP-based access control for REST API")
fmt.Println("\nSupported Debian versions:")
fmt.Println(" - Debian 12 (Bookworm)")
fmt.Println(" - Debian 13 (Trixie)")
fmt.Println("\nSupported Ubuntu versions:")
fmt.Println(" - Ubuntu 20.04 LTS (Focal Fossa)")
fmt.Println(" - Ubuntu 22.04 LTS (Jammy Jellyfish)")
fmt.Println(" - Ubuntu 24.04 LTS (Noble Numbat)")
fmt.Println(" - And other releases (archived releases use old-releases.ubuntu.com)")
fmt.Println("\nREST API Endpoints:")
fmt.Println(" GET /api/status - General status and statistics")
fmt.Println(" GET /api/sync/stats - Synchronization statistics")
fmt.Println(" POST /api/sync/trigger - Trigger manual sync")
fmt.Println(" GET /api/server/stats - HTTP server statistics")
fmt.Println(" GET /api/clients - Connected clients information")
fmt.Println(" GET /api/disk - Disk space information")
fmt.Println(" GET /api/updates/check - Check for available updates")
fmt.Println(" GET /api/health - Health check (no auth)")
fmt.Println("\nPackage Management API:")
fmt.Println(" POST /api/packages/upload - Upload custom .deb package")
fmt.Println(" GET /api/packages/list - List custom packages")
fmt.Println(" POST /api/packages/remove - Remove a package")
fmt.Println(" POST /api/packages/regenerate - Regenerate indexes")
fmt.Println("\nPackage Search API (like apt-file):")
fmt.Println(" GET /api/search?q=<query> - Search packages (name, description, files)")
fmt.Println(" GET /api/search/file?path=<path> - Search by file path (like apt-file search)")
fmt.Println(" GET /api/search/package?name=<name> - Search by package name")
fmt.Println(" GET /api/search/description?q=<query> - Search by description")
fmt.Println(" GET /api/search/package-files?package=<name> - List files in package (like apt-file list)")
fmt.Println(" GET /api/search/package-info?package=<name> - Get package details")
fmt.Println(" GET /api/search/status - Get search index status")
fmt.Println("\nGPG Signing API:")
fmt.Println(" GET /api/gpg/status - Get GPG signing status")
fmt.Println(" GET /api/gpg/info - Get GPG key information")
fmt.Println(" POST /api/gpg/generate - Generate new GPG key pair")
fmt.Println(" POST /api/gpg/sign - Sign all Release files")
fmt.Println(" GET /api/gpg/export - Export public key (PGP armored)")
fmt.Println(" GET /api/gpg/export?format=json - Export public key (JSON)")
fmt.Println(" GET /api/gpg/instructions - Get client setup instructions")
fmt.Println(" GET /api/gpg-key - Download public key (no auth)")
fmt.Println("\nCVE Scanner API:")
fmt.Println(" GET /api/cve/status - Get CVE scanner status")
fmt.Println(" POST /api/cve/update - Update CVE database")
fmt.Println(" GET /api/cve/scan - Scan repository for CVEs")
fmt.Println(" GET /api/cve/summary - Get CVE summary across releases")
fmt.Println(" GET /api/cve/package?name=<pkg> - Get CVEs for a package")
fmt.Println(" GET /api/cve/search?cve=<CVE-ID> - Search for specific CVE")
fmt.Println(" GET /api/cve/vulnerable - List vulnerable packages")
fmt.Println("\nExamples:")
fmt.Println(" # Run interactive setup wizard (as root)")
fmt.Printf(" sudo %s -setup\n\n", os.Args[0])
fmt.Println(" # Start with default config")
fmt.Printf(" %s\n\n", os.Args[0])
fmt.Println(" # Start with custom config")
fmt.Printf(" %s -config /etc/myconfig.json\n\n", os.Args[0])
fmt.Println(" # Uninstall (as root)")
fmt.Printf(" sudo %s -uninstall\n\n", os.Args[0])
fmt.Println(" # Check API status")
fmt.Println(" curl http://127.0.0.1:9090/api/status | jq")
fmt.Println("\n # Trigger manual sync")
fmt.Println(" curl -X POST http://127.0.0.1:9090/api/sync/trigger")
fmt.Println("\n # Add a custom package (CLI)")
fmt.Printf(" %s package add myapp_1.0.0_amd64.deb bookworm main amd64\n", os.Args[0])
fmt.Println("\n # Upload a package (API)")
fmt.Println(" curl -F 'package=@myapp.deb' -F 'release=bookworm' -F 'component=main' -F 'architecture=amd64' http://127.0.0.1:9090/api/packages/upload")
fmt.Println("\n # Generate GPG key and sign repository (API)")
fmt.Println(" curl -X POST http://127.0.0.1:9090/api/gpg/generate")
fmt.Println(" curl -X POST http://127.0.0.1:9090/api/gpg/sign")
fmt.Println("\n # Get client setup instructions")
fmt.Println(" curl http://127.0.0.1:9090/api/gpg/instructions")
fmt.Println("\nFor more information, see README.md")
}