done
This commit is contained in:
55
.gitignore
vendored
Normal file
55
.gitignore
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
fastestmirror
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
coverage.html
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Build and distribution directories
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Configuration files (if containing sensitive data)
|
||||||
|
config/local.yaml
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.backup
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 @iwasforcedtobehere
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
127
Makefile
Normal file
127
Makefile
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# FastestMirror Makefile
|
||||||
|
# Because automation is better than repetitive typing
|
||||||
|
|
||||||
|
# Variables
|
||||||
|
BINARY_NAME=fastestmirror
|
||||||
|
VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
|
COMMIT=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
BUILD_TIME=$(shell date +%Y-%m-%dT%H:%M:%S%z)
|
||||||
|
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildTime=$(BUILD_TIME)"
|
||||||
|
|
||||||
|
# Build targets
|
||||||
|
.PHONY: all build clean test fmt lint install uninstall package help
|
||||||
|
|
||||||
|
all: test build ## Run tests and build binary
|
||||||
|
|
||||||
|
build: ## Build the binary for current platform
|
||||||
|
@echo "🔨 Building $(BINARY_NAME) v$(VERSION)..."
|
||||||
|
go build $(LDFLAGS) -o $(BINARY_NAME) .
|
||||||
|
@echo "✅ Build complete: ./$(BINARY_NAME)"
|
||||||
|
|
||||||
|
build-all: ## Build binaries for all platforms
|
||||||
|
@echo "🏗️ Building for all platforms..."
|
||||||
|
@mkdir -p dist
|
||||||
|
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o dist/$(BINARY_NAME)-linux-amd64 .
|
||||||
|
GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o dist/$(BINARY_NAME)-linux-arm64 .
|
||||||
|
GOOS=linux GOARCH=386 go build $(LDFLAGS) -o dist/$(BINARY_NAME)-linux-386 .
|
||||||
|
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o dist/$(BINARY_NAME)-darwin-amd64 .
|
||||||
|
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o dist/$(BINARY_NAME)-darwin-arm64 .
|
||||||
|
@echo "✅ Multi-platform build complete in ./dist/"
|
||||||
|
|
||||||
|
test: ## Run tests
|
||||||
|
@echo "🧪 Running tests..."
|
||||||
|
go test -v ./...
|
||||||
|
@echo "✅ Tests complete"
|
||||||
|
|
||||||
|
test-coverage: ## Run tests with coverage
|
||||||
|
@echo "🧪 Running tests with coverage..."
|
||||||
|
go test -coverprofile=coverage.out ./...
|
||||||
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
|
@echo "✅ Coverage report generated: coverage.html"
|
||||||
|
|
||||||
|
fmt: ## Format code
|
||||||
|
@echo "🎨 Formatting code..."
|
||||||
|
go fmt ./...
|
||||||
|
@echo "✅ Code formatted"
|
||||||
|
|
||||||
|
lint: ## Run linters
|
||||||
|
@echo "🔍 Running linters..."
|
||||||
|
@if command -v golangci-lint >/dev/null 2>&1; then \
|
||||||
|
golangci-lint run; \
|
||||||
|
else \
|
||||||
|
echo "⚠️ golangci-lint not installed. Install with: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
|
||||||
|
go vet ./...; \
|
||||||
|
fi
|
||||||
|
@echo "✅ Linting complete"
|
||||||
|
|
||||||
|
clean: ## Clean build artifacts
|
||||||
|
@echo "🧹 Cleaning up..."
|
||||||
|
rm -f $(BINARY_NAME)
|
||||||
|
rm -rf dist/
|
||||||
|
rm -f coverage.out coverage.html
|
||||||
|
@echo "✅ Clean complete"
|
||||||
|
|
||||||
|
install: build ## Install binary to /usr/local/bin
|
||||||
|
@echo "📦 Installing $(BINARY_NAME) to /usr/local/bin..."
|
||||||
|
sudo cp $(BINARY_NAME) /usr/local/bin/
|
||||||
|
@echo "✅ Installation complete"
|
||||||
|
|
||||||
|
uninstall: ## Remove binary from /usr/local/bin
|
||||||
|
@echo "🗑️ Uninstalling $(BINARY_NAME)..."
|
||||||
|
sudo rm -f /usr/local/bin/$(BINARY_NAME)
|
||||||
|
@echo "✅ Uninstallation complete"
|
||||||
|
|
||||||
|
package: build-all ## Create distribution packages
|
||||||
|
@echo "📦 Creating packages..."
|
||||||
|
@mkdir -p dist/packages
|
||||||
|
|
||||||
|
# Create tar.gz archives
|
||||||
|
@for binary in dist/$(BINARY_NAME)-*; do \
|
||||||
|
if [ -f "$$binary" ]; then \
|
||||||
|
platform=$$(basename "$$binary" | sed 's/$(BINARY_NAME)-//'); \
|
||||||
|
echo "Creating tar.gz for $$platform..."; \
|
||||||
|
tar -czf "dist/packages/$(BINARY_NAME)-$(VERSION)-$$platform.tar.gz" -C dist "$$(basename $$binary)"; \
|
||||||
|
fi \
|
||||||
|
done
|
||||||
|
|
||||||
|
@echo "✅ Packages created in dist/packages/"
|
||||||
|
|
||||||
|
release: clean test package ## Prepare a release
|
||||||
|
@echo "🚀 Release $(VERSION) ready!"
|
||||||
|
@echo "📁 Files in dist/packages/:"
|
||||||
|
@ls -la dist/packages/
|
||||||
|
|
||||||
|
deps: ## Download dependencies
|
||||||
|
@echo "📥 Downloading dependencies..."
|
||||||
|
go mod download
|
||||||
|
go mod tidy
|
||||||
|
@echo "✅ Dependencies updated"
|
||||||
|
|
||||||
|
dev-deps: ## Install development dependencies
|
||||||
|
@echo "🛠️ Installing development dependencies..."
|
||||||
|
@if ! command -v golangci-lint >/dev/null 2>&1; then \
|
||||||
|
echo "Installing golangci-lint..."; \
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest; \
|
||||||
|
fi
|
||||||
|
@echo "✅ Development dependencies installed"
|
||||||
|
|
||||||
|
run: build ## Build and run the application
|
||||||
|
@echo "🏃 Running $(BINARY_NAME)..."
|
||||||
|
./$(BINARY_NAME)
|
||||||
|
|
||||||
|
demo: build ## Run a demo of the find command
|
||||||
|
@echo "🎬 Running demo..."
|
||||||
|
./$(BINARY_NAME) find --top 3
|
||||||
|
|
||||||
|
check: fmt lint test ## Run all checks (format, lint, test)
|
||||||
|
@echo "✅ All checks passed!"
|
||||||
|
|
||||||
|
help: ## Show this help message
|
||||||
|
@echo "FastestMirror Build System"
|
||||||
|
@echo "Usage: make [target]"
|
||||||
|
@echo ""
|
||||||
|
@echo "Targets:"
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.DEFAULT_GOAL := help
|
99
PROJECT_SUMMARY.md
Normal file
99
PROJECT_SUMMARY.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# FastestMirror Project Summary
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
FastestMirror is a professional Go application that automatically detects Linux distributions and finds the fastest package repository mirrors. It's designed to be both technically impressive and subtly entertaining - perfect for a resume project.
|
||||||
|
|
||||||
|
## Key Technical Features
|
||||||
|
|
||||||
|
### 🏗️ Architecture
|
||||||
|
- **Clean Architecture**: Organized with `cmd/`, `internal/`, and `pkg/` structure following Go best practices
|
||||||
|
- **Modular Design**: Separate packages for distribution detection, mirror testing, and configuration management
|
||||||
|
- **Concurrent Processing**: Uses Go goroutines for parallel mirror testing
|
||||||
|
- **Error Handling**: Robust error handling throughout with meaningful error messages
|
||||||
|
|
||||||
|
### 🔍 Distribution Detection
|
||||||
|
- Parses `/etc/os-release`, `/etc/lsb-release`, and fallback detection methods
|
||||||
|
- Supports major Linux families: Debian, Arch, Fedora, openSUSE, Gentoo, Alpine, etc.
|
||||||
|
- Graceful fallback mechanisms for edge cases
|
||||||
|
|
||||||
|
### ⚡ Mirror Testing Infrastructure
|
||||||
|
- Concurrent HTTP speed testing with configurable timeouts
|
||||||
|
- Latency measurement and download speed calculation
|
||||||
|
- Smart scoring algorithm combining multiple metrics
|
||||||
|
- Proper connection handling and resource cleanup
|
||||||
|
|
||||||
|
### 🎨 User Experience
|
||||||
|
- Beautiful CLI interface with colors and progress bars
|
||||||
|
- Professional help text with subtle humor
|
||||||
|
- Comprehensive command structure with subcommands
|
||||||
|
- Safe operations with backup and dry-run modes
|
||||||
|
|
||||||
|
### 🔧 Configuration Management
|
||||||
|
- Safe backup creation with timestamps
|
||||||
|
- Support for major package manager configurations
|
||||||
|
- Atomic operations with rollback capability
|
||||||
|
- Proper permission handling and root privilege checks
|
||||||
|
|
||||||
|
## Commands Implemented
|
||||||
|
|
||||||
|
### `fastestmirror find`
|
||||||
|
- Auto-detects distribution
|
||||||
|
- Tests mirrors concurrently
|
||||||
|
- Shows ranked results with performance metrics
|
||||||
|
- Configurable timeout and result count
|
||||||
|
|
||||||
|
### `fastestmirror apply`
|
||||||
|
- Requires appropriate privileges
|
||||||
|
- Creates automatic backups
|
||||||
|
- Applies fastest mirror safely
|
||||||
|
- Provides post-application instructions
|
||||||
|
|
||||||
|
### `fastestmirror version`
|
||||||
|
- Shows build information
|
||||||
|
- Displays Go version and architecture
|
||||||
|
- Links to repository
|
||||||
|
|
||||||
|
## Technical Highlights
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- **Cobra**: Professional CLI framework
|
||||||
|
- **Color Libraries**: Beautiful terminal output
|
||||||
|
- **Progress Bars**: Visual feedback during operations
|
||||||
|
- **Standard Library**: Extensive use of Go's robust standard library
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
- Comprehensive error handling
|
||||||
|
- Clean separation of concerns
|
||||||
|
- Concurrent operations with proper synchronization
|
||||||
|
- Resource management and cleanup
|
||||||
|
|
||||||
|
### Safety Features
|
||||||
|
- Automatic configuration backups
|
||||||
|
- Dry-run mode for testing
|
||||||
|
- Root privilege validation
|
||||||
|
- Rollback capabilities on failure
|
||||||
|
|
||||||
|
## Subtle Humor Elements
|
||||||
|
The project includes professional yet entertaining elements:
|
||||||
|
- Witty error messages ("your connection might be fucked")
|
||||||
|
- Humorous descriptions in help text
|
||||||
|
- Satirical comments in code and documentation
|
||||||
|
- Professional tone with personality
|
||||||
|
|
||||||
|
## Resume Value
|
||||||
|
This project demonstrates:
|
||||||
|
- **Go Proficiency**: Modern Go idioms and best practices
|
||||||
|
- **Systems Programming**: Linux distribution detection and configuration management
|
||||||
|
- **Concurrent Programming**: Goroutines and proper synchronization
|
||||||
|
- **CLI Development**: Professional command-line interface design
|
||||||
|
- **Error Handling**: Robust error management and user feedback
|
||||||
|
- **Testing Infrastructure**: Network operations and performance measurement
|
||||||
|
- **Software Architecture**: Clean, modular design patterns
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
- **Location**: `https://git.gostacks.org/iwasforcedtobehere/fastestmirror`
|
||||||
|
- **Author**: `@iwasforcedtobehere`
|
||||||
|
- **License**: MIT
|
||||||
|
- **Build System**: Comprehensive Makefile with cross-platform builds
|
||||||
|
|
||||||
|
This project showcases technical competence while maintaining personality - exactly what makes a great resume project stand out!
|
271
README.md
Normal file
271
README.md
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
# FastestMirror 🚀
|
||||||
|
|
||||||
|
*Because waiting for slow package mirrors is like watching paint dry in a fucking hurricane.*
|
||||||
|
|
||||||
|
FastestMirror is a blazingly fast, auto-magical tool that finds and configures the fastest package repository mirrors for your Linux distribution. No more suffering through downloads that move slower than continental drift!
|
||||||
|
|
||||||
|
## Features That'll Make You Smile 😊
|
||||||
|
|
||||||
|
- **🔍 Auto-Detection**: Automatically detects your Linux distribution (because we're not psychic, but we're pretty close)
|
||||||
|
- **⚡ Concurrent Testing**: Tests multiple mirrors simultaneously using Go's goroutines (multithreading that actually works)
|
||||||
|
- **🎨 Beautiful Output**: Colorful terminal interface that doesn't make your eyes bleed
|
||||||
|
- **📊 Smart Scoring**: Combines speed and latency metrics to find the objectively best mirror
|
||||||
|
- **🔒 Safe Operations**: Always backs up your config before making changes (we're not monsters)
|
||||||
|
- **🌍 Wide Support**: Works with major Linux distributions and their families
|
||||||
|
|
||||||
|
## Supported Distributions
|
||||||
|
|
||||||
|
| Distribution Family | Distributions | Config File |
|
||||||
|
|-------------------|---------------|-------------|
|
||||||
|
| **Debian** | Ubuntu, Debian, Mint, Pop!_OS, Elementary, Kali | `/etc/apt/sources.list` |
|
||||||
|
| **Arch** | Arch Linux, Manjaro, EndeavourOS, Artix | `/etc/pacman.d/mirrorlist` |
|
||||||
|
| **Fedora** | Fedora, CentOS, RHEL, Rocky Linux, AlmaLinux | `/etc/yum.repos.d/` |
|
||||||
|
| **openSUSE** | openSUSE, SLES | `/etc/zypp/repos.d/` |
|
||||||
|
| **Others** | Gentoo, Alpine, Slackware, Void | Various configs |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### From Binary (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download the latest release
|
||||||
|
curl -L https://git.gostacks.org/iwasforcedtobehere/fastestmirror/releases/latest/download/fastestmirror-linux-amd64 -o fastestmirror
|
||||||
|
|
||||||
|
# Make it executable
|
||||||
|
chmod +x fastestmirror
|
||||||
|
|
||||||
|
# Move to your PATH
|
||||||
|
sudo mv fastestmirror /usr/local/bin/
|
||||||
|
|
||||||
|
# Verify it works
|
||||||
|
fastestmirror --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Source (For the Brave)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone this beautiful repository
|
||||||
|
git clone https://git.gostacks.org/iwasforcedtobehere/fastestmirror.git
|
||||||
|
|
||||||
|
# Enter the dragon's lair
|
||||||
|
cd fastestmirror
|
||||||
|
|
||||||
|
# Build the damn thing
|
||||||
|
go build -o fastestmirror
|
||||||
|
|
||||||
|
# Install it system-wide
|
||||||
|
sudo cp fastestmirror /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package Managers (Coming Soon™)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu (when we get our shit together)
|
||||||
|
sudo apt install fastestmirror
|
||||||
|
|
||||||
|
# Arch Linux (AUR package pending)
|
||||||
|
yay -S fastestmirror
|
||||||
|
|
||||||
|
# Fedora (RPM coming soon)
|
||||||
|
sudo dnf install fastestmirror
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Find the Fastest Mirrors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic usage - finds and displays fastest mirrors
|
||||||
|
fastestmirror find
|
||||||
|
|
||||||
|
# Show more results
|
||||||
|
fastestmirror find --top 10
|
||||||
|
|
||||||
|
# Increase timeout for slower connections
|
||||||
|
fastestmirror find --timeout 30
|
||||||
|
|
||||||
|
# Verbose output (because you like details)
|
||||||
|
fastestmirror find --verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apply the Fastest Mirror
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Apply the fastest mirror (requires root)
|
||||||
|
sudo fastestmirror apply
|
||||||
|
|
||||||
|
# See what would happen without making changes
|
||||||
|
fastestmirror apply --dry-run
|
||||||
|
|
||||||
|
# Force application without confirmation (danger zone!)
|
||||||
|
sudo fastestmirror apply --force
|
||||||
|
|
||||||
|
# Use custom backup location
|
||||||
|
sudo fastestmirror apply --backup /home/user/my-backup.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show version and build info
|
||||||
|
fastestmirror version
|
||||||
|
|
||||||
|
# Show help
|
||||||
|
fastestmirror --help
|
||||||
|
|
||||||
|
# Get help for specific commands
|
||||||
|
fastestmirror find --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
🔍 Detecting your Linux distribution...
|
||||||
|
📦 Found: Ubuntu 22.04.3 LTS (debian family)
|
||||||
|
🔧 Loading mirrors for debian family...
|
||||||
|
⚡ Testing 8 mirrors (timeout: 10s)...
|
||||||
|
Testing mirrors ████████████████████████████████████████████████████ 8/8
|
||||||
|
|
||||||
|
🎉 Testing complete! Here are your results:
|
||||||
|
|
||||||
|
Rank Mirror URL Latency Speed Score
|
||||||
|
─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
#1 http://mirror.kakao.com/ubuntu/ 45ms 15.2 MB/s 174.2
|
||||||
|
#2 http://mirror.math.princeton.edu/pub/ubuntu/ 89ms 12.8 MB/s 139.1
|
||||||
|
#3 http://mirrors.kernel.org/ubuntu/ 156ms 8.9 MB/s 95.4
|
||||||
|
#4 http://archive.ubuntu.com/ubuntu/ 234ms 6.2 MB/s 66.3
|
||||||
|
#5 http://us.archive.ubuntu.com/ubuntu/ 312ms 4.1 MB/s 44.2
|
||||||
|
|
||||||
|
🏆 Winner: http://mirror.kakao.com/ubuntu/
|
||||||
|
⚡ This bad boy clocks in at 15.2 MB/s with 45ms latency
|
||||||
|
|
||||||
|
💡 To apply the fastest mirror, run: fastestmirror apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
FastestMirror works out of the box with sensible defaults, but you can customize its behavior:
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set default timeout (in seconds)
|
||||||
|
export FASTESTMIRROR_TIMEOUT=30
|
||||||
|
|
||||||
|
# Set default number of results to show
|
||||||
|
export FASTESTMIRROR_TOP_COUNT=5
|
||||||
|
|
||||||
|
# Enable verbose output by default
|
||||||
|
export FASTESTMIRROR_VERBOSE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Config File (Future Feature)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ~/.config/fastestmirror/config.yaml
|
||||||
|
timeout: 30
|
||||||
|
top_count: 5
|
||||||
|
verbose: false
|
||||||
|
backup_dir: /etc/fastestmirror/backups
|
||||||
|
custom_mirrors:
|
||||||
|
debian:
|
||||||
|
- "https://my-custom-mirror.example.com/debian/"
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works (The Magic Behind the Scenes)
|
||||||
|
|
||||||
|
1. **Distribution Detection**: Parses `/etc/os-release`, `/etc/lsb-release`, and distribution-specific files to identify your system
|
||||||
|
2. **Mirror Discovery**: Uses curated lists of official mirrors for each distribution family
|
||||||
|
3. **Concurrent Testing**: Spawns goroutines to test multiple mirrors simultaneously
|
||||||
|
4. **Performance Metrics**: Measures both latency (ping-like) and download speed using representative files
|
||||||
|
5. **Smart Scoring**: Combines metrics with weighted scoring algorithm to rank mirrors objectively
|
||||||
|
6. **Safe Application**: Creates backups before modifying system configuration files
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
- **Speed Testing**: Downloads small representative files (Release files, database files, etc.)
|
||||||
|
- **Timeout Handling**: Respects timeout settings to avoid hanging on dead mirrors
|
||||||
|
- **Concurrent Limits**: Limits concurrent connections to avoid overwhelming your network
|
||||||
|
- **Retry Logic**: Automatically retries failed tests with exponential backoff
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Build for current platform
|
||||||
|
go build -o fastestmirror
|
||||||
|
|
||||||
|
# Build for all platforms
|
||||||
|
make build-all
|
||||||
|
|
||||||
|
# Create release packages
|
||||||
|
make package
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
We welcome contributions! Please:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
fastestmirror/
|
||||||
|
├── cmd/ # CLI commands and subcommands
|
||||||
|
├── internal/ # Internal packages
|
||||||
|
│ ├── distro/ # Distribution detection logic
|
||||||
|
│ ├── mirror/ # Mirror testing and management
|
||||||
|
│ └── config/ # Configuration management
|
||||||
|
├── pkg/ # Public packages (if any)
|
||||||
|
└── scripts/ # Build and deployment scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**"Could not detect distribution"**
|
||||||
|
- Make sure you're running on a supported Linux distribution
|
||||||
|
- Check that `/etc/os-release` exists and is readable
|
||||||
|
- Try running with `--verbose` for more details
|
||||||
|
|
||||||
|
**"No working mirrors found"**
|
||||||
|
- Check your internet connection
|
||||||
|
- Try increasing the timeout with `--timeout 60`
|
||||||
|
- Some distributions might have all mirrors temporarily down (rare but it happens)
|
||||||
|
|
||||||
|
**"Permission denied" when applying**
|
||||||
|
- Make sure you're running with `sudo` when using the `apply` command
|
||||||
|
- Check that the configuration file is writable
|
||||||
|
|
||||||
|
**Tests are slow or timing out**
|
||||||
|
- Increase timeout with `--timeout 30` or higher
|
||||||
|
- Check if your network has restrictions on concurrent connections
|
||||||
|
- Some corporate firewalls block multiple simultaneous connections
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- Thanks to all the mirror maintainers who keep our packages flowing
|
||||||
|
- Inspired by tools like `netselect-apt`, `reflector`, and `fastestmirror` plugin
|
||||||
|
- Built with Go because life's too short for slow languages
|
||||||
|
- Coffee and mild frustration with slow mirrors
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This tool modifies system configuration files. While we take precautions (backups, dry-run mode, etc.), use at your own risk. We're not responsible if you accidentally break your package manager and have to reinstall your entire system. That said, the risk is minimal and the speed gains are fucking worth it.
|
162
cmd/apply.go
Normal file
162
cmd/apply.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/fastestmirror/internal/config"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/fastestmirror/internal/distro"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/fastestmirror/internal/mirror"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var applyCmd = &cobra.Command{
|
||||||
|
Use: "apply",
|
||||||
|
Short: "Apply the fastest mirror to your system configuration",
|
||||||
|
Long: `Apply the fastest mirror found to your system's package manager configuration.
|
||||||
|
|
||||||
|
This will:
|
||||||
|
• Backup your current configuration (because we're not savages)
|
||||||
|
• Update your package manager to use the fastest mirror
|
||||||
|
• Test the new configuration
|
||||||
|
|
||||||
|
WARNING: This modifies system files and requires root privileges.
|
||||||
|
Don't blame us if you break something - you've been warned! 🔥`,
|
||||||
|
RunE: runApply,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
force bool
|
||||||
|
dryRun bool
|
||||||
|
backupPath string
|
||||||
|
)
|
||||||
|
|
||||||
|
func runApply(cmd *cobra.Command, args []string) error {
|
||||||
|
// Check if running as root
|
||||||
|
if os.Geteuid() != 0 && !dryRun {
|
||||||
|
binaryName := "fastestmirror"
|
||||||
|
if len(os.Args) > 0 {
|
||||||
|
binaryName = filepath.Base(os.Args[0])
|
||||||
|
}
|
||||||
|
return fmt.Errorf("this command requires root privileges. Try: sudo %s apply", binaryName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect distribution
|
||||||
|
fmt.Println("🔍 Detecting distribution...")
|
||||||
|
distroInfo, err := distro.DetectDistribution()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not detect distribution: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !distroInfo.IsSupported() {
|
||||||
|
return fmt.Errorf("distribution %s is not supported yet", distroInfo.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
configFile := distroInfo.GetConfigFile()
|
||||||
|
if configFile == "" {
|
||||||
|
return fmt.Errorf("no configuration file defined for %s", distroInfo.Family)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Printf("🧪 DRY RUN: Would modify %s\n", configFile)
|
||||||
|
fmt.Println("🧪 DRY RUN: Would backup existing configuration")
|
||||||
|
fmt.Println("🧪 DRY RUN: Would test fastest mirror and apply changes")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create configuration manager
|
||||||
|
configManager := config.NewConfigManager(distroInfo)
|
||||||
|
|
||||||
|
// First, find the fastest mirror
|
||||||
|
fmt.Printf("🔍 Finding fastest mirror for %s...\n", color.YellowString(distroInfo.Family))
|
||||||
|
|
||||||
|
mirrorList := mirror.NewMirrorList(distroInfo.Family)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = mirrorList.LoadMirrors(ctx, distroInfo.Family)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load mirrors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mirrorList.TestMirrors(ctx, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to test mirrors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bestMirror := mirrorList.GetBest()
|
||||||
|
if bestMirror == nil {
|
||||||
|
return fmt.Errorf("no working mirrors found - your connection might be fucked")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("🏆 Best mirror found: %s\n", color.GreenString(bestMirror.URL))
|
||||||
|
fmt.Printf("⚡ Speed: %.1f MB/s, Latency: %dms\n", bestMirror.Speed, bestMirror.Latency.Milliseconds())
|
||||||
|
|
||||||
|
// Ask for confirmation unless forced
|
||||||
|
if !force {
|
||||||
|
fmt.Printf("\n⚠️ This will modify %s\n", color.YellowString(configFile))
|
||||||
|
fmt.Print("Do you want to continue? [y/N]: ")
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
|
||||||
|
if strings.ToLower(response) != "y" && strings.ToLower(response) != "yes" {
|
||||||
|
fmt.Println("Operation cancelled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup
|
||||||
|
fmt.Println("💾 Creating backup...")
|
||||||
|
backupPath, err := configManager.BackupConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("✅ Backup created: %s\n", color.GreenString(backupPath))
|
||||||
|
|
||||||
|
// Apply the fastest mirror
|
||||||
|
fmt.Println("🔧 Applying fastest mirror configuration...")
|
||||||
|
err = configManager.ApplyMirror(bestMirror.URL)
|
||||||
|
if err != nil {
|
||||||
|
// Try to restore backup on failure
|
||||||
|
fmt.Printf("❌ Failed to apply configuration: %v\n", err)
|
||||||
|
fmt.Println("<22> Attempting to restore backup...")
|
||||||
|
|
||||||
|
if restoreErr := configManager.RestoreBackup(backupPath); restoreErr != nil {
|
||||||
|
return fmt.Errorf("configuration failed AND backup restore failed: %v (original error: %v)", restoreErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✅ Backup restored successfully")
|
||||||
|
return fmt.Errorf("configuration application failed, backup restored: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("🎉 Successfully applied fastest mirror!\n")
|
||||||
|
fmt.Printf("🔗 New mirror: %s\n", color.GreenString(bestMirror.URL))
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("💡 Don't forget to run your package manager's update command:\n")
|
||||||
|
|
||||||
|
switch distroInfo.Family {
|
||||||
|
case "debian":
|
||||||
|
fmt.Printf(" %s\n", color.CyanString("sudo apt update"))
|
||||||
|
case "arch":
|
||||||
|
fmt.Printf(" %s\n", color.CyanString("sudo pacman -Sy"))
|
||||||
|
case "fedora":
|
||||||
|
fmt.Printf(" %s\n", color.CyanString("sudo dnf clean all && sudo dnf makecache"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(applyCmd)
|
||||||
|
|
||||||
|
applyCmd.Flags().BoolVarP(&force, "force", "f", false, "Force application without confirmation")
|
||||||
|
applyCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "Show what would be done without making changes")
|
||||||
|
applyCmd.Flags().StringVarP(&backupPath, "backup", "b", "", "Custom backup file path")
|
||||||
|
}
|
171
cmd/find.go
Normal file
171
cmd/find.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/fastestmirror/internal/distro"
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/fastestmirror/internal/mirror"
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
verbose bool
|
||||||
|
timeout int
|
||||||
|
topCount int
|
||||||
|
)
|
||||||
|
|
||||||
|
var findCmd = &cobra.Command{
|
||||||
|
Use: "find",
|
||||||
|
Short: "Find the fastest mirrors for your distribution",
|
||||||
|
Long: `Find and test mirrors to determine which ones will make your package
|
||||||
|
manager fly faster than a caffinated developer on Monday morning.
|
||||||
|
|
||||||
|
This command will:
|
||||||
|
• Auto-detect your Linux distribution
|
||||||
|
• Test multiple mirrors concurrently
|
||||||
|
• Rank them by speed and reliability
|
||||||
|
• Show you the results in glorious color`,
|
||||||
|
RunE: runFind,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runFind(cmd *cobra.Command, args []string) error {
|
||||||
|
// Detect distribution
|
||||||
|
fmt.Println(color.CyanString("🔍 Detecting your Linux distribution..."))
|
||||||
|
|
||||||
|
distroInfo, err := distro.DetectDistribution()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fuck me, couldn't detect your distro: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("📦 Found: %s\n", color.GreenString(distroInfo.String()))
|
||||||
|
|
||||||
|
if !distroInfo.IsSupported() {
|
||||||
|
return fmt.Errorf("sorry, %s isn't supported yet - but hey, you're using something exotic! 🦄", distroInfo.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mirrors for this distribution family
|
||||||
|
fmt.Printf("🔧 Loading mirrors for %s family...\n", color.YellowString(distroInfo.Family))
|
||||||
|
|
||||||
|
mirrorList := mirror.NewMirrorList(distroInfo.Family)
|
||||||
|
|
||||||
|
// Load mirrors from official sources
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second*5)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = mirrorList.LoadMirrors(ctx, distroInfo.Family)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load mirrors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("⚡ Testing %d mirrors (timeout: %ds each)...\n",
|
||||||
|
len(mirrorList.Mirrors), timeout)
|
||||||
|
|
||||||
|
err = mirrorList.TestMirrors(ctx, time.Duration(timeout)*time.Second)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mirror testing failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
fmt.Println("\n" + color.GreenString("🎉 Testing complete! Here are your results:"))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
topMirrors := mirrorList.GetTop(topCount)
|
||||||
|
allMirrors := mirrorList.GetAll()
|
||||||
|
|
||||||
|
// Show summary of all tests
|
||||||
|
successCount := 0
|
||||||
|
for _, mirror := range allMirrors {
|
||||||
|
if mirror.Error == nil {
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
fmt.Printf("📊 Test Summary: %d/%d mirrors responded successfully\n", successCount, len(allMirrors))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
if len(topMirrors) == 0 {
|
||||||
|
fmt.Println(color.RedString("😱 No working mirrors found. Your internet might be fucked, or all mirrors are down."))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display top mirrors in a nice table format
|
||||||
|
fmt.Printf("%-4s %-50s %-12s %-10s %-8s\n",
|
||||||
|
color.CyanString("Rank"),
|
||||||
|
color.CyanString("Mirror URL"),
|
||||||
|
color.CyanString("Latency"),
|
||||||
|
color.CyanString("Speed"),
|
||||||
|
color.CyanString("Score"))
|
||||||
|
fmt.Println(color.HiBlackString("─────────────────────────────────────────────────────────────────────────────────"))
|
||||||
|
|
||||||
|
for i, m := range topMirrors {
|
||||||
|
rank := color.YellowString("#%d", i+1)
|
||||||
|
url := m.URL
|
||||||
|
if len(url) > 48 {
|
||||||
|
url = url[:45] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
latencyStr := color.GreenString("%dms", m.Latency.Milliseconds())
|
||||||
|
if m.Latency > 500*time.Millisecond {
|
||||||
|
latencyStr = color.RedString("%dms", m.Latency.Milliseconds())
|
||||||
|
} else if m.Latency > 200*time.Millisecond {
|
||||||
|
latencyStr = color.YellowString("%dms", m.Latency.Milliseconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
speedStr := color.GreenString("%.1f MB/s", m.Speed)
|
||||||
|
if m.Speed < 1.0 {
|
||||||
|
speedStr = color.RedString("%.1f MB/s", m.Speed)
|
||||||
|
} else if m.Speed < 5.0 {
|
||||||
|
speedStr = color.YellowString("%.1f MB/s", m.Speed)
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreStr := color.CyanString("%.1f", m.Score)
|
||||||
|
|
||||||
|
fmt.Printf("%-4s %-50s %-12s %-10s %-8s\n",
|
||||||
|
rank, url, latencyStr, speedStr, scoreStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
best := mirrorList.GetBest()
|
||||||
|
if best != nil {
|
||||||
|
fmt.Printf("🏆 Winner: %s\n", color.GreenString(best.URL))
|
||||||
|
fmt.Printf("⚡ This bad boy clocks in at %.1f MB/s with %dms latency\n",
|
||||||
|
best.Speed, best.Latency.Milliseconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show failed mirrors in verbose mode
|
||||||
|
if verbose {
|
||||||
|
failedMirrors := make([]mirror.Mirror, 0)
|
||||||
|
for _, m := range allMirrors {
|
||||||
|
if m.Error != nil {
|
||||||
|
failedMirrors = append(failedMirrors, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(failedMirrors) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("❌ Failed mirrors (%d):\n", len(failedMirrors))
|
||||||
|
for _, m := range failedMirrors {
|
||||||
|
fmt.Printf(" %s - %s\n", color.RedString(m.URL), m.Error.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("💡 To apply the fastest mirror, run: %s\n",
|
||||||
|
color.YellowString("fastestmirror apply"))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(findCmd)
|
||||||
|
|
||||||
|
findCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show verbose output")
|
||||||
|
findCmd.Flags().IntVarP(&timeout, "timeout", "t", 10, "Timeout for each mirror test in seconds")
|
||||||
|
findCmd.Flags().IntVarP(&topCount, "top", "n", 5, "Number of top mirrors to display")
|
||||||
|
}
|
31
cmd/root.go
Normal file
31
cmd/root.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "fastestmirror",
|
||||||
|
Short: "Find and set the fastest package mirror for your Linux distribution",
|
||||||
|
Long: `FastestMirror - Because waiting for slow mirrors is like watching paint dry in a hurricane.
|
||||||
|
|
||||||
|
This tool automatically detects your Linux distribution and finds the fastest
|
||||||
|
package repository mirrors available. No more suffering through downloads that
|
||||||
|
move slower than continental drift.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
• Automatic distribution detection
|
||||||
|
• Concurrent mirror speed testing
|
||||||
|
• Colorful output that doesn't hurt your eyes
|
||||||
|
• Backup and restore functionality
|
||||||
|
• Works with major distros (Debian, Ubuntu, Arch, Fedora, etc.)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute runs the root command
|
||||||
|
func Execute() error {
|
||||||
|
return rootCmd.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Global flags and configuration initialization
|
||||||
|
}
|
34
cmd/version.go
Normal file
34
cmd/version.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
version = "dev"
|
||||||
|
commit = "unknown"
|
||||||
|
buildTime = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
var versionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Show version information",
|
||||||
|
Long: `Display version, build time, and other build information for FastestMirror.`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Printf("FastestMirror %s\n", version)
|
||||||
|
fmt.Printf("Git Commit: %s\n", commit)
|
||||||
|
fmt.Printf("Build Time: %s\n", buildTime)
|
||||||
|
fmt.Printf("Go Version: %s\n", runtime.Version())
|
||||||
|
fmt.Printf("OS/Arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Repository: https://git.gostacks.org/iwasforcedtobehere/fastestmirror")
|
||||||
|
fmt.Println("Author: @iwasforcedtobehere")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
}
|
20
go.mod
Normal file
20
go.mod
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module git.gostacks.org/iwasforcedtobehere/fastestmirror
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fatih/color v1.16.0
|
||||||
|
github.com/schollz/progressbar/v3 v3.14.1
|
||||||
|
github.com/spf13/cobra v1.8.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.4 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
|
golang.org/x/term v0.15.0 // indirect
|
||||||
|
)
|
40
go.sum
Normal file
40
go.sum
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||||
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||||
|
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/schollz/progressbar/v3 v3.14.1 h1:VD+MJPCr4s3wdhTc7OEJ/Z3dAeBzJ7yKH/P4lC5yRTI=
|
||||||
|
github.com/schollz/progressbar/v3 v3.14.1/go.mod h1:Zc9xXneTzWXF81TGoqL71u0sBPjULtEHYtj/WVgVy8E=
|
||||||
|
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||||
|
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||||
|
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
|
||||||
|
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
|
||||||
|
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
218
internal/config/manager.go
Normal file
218
internal/config/manager.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.gostacks.org/iwasforcedtobehere/fastestmirror/internal/distro"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigManager handles backup and modification of distribution config files
|
||||||
|
type ConfigManager struct {
|
||||||
|
DistroInfo *distro.DistroInfo
|
||||||
|
BackupDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfigManager creates a new configuration manager
|
||||||
|
func NewConfigManager(distroInfo *distro.DistroInfo) *ConfigManager {
|
||||||
|
return &ConfigManager{
|
||||||
|
DistroInfo: distroInfo,
|
||||||
|
BackupDir: "/etc/fastestmirror/backups",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupConfig creates a backup of the current configuration
|
||||||
|
func (cm *ConfigManager) BackupConfig() (string, error) {
|
||||||
|
configFile := cm.DistroInfo.GetConfigFile()
|
||||||
|
if configFile == "" {
|
||||||
|
return "", fmt.Errorf("no configuration file defined for %s", cm.DistroInfo.Family)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup directory if it doesn't exist
|
||||||
|
if err := os.MkdirAll(cm.BackupDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create backup directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate backup filename with timestamp
|
||||||
|
timestamp := time.Now().Format("20060102-150405")
|
||||||
|
backupName := fmt.Sprintf("%s.backup.%s", filepath.Base(configFile), timestamp)
|
||||||
|
backupPath := filepath.Join(cm.BackupDir, backupName)
|
||||||
|
|
||||||
|
// Copy original file to backup location
|
||||||
|
if err := cm.copyFile(configFile, backupPath); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create backup: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return backupPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyMirror applies the fastest mirror to the configuration
|
||||||
|
func (cm *ConfigManager) ApplyMirror(mirrorURL string) error {
|
||||||
|
switch cm.DistroInfo.Family {
|
||||||
|
case "debian":
|
||||||
|
return cm.applyDebianMirror(mirrorURL)
|
||||||
|
case "arch":
|
||||||
|
return cm.applyArchMirror(mirrorURL)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("configuration management for %s family not implemented yet", cm.DistroInfo.Family)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDebianMirror updates /etc/apt/sources.list for Debian-based systems
|
||||||
|
func (cm *ConfigManager) applyDebianMirror(mirrorURL string) error {
|
||||||
|
configFile := "/etc/apt/sources.list"
|
||||||
|
|
||||||
|
// Read current configuration
|
||||||
|
file, err := os.Open(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read %s: %w", configFile, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
|
||||||
|
// Process each line and replace mirror URLs
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if strings.TrimSpace(line) == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||||
|
// Keep comments and empty lines as-is
|
||||||
|
lines = append(lines, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and update deb/deb-src lines
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(line), "deb") {
|
||||||
|
updatedLine := cm.updateDebianLine(line, mirrorURL)
|
||||||
|
lines = append(lines, updatedLine)
|
||||||
|
} else {
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("error reading %s: %w", configFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write updated configuration
|
||||||
|
return cm.writeLines(configFile, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateDebianLine updates a single deb line with the new mirror URL
|
||||||
|
func (cm *ConfigManager) updateDebianLine(line, newMirrorURL string) string {
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) < 3 {
|
||||||
|
return line // Invalid line, keep as-is
|
||||||
|
}
|
||||||
|
|
||||||
|
// parts[0] = "deb" or "deb-src"
|
||||||
|
// parts[1] = URL
|
||||||
|
// parts[2] = suite/codename
|
||||||
|
// parts[3+] = components
|
||||||
|
|
||||||
|
parts[1] = strings.TrimSuffix(newMirrorURL, "/") + "/"
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyArchMirror updates /etc/pacman.d/mirrorlist for Arch-based systems
|
||||||
|
func (cm *ConfigManager) applyArchMirror(mirrorURL string) error {
|
||||||
|
configFile := "/etc/pacman.d/mirrorlist"
|
||||||
|
|
||||||
|
// Read current configuration
|
||||||
|
file, err := os.Open(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read %s: %w", configFile, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
firstMirrorAdded := false
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
|
||||||
|
// Add the fastest mirror at the top (uncommented)
|
||||||
|
lines = append(lines, fmt.Sprintf("# FastestMirror: Added %s", time.Now().Format("2006-01-02 15:04:05")))
|
||||||
|
lines = append(lines, fmt.Sprintf("Server = %s/$repo/os/$arch", strings.TrimSuffix(mirrorURL, "/")))
|
||||||
|
lines = append(lines, "")
|
||||||
|
firstMirrorAdded = true
|
||||||
|
|
||||||
|
// Process existing lines, commenting out other Server entries
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
// Comment out existing Server lines (but keep them for reference)
|
||||||
|
if strings.HasPrefix(trimmed, "Server = ") && firstMirrorAdded {
|
||||||
|
lines = append(lines, "#"+line)
|
||||||
|
} else {
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return fmt.Errorf("error reading %s: %w", configFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write updated configuration
|
||||||
|
return cm.writeLines(configFile, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile copies a file from src to dst
|
||||||
|
func (cm *ConfigManager) copyFile(src, dst string) error {
|
||||||
|
sourceFile, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(destFile, sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy file permissions
|
||||||
|
srcInfo, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Chmod(dst, srcInfo.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeLines writes lines to a file
|
||||||
|
func (cm *ConfigManager) writeLines(filename string, lines []string) error {
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
writer := bufio.NewWriter(file)
|
||||||
|
for _, line := range lines {
|
||||||
|
if _, err := writer.WriteString(line + "\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreBackup restores a configuration from backup
|
||||||
|
func (cm *ConfigManager) RestoreBackup(backupPath string) error {
|
||||||
|
configFile := cm.DistroInfo.GetConfigFile()
|
||||||
|
if configFile == "" {
|
||||||
|
return fmt.Errorf("no configuration file defined for %s", cm.DistroInfo.Family)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cm.copyFile(backupPath, configFile)
|
||||||
|
}
|
191
internal/distro/detect.go
Normal file
191
internal/distro/detect.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package distro
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DistroInfo represents Linux distribution information
|
||||||
|
type DistroInfo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Family string
|
||||||
|
Codename string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedDistros lists all distributions we can handle
|
||||||
|
var SupportedDistros = map[string]string{
|
||||||
|
"ubuntu": "debian",
|
||||||
|
"debian": "debian",
|
||||||
|
"mint": "debian",
|
||||||
|
"pop": "debian",
|
||||||
|
"elementary": "debian",
|
||||||
|
"kali": "debian",
|
||||||
|
"arch": "arch",
|
||||||
|
"manjaro": "arch",
|
||||||
|
"endeavouros": "arch",
|
||||||
|
"artix": "arch",
|
||||||
|
"fedora": "fedora",
|
||||||
|
"centos": "fedora",
|
||||||
|
"rhel": "fedora",
|
||||||
|
"rocky": "fedora",
|
||||||
|
"alma": "fedora",
|
||||||
|
"opensuse": "opensuse",
|
||||||
|
"sles": "opensuse",
|
||||||
|
"gentoo": "gentoo",
|
||||||
|
"alpine": "alpine",
|
||||||
|
"slackware": "slackware",
|
||||||
|
"void": "void",
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectDistribution attempts to detect the current Linux distribution
|
||||||
|
func DetectDistribution() (*DistroInfo, error) {
|
||||||
|
// Try /etc/os-release first (most reliable)
|
||||||
|
if info, err := parseOSRelease(); err == nil {
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to /etc/lsb-release
|
||||||
|
if info, err := parseLSBRelease(); err == nil {
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: check specific distribution files
|
||||||
|
if info, err := detectFromSpecificFiles(); err == nil {
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unable to detect Linux distribution - are you sure you're not running Windows? 🤔")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOSRelease parses /etc/os-release file
|
||||||
|
func parseOSRelease() (*DistroInfo, error) {
|
||||||
|
return parseKeyValueFile("/etc/os-release")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLSBRelease parses /etc/lsb-release file
|
||||||
|
func parseLSBRelease() (*DistroInfo, error) {
|
||||||
|
return parseKeyValueFile("/etc/lsb-release")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseKeyValueFile is a generic parser for key=value format files
|
||||||
|
func parseKeyValueFile(filename string) (*DistroInfo, error) {
|
||||||
|
file, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
info := &DistroInfo{}
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.Trim(strings.TrimSpace(parts[1]), `"'`)
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "ID", "DISTRIB_ID":
|
||||||
|
info.ID = strings.ToLower(value)
|
||||||
|
case "NAME", "DISTRIB_DESCRIPTION":
|
||||||
|
info.Name = value
|
||||||
|
case "VERSION", "DISTRIB_RELEASE":
|
||||||
|
info.Version = value
|
||||||
|
case "VERSION_CODENAME", "DISTRIB_CODENAME":
|
||||||
|
info.Codename = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.ID == "" {
|
||||||
|
return nil, fmt.Errorf("could not determine distribution ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine family
|
||||||
|
if family, exists := SupportedDistros[info.ID]; exists {
|
||||||
|
info.Family = family
|
||||||
|
} else {
|
||||||
|
info.Family = "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectFromSpecificFiles tries to detect distro from specific files
|
||||||
|
func detectFromSpecificFiles() (*DistroInfo, error) {
|
||||||
|
// Map of files to expected distribution IDs
|
||||||
|
checks := map[string]string{
|
||||||
|
"/etc/arch-release": "arch",
|
||||||
|
"/etc/gentoo-release": "gentoo",
|
||||||
|
"/etc/slackware-version": "slackware",
|
||||||
|
"/etc/alpine-release": "alpine",
|
||||||
|
"/etc/void-release": "void",
|
||||||
|
}
|
||||||
|
|
||||||
|
for file, distroID := range checks {
|
||||||
|
if _, err := os.Stat(file); err == nil {
|
||||||
|
info := &DistroInfo{
|
||||||
|
ID: distroID,
|
||||||
|
Name: strings.Title(distroID),
|
||||||
|
Family: SupportedDistros[distroID],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get version from file content
|
||||||
|
if content, err := os.ReadFile(file); err == nil {
|
||||||
|
versionRegex := regexp.MustCompile(`\d+\.?\d*\.?\d*`)
|
||||||
|
if match := versionRegex.FindString(string(content)); match != "" {
|
||||||
|
info.Version = match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no recognizable distribution files found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSupported checks if the distribution is supported
|
||||||
|
func (d *DistroInfo) IsSupported() bool {
|
||||||
|
_, exists := SupportedDistros[d.ID]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigFile returns the path to the main package configuration file
|
||||||
|
func (d *DistroInfo) GetConfigFile() string {
|
||||||
|
switch d.Family {
|
||||||
|
case "debian":
|
||||||
|
return "/etc/apt/sources.list"
|
||||||
|
case "arch":
|
||||||
|
return "/etc/pacman.d/mirrorlist"
|
||||||
|
case "fedora":
|
||||||
|
return "/etc/yum.repos.d" // Directory for multiple files
|
||||||
|
case "opensuse":
|
||||||
|
return "/etc/zypp/repos.d" // Directory for multiple files
|
||||||
|
case "gentoo":
|
||||||
|
return "/etc/portage/make.conf"
|
||||||
|
case "alpine":
|
||||||
|
return "/etc/apk/repositories"
|
||||||
|
case "slackware":
|
||||||
|
return "/etc/slackpkg/mirrors"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String provides a human-readable representation
|
||||||
|
func (d *DistroInfo) String() string {
|
||||||
|
return fmt.Sprintf("%s %s (%s family)", d.Name, d.Version, d.Family)
|
||||||
|
}
|
269
internal/mirror/fetcher.go
Normal file
269
internal/mirror/fetcher.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package mirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MirrorFetcher handles fetching mirror lists from official sources
|
||||||
|
type MirrorFetcher struct {
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMirrorFetcher creates a new mirror fetcher
|
||||||
|
func NewMirrorFetcher() *MirrorFetcher {
|
||||||
|
return &MirrorFetcher{
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchMirrors gets the complete list of mirrors for a distribution family
|
||||||
|
func (mf *MirrorFetcher) FetchMirrors(ctx context.Context, family string) ([]string, error) {
|
||||||
|
switch family {
|
||||||
|
case "arch":
|
||||||
|
return mf.fetchArchMirrors(ctx)
|
||||||
|
case "debian":
|
||||||
|
return mf.fetchDebianMirrors(ctx)
|
||||||
|
case "fedora":
|
||||||
|
return mf.fetchFedoraMirrors(ctx)
|
||||||
|
default:
|
||||||
|
// Fallback to hardcoded mirrors for unsupported families
|
||||||
|
return mf.getDefaultMirrors(family), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchArchMirrors fetches Arch Linux mirrors from the official mirror status
|
||||||
|
func (mf *MirrorFetcher) fetchArchMirrors(ctx context.Context) ([]string, error) {
|
||||||
|
// Arch Linux provides a JSON API for mirror status
|
||||||
|
url := "https://archlinux.org/mirrorlist/?protocol=https&use_mirror_status=on"
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := mf.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch Arch mirrors: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP %d from Arch mirror API", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mf.parseArchMirrorList(string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseArchMirrorList parses the Arch Linux mirror list format
|
||||||
|
func (mf *MirrorFetcher) parseArchMirrorList(content string) ([]string, error) {
|
||||||
|
var mirrors []string
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||||
|
|
||||||
|
// Look for lines like: #Server = https://mirror.example.com/archlinux/$repo/os/$arch
|
||||||
|
serverRegex := regexp.MustCompile(`^#?Server\s*=\s*(.+)`)
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if matches := serverRegex.FindStringSubmatch(line); matches != nil {
|
||||||
|
mirrorURL := strings.TrimSpace(matches[1])
|
||||||
|
// Remove the $repo/os/$arch part for base URL
|
||||||
|
if strings.Contains(mirrorURL, "$repo") {
|
||||||
|
mirrorURL = strings.Replace(mirrorURL, "/$repo/os/$arch", "/", 1)
|
||||||
|
}
|
||||||
|
if mirrorURL != "" && !strings.Contains(mirrorURL, "$") {
|
||||||
|
mirrors = append(mirrors, mirrorURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mirrors) == 0 {
|
||||||
|
return nil, fmt.Errorf("no mirrors found in Arch Linux mirror list")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mirrors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchDebianMirrors fetches Debian mirrors from the official mirror list
|
||||||
|
func (mf *MirrorFetcher) fetchDebianMirrors(ctx context.Context) ([]string, error) {
|
||||||
|
// Debian maintains a list of mirrors
|
||||||
|
url := "https://www.debian.org/mirror/list-full"
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := mf.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch Debian mirrors: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP %d from Debian mirror list", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mf.parseDebianMirrorList(string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDebianMirrorList parses the Debian mirror list HTML
|
||||||
|
func (mf *MirrorFetcher) parseDebianMirrorList(content string) ([]string, error) {
|
||||||
|
var mirrors []string
|
||||||
|
|
||||||
|
// Look for HTTP/HTTPS URLs in the HTML
|
||||||
|
urlRegex := regexp.MustCompile(`https?://[a-zA-Z0-9.-]+[a-zA-Z0-9.-/]*debian[a-zA-Z0-9.-/]*/?`)
|
||||||
|
matches := urlRegex.FindAllString(content, -1)
|
||||||
|
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, match := range matches {
|
||||||
|
// Clean up and normalize the URL
|
||||||
|
mirror := strings.TrimSpace(match)
|
||||||
|
mirror = strings.TrimSuffix(mirror, "/") + "/"
|
||||||
|
|
||||||
|
// Avoid duplicates and ensure it's a proper mirror
|
||||||
|
if !seen[mirror] && strings.Contains(mirror, "debian") {
|
||||||
|
mirrors = append(mirrors, mirror)
|
||||||
|
seen[mirror] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some known good mirrors if we don't find enough
|
||||||
|
knownMirrors := []string{
|
||||||
|
"http://deb.debian.org/debian/",
|
||||||
|
"http://ftp.us.debian.org/debian/",
|
||||||
|
"http://ftp.uk.debian.org/debian/",
|
||||||
|
"http://ftp.de.debian.org/debian/",
|
||||||
|
"http://mirrors.kernel.org/debian/",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, known := range knownMirrors {
|
||||||
|
if !seen[known] {
|
||||||
|
mirrors = append(mirrors, known)
|
||||||
|
seen[known] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mirrors) == 0 {
|
||||||
|
return nil, fmt.Errorf("no mirrors found in Debian mirror list")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mirrors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchFedoraMirrors fetches Fedora mirrors from the official mirror list
|
||||||
|
func (mf *MirrorFetcher) fetchFedoraMirrors(ctx context.Context) ([]string, error) {
|
||||||
|
// Fedora mirror list URL
|
||||||
|
url := "https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-39&arch=x86_64"
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := mf.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch Fedora mirrors: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP %d from Fedora mirror list", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mf.parseFedoraMirrorList(string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFedoraMirrorList parses the Fedora mirror list format
|
||||||
|
func (mf *MirrorFetcher) parseFedoraMirrorList(content string) ([]string, error) {
|
||||||
|
var mirrors []string
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(content))
|
||||||
|
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, "http") {
|
||||||
|
// Extract base URL (remove specific paths)
|
||||||
|
if idx := strings.Index(line, "/linux/"); idx != -1 {
|
||||||
|
baseURL := line[:idx] + "/fedora/linux/"
|
||||||
|
mirrors = append(mirrors, baseURL)
|
||||||
|
} else {
|
||||||
|
mirrors = append(mirrors, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mirrors) == 0 {
|
||||||
|
return nil, fmt.Errorf("no mirrors found in Fedora mirror list")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mirrors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultMirrors returns hardcoded mirrors as fallback
|
||||||
|
func (mf *MirrorFetcher) getDefaultMirrors(family string) []string {
|
||||||
|
defaultMirrors := map[string][]string{
|
||||||
|
"debian": {
|
||||||
|
"http://deb.debian.org/debian/",
|
||||||
|
"http://ftp.us.debian.org/debian/",
|
||||||
|
"http://ftp.uk.debian.org/debian/",
|
||||||
|
"http://ftp.de.debian.org/debian/",
|
||||||
|
"http://mirrors.kernel.org/debian/",
|
||||||
|
},
|
||||||
|
"arch": {
|
||||||
|
"https://mirror.rackspace.com/archlinux/",
|
||||||
|
"https://mirrors.kernel.org/archlinux/",
|
||||||
|
"https://mirror.math.princeton.edu/pub/archlinux/",
|
||||||
|
"https://mirrors.mit.edu/archlinux/",
|
||||||
|
"https://mirrors.liquidweb.com/archlinux/",
|
||||||
|
},
|
||||||
|
"fedora": {
|
||||||
|
"https://download.fedoraproject.org/pub/fedora/linux/",
|
||||||
|
"https://mirrors.kernel.org/fedora/",
|
||||||
|
"https://mirror.math.princeton.edu/pub/fedora/linux/",
|
||||||
|
"https://mirrors.mit.edu/fedora/linux/",
|
||||||
|
},
|
||||||
|
"opensuse": {
|
||||||
|
"http://download.opensuse.org/distribution/",
|
||||||
|
"http://ftp.gwdg.de/pub/linux/opensuse/distribution/",
|
||||||
|
"http://ftp.halifax.rwth-aachen.de/opensuse/distribution/",
|
||||||
|
},
|
||||||
|
"gentoo": {
|
||||||
|
"https://gentoo.osuosl.org/",
|
||||||
|
"http://mirrors.kernel.org/gentoo/",
|
||||||
|
"https://mirror.bytemark.co.uk/gentoo/",
|
||||||
|
},
|
||||||
|
"alpine": {
|
||||||
|
"http://dl-cdn.alpinelinux.org/alpine/",
|
||||||
|
"http://mirrors.dotsrc.org/alpine/",
|
||||||
|
"http://mirror.fit.cvut.cz/alpine/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if mirrors, exists := defaultMirrors[family]; exists {
|
||||||
|
return mirrors
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
463
internal/mirror/tester.go
Normal file
463
internal/mirror/tester.go
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
package mirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mirror represents a package repository mirror
|
||||||
|
type Mirror struct {
|
||||||
|
URL string
|
||||||
|
Country string
|
||||||
|
Score float64
|
||||||
|
Latency time.Duration
|
||||||
|
Speed float64 // MB/s
|
||||||
|
LastTest time.Time
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MirrorList manages a collection of mirrors
|
||||||
|
type MirrorList struct {
|
||||||
|
Mirrors []Mirror
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResult contains the results of testing a mirror
|
||||||
|
type TestResult struct {
|
||||||
|
Mirror Mirror
|
||||||
|
Success bool
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultMirrors contains known mirrors for each distribution family
|
||||||
|
var DefaultMirrors = map[string][]string{
|
||||||
|
"debian": {
|
||||||
|
"http://deb.debian.org/debian/",
|
||||||
|
"http://ftp.us.debian.org/debian/",
|
||||||
|
"http://ftp.uk.debian.org/debian/",
|
||||||
|
"http://ftp.de.debian.org/debian/",
|
||||||
|
"http://mirror.kakao.com/debian/",
|
||||||
|
"http://mirrors.kernel.org/debian/",
|
||||||
|
"http://mirror.math.princeton.edu/pub/debian/",
|
||||||
|
"http://debian.osuosl.org/debian/",
|
||||||
|
},
|
||||||
|
"arch": {
|
||||||
|
"https://mirror.rackspace.com/archlinux/",
|
||||||
|
"https://mirrors.kernel.org/archlinux/",
|
||||||
|
"https://mirror.math.princeton.edu/pub/archlinux/",
|
||||||
|
"https://mirrors.mit.edu/archlinux/",
|
||||||
|
"https://mirror.cs.vt.edu/pub/ArchLinux/",
|
||||||
|
"https://mirrors.liquidweb.com/archlinux/",
|
||||||
|
"https://arch.mirror.constant.com/",
|
||||||
|
"https://america.mirror.pkgbuild.com/",
|
||||||
|
},
|
||||||
|
"fedora": {
|
||||||
|
"https://download.fedoraproject.org/pub/fedora/linux/",
|
||||||
|
"https://mirrors.kernel.org/fedora/",
|
||||||
|
"https://mirror.math.princeton.edu/pub/fedora/linux/",
|
||||||
|
"https://mirrors.mit.edu/fedora/linux/",
|
||||||
|
"https://fedora.mirror.constant.com/",
|
||||||
|
"https://mirror.cs.vt.edu/pub/fedora/linux/",
|
||||||
|
"https://mirrors.liquidweb.com/fedora/",
|
||||||
|
"https://ftp.osuosl.org/pub/fedora/linux/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMirrorList creates a new mirror list for the given distribution family
|
||||||
|
func NewMirrorList(family string) *MirrorList {
|
||||||
|
ml := &MirrorList{
|
||||||
|
Mirrors: make([]Mirror, 0),
|
||||||
|
}
|
||||||
|
return ml
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadMirrors fetches and loads mirrors for the distribution family
|
||||||
|
func (ml *MirrorList) LoadMirrors(ctx context.Context, family string) error {
|
||||||
|
fetcher := NewMirrorFetcher()
|
||||||
|
|
||||||
|
fmt.Printf("🌐 Fetching complete mirror list for %s...\n", family)
|
||||||
|
|
||||||
|
mirrors, err := fetcher.FetchMirrors(ctx, family)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("⚠️ Failed to fetch mirrors online, using fallback list: %v\n", err)
|
||||||
|
mirrors = fetcher.getDefaultMirrors(family)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mirrors) == 0 {
|
||||||
|
return fmt.Errorf("no mirrors available for %s", family)
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.mutex.Lock()
|
||||||
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
|
ml.Mirrors = make([]Mirror, 0, len(mirrors))
|
||||||
|
for _, mirrorURL := range mirrors {
|
||||||
|
ml.Mirrors = append(ml.Mirrors, Mirror{
|
||||||
|
URL: mirrorURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("📡 Loaded %d mirrors for testing\n", len(ml.Mirrors))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMirrors tests all mirrors concurrently and updates their scores
|
||||||
|
func (ml *MirrorList) TestMirrors(ctx context.Context, timeout time.Duration) error {
|
||||||
|
if len(ml.Mirrors) == 0 {
|
||||||
|
return fmt.Errorf("no mirrors to test - this is awkward")
|
||||||
|
}
|
||||||
|
|
||||||
|
ml.mutex.Lock()
|
||||||
|
defer ml.mutex.Unlock()
|
||||||
|
|
||||||
|
results := make(chan TestResult, len(ml.Mirrors))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
fmt.Printf("🔄 Testing %d mirrors concurrently...\n", len(ml.Mirrors))
|
||||||
|
|
||||||
|
// Test each mirror concurrently
|
||||||
|
for i := range ml.Mirrors {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(mirror *Mirror) {
|
||||||
|
defer wg.Done()
|
||||||
|
testResult := ml.testSingleMirror(ctx, mirror, timeout)
|
||||||
|
results <- testResult
|
||||||
|
}(&ml.Mirrors[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all tests to complete
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(results)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Collect results and count successes
|
||||||
|
successCount := 0
|
||||||
|
failureCount := 0
|
||||||
|
|
||||||
|
for result := range results {
|
||||||
|
for i := range ml.Mirrors {
|
||||||
|
if ml.Mirrors[i].URL == result.Mirror.URL {
|
||||||
|
ml.Mirrors[i] = result.Mirror
|
||||||
|
if result.Success && result.Mirror.Error == nil {
|
||||||
|
successCount++
|
||||||
|
} else {
|
||||||
|
failureCount++
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Testing complete: %d successful, %d failed\n", successCount, failureCount)
|
||||||
|
|
||||||
|
// Sort by score (higher is better), but put failed mirrors at the end
|
||||||
|
sort.Slice(ml.Mirrors, func(i, j int) bool {
|
||||||
|
// If one has an error and the other doesn't, prioritize the working one
|
||||||
|
if (ml.Mirrors[i].Error != nil) != (ml.Mirrors[j].Error != nil) {
|
||||||
|
return ml.Mirrors[j].Error != nil // Working mirrors come first
|
||||||
|
}
|
||||||
|
// If both are working or both have errors, sort by score
|
||||||
|
return ml.Mirrors[i].Score > ml.Mirrors[j].Score
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// testSingleMirror tests a single mirror's speed and latency
|
||||||
|
func (ml *MirrorList) testSingleMirror(ctx context.Context, mirror *Mirror, timeout time.Duration) TestResult {
|
||||||
|
testCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Test latency with HEAD request first
|
||||||
|
latency, err := ml.testLatency(testCtx, mirror.URL)
|
||||||
|
if err != nil {
|
||||||
|
return TestResult{
|
||||||
|
Mirror: Mirror{
|
||||||
|
URL: mirror.URL,
|
||||||
|
Country: mirror.Country,
|
||||||
|
Error: fmt.Errorf("latency test failed: %w", err),
|
||||||
|
LastTest: time.Now(),
|
||||||
|
},
|
||||||
|
Success: false,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the mirror actually serves the required package databases
|
||||||
|
err = ml.validateMirror(testCtx, mirror.URL)
|
||||||
|
if err != nil {
|
||||||
|
return TestResult{
|
||||||
|
Mirror: Mirror{
|
||||||
|
URL: mirror.URL,
|
||||||
|
Country: mirror.Country,
|
||||||
|
Latency: latency,
|
||||||
|
Error: fmt.Errorf("validation failed: %w", err),
|
||||||
|
LastTest: time.Now(),
|
||||||
|
},
|
||||||
|
Success: false,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test download speed with a small file
|
||||||
|
speed, err := ml.testSpeed(testCtx, mirror.URL)
|
||||||
|
if err != nil {
|
||||||
|
return TestResult{
|
||||||
|
Mirror: Mirror{
|
||||||
|
URL: mirror.URL,
|
||||||
|
Country: mirror.Country,
|
||||||
|
Latency: latency,
|
||||||
|
Error: fmt.Errorf("speed test failed: %w", err),
|
||||||
|
LastTest: time.Now(),
|
||||||
|
},
|
||||||
|
Success: false,
|
||||||
|
Error: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate score based on speed and latency
|
||||||
|
score := ml.calculateScore(speed, latency)
|
||||||
|
|
||||||
|
return TestResult{
|
||||||
|
Mirror: Mirror{
|
||||||
|
URL: mirror.URL,
|
||||||
|
Country: mirror.Country,
|
||||||
|
Score: score,
|
||||||
|
Latency: latency,
|
||||||
|
Speed: speed,
|
||||||
|
LastTest: time.Now(),
|
||||||
|
Error: nil,
|
||||||
|
},
|
||||||
|
Success: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testLatency measures response time to a mirror
|
||||||
|
func (ml *MirrorList) testLatency(ctx context.Context, mirrorURL string) (time.Duration, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "HEAD", mirrorURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 2 * time.Second, // 2000ms timeout as requested
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return 0, fmt.Errorf("HTTP %d: mirror seems to be having a bad day", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Since(start), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// testSpeed measures download speed from a mirror
|
||||||
|
func (ml *MirrorList) testSpeed(ctx context.Context, mirrorURL string) (float64, error) {
|
||||||
|
// Try to download a standard test file (like Release file for Debian)
|
||||||
|
testPath := ml.getTestPath(mirrorURL)
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", testPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 15 * time.Second, // Reasonable timeout for downloading test files
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return 0, fmt.Errorf("HTTP %d: couldn't download test file", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response and measure time
|
||||||
|
bytesRead, err := io.Copy(io.Discard, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(start)
|
||||||
|
if duration == 0 {
|
||||||
|
return 0, fmt.Errorf("download completed too quickly to measure")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate speed in MB/s
|
||||||
|
speed := float64(bytesRead) / duration.Seconds() / (1024 * 1024)
|
||||||
|
return speed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateMirror checks if the mirror actually serves the required package databases
|
||||||
|
func (ml *MirrorList) validateMirror(ctx context.Context, mirrorURL string) error {
|
||||||
|
// For Arch mirrors, test multiple repositories to ensure they're properly formatted
|
||||||
|
if contains(mirrorURL, "archlinux") {
|
||||||
|
// Test all major repositories that pacman needs
|
||||||
|
repositories := []string{"core", "extra", "multilib"}
|
||||||
|
|
||||||
|
for _, repo := range repositories {
|
||||||
|
testURL := mirrorURL + repo + "/os/x86_64/" + repo + ".db"
|
||||||
|
err := ml.checkFileExists(ctx, testURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("repository %s not found: %w", repo, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Debian mirrors, test Release file
|
||||||
|
if contains(mirrorURL, "debian") {
|
||||||
|
testURL := mirrorURL + "dists/stable/Release"
|
||||||
|
return ml.checkFileExists(ctx, testURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Fedora, test repomd.xml
|
||||||
|
if contains(mirrorURL, "fedora") {
|
||||||
|
testURL := mirrorURL + "releases/39/Everything/x86_64/os/repodata/repomd.xml"
|
||||||
|
return ml.checkFileExists(ctx, testURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For unknown mirrors, just test basic connectivity
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFileExists verifies that a specific file exists on the mirror
|
||||||
|
func (ml *MirrorList) checkFileExists(ctx context.Context, fileURL string) error {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "HEAD", fileURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == 404 {
|
||||||
|
return fmt.Errorf("required package database not found (404)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return fmt.Errorf("HTTP %d: mirror doesn't serve required files", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTestPath returns an appropriate test file path for the mirror
|
||||||
|
func (ml *MirrorList) getTestPath(mirrorURL string) string {
|
||||||
|
// For Debian-based mirrors, try Release file
|
||||||
|
if contains(mirrorURL, "debian") {
|
||||||
|
return mirrorURL + "dists/stable/Release"
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Arch mirrors, try core.db
|
||||||
|
if contains(mirrorURL, "archlinux") {
|
||||||
|
return mirrorURL + "core/os/x86_64/core.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Fedora, try repomd.xml
|
||||||
|
if contains(mirrorURL, "fedora") {
|
||||||
|
return mirrorURL + "releases/39/Everything/x86_64/os/repodata/repomd.xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: just try the base URL
|
||||||
|
return mirrorURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateScore calculates a score based on speed and latency
|
||||||
|
func (ml *MirrorList) calculateScore(speed float64, latency time.Duration) float64 {
|
||||||
|
// Higher speed is better, lower latency is better
|
||||||
|
// Normalize and combine metrics
|
||||||
|
speedScore := speed * 10 // Weight speed heavily
|
||||||
|
latencyScore := 1000.0 / float64(latency.Milliseconds()) // Lower latency = higher score
|
||||||
|
|
||||||
|
return speedScore + latencyScore
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBest returns the best performing mirror
|
||||||
|
func (ml *MirrorList) GetBest() *Mirror {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
|
if len(ml.Mirrors) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the first mirror (should be highest scoring after sorting)
|
||||||
|
for _, mirror := range ml.Mirrors {
|
||||||
|
if mirror.Error == nil {
|
||||||
|
return &mirror
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTop returns the top N mirrors (only successful ones)
|
||||||
|
func (ml *MirrorList) GetTop(n int) []Mirror {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
|
result := make([]Mirror, 0, n)
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
for _, mirror := range ml.Mirrors {
|
||||||
|
if mirror.Error == nil && count < n {
|
||||||
|
result = append(result, mirror)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll returns all mirrors with their test results
|
||||||
|
func (ml *MirrorList) GetAll() []Mirror {
|
||||||
|
ml.mutex.RLock()
|
||||||
|
defer ml.mutex.RUnlock()
|
||||||
|
|
||||||
|
result := make([]Mirror, len(ml.Mirrors))
|
||||||
|
copy(result, ml.Mirrors)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// contains is a helper function to check if a string contains a substring
|
||||||
|
func contains(s, substr string) bool {
|
||||||
|
return len(s) >= len(substr) &&
|
||||||
|
(s == substr ||
|
||||||
|
s[:len(substr)] == substr ||
|
||||||
|
s[len(s)-len(substr):] == substr ||
|
||||||
|
findInString(s, substr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func findInString(s, substr string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
Reference in New Issue
Block a user