diff --git a/mirava.go b/mirava.go index d97f886..5fcfa32 100644 --- a/mirava.go +++ b/mirava.go @@ -11,5 +11,6 @@ func CreateMiravaService() *pkg.MiravaService { Pacman: pkg.NewPacmanMirrorService(), Go: pkg.NewGoMirrorService(), Composer: pkg.NewComposerMirrorService(), + Nuget: pkg.NewNuGetMirrorService(), } } diff --git a/pkg/nuget.go b/pkg/nuget.go new file mode 100644 index 0000000..7d0c3d0 --- /dev/null +++ b/pkg/nuget.go @@ -0,0 +1,464 @@ +package pkg + +import ( + "context" + "fmt" + "io" + "net/http" + "regexp" + "sort" + "strings" + "time" +) + +type NuGetMirrorService struct { + HttpClient *http.Client +} + +type NuGetCheckSpeedParams struct { + Package string // Package to test speed with (e.g., "microsoft.aspnetcore.app.runtime.win-x64") + Version string // Specific version, empty for latest +} + +type NuGetCheckSpeedData struct { + DownloadMb float64 + DurationSec float64 + TimeoutSec int + SpeedMBps float64 + SpeedRating string + BytesDownloaded int64 + ContentLength int64 + Package string + Version string + MirrorURL string +} + +type NuGetCheckPackageData struct { + Package string + Version string + Description string + Versions []string + LatestVersion string +} + +type NuGetCheckStatusData struct { + Status bool + Repository string + StatusCode int +} + +func (m *NuGetMirrorService) CheckSpeed( + mirrorURL string, + timeout int, + verbose bool, + params NuGetCheckSpeedParams, +) (float64, *NuGetCheckSpeedData, error) { + + baseURL := strings.TrimSuffix(mirrorURL, "/") + + // Default test package if not specified + packageName := params.Package + if packageName == "" { + packageName = "microsoft.aspnetcore.app.runtime.win-x64" + } + + // Determine the version to download + var packageVersion string + var downloadURL string + + if params.Version != "" { + packageVersion = params.Version + // Construct direct download URL for specific version + downloadURL = fmt.Sprintf("%s/repository/nuget/%s/%s", baseURL, packageName, packageVersion) + } else { + // Fetch the directory listing to find the latest version + browseURL := fmt.Sprintf("%s/service/rest/repository/browse/nuget/%s", baseURL, packageName) + + if verbose { + fmt.Printf("Fetching version list from: %s\n", browseURL) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", browseURL, nil) + if err != nil { + return 0, nil, &HttpRequestError{ + URL: browseURL, + Err: err, + } + } + + req.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate") + req.Header.Set("User-Agent", USER_AGENT) + + resp, err := m.HttpClient.Do(req) + if err != nil { + return 0, nil, &HttpRequestError{ + URL: browseURL, + Err: err, + } + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return 0, nil, &HttpRequestError{ + URL: browseURL, + Err: fmt.Errorf("HTTP %d for version list", resp.StatusCode), + } + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, nil, fmt.Errorf("failed to read version list: %w", err) + } + + // Parse HTML to find version directories + // Looking for patterns like: 8.0.23 + versionRegex := regexp.MustCompile(``) + matches := versionRegex.FindAllStringSubmatch(string(body), -1) + + if len(matches) == 0 { + return 0, nil, fmt.Errorf("no versions found for package %s", packageName) + } + + // Collect all versions + var versions []string + for _, match := range matches { + if len(match) > 1 { + versions = append(versions, match[1]) + } + } + + if len(versions) == 0 { + return 0, nil, fmt.Errorf("no valid versions found for package %s", packageName) + } + + // Sort versions (as strings - works for semantic versioning) + sort.Slice(versions, func(i, j int) bool { + return versions[i] > versions[j] + }) + + packageVersion = versions[0] // Latest version + downloadURL = fmt.Sprintf("%s/repository/nuget/%s/%s", baseURL, packageName, packageVersion) + + if verbose { + fmt.Printf("Latest version found: %s\n", packageVersion) + fmt.Printf("Total versions available: %d\n", len(versions)) + } + } + + if verbose { + fmt.Printf("Testing NuGet mirror speed with: %s (timeout: %d seconds)\n", downloadURL, timeout) + fmt.Printf("Downloading package nupkg...\n") + } + + ctx, cancel := context.WithTimeout( + context.Background(), + time.Duration(timeout)*time.Second, + ) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) + if err != nil { + return 0, nil, &HttpRequestError{ + URL: downloadURL, + Err: err, + } + } + + req.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate") + req.Header.Set("User-Agent", USER_AGENT) + + startZip := time.Now() + + resp, err := m.HttpClient.Do(req) + if err != nil { + if ctx.Err() == context.DeadlineExceeded { + return 0, nil, &HttpRequestError{ + URL: downloadURL, + Err: fmt.Errorf("timeout reached before connection established"), + } + } + + return 0, nil, &HttpRequestError{ + URL: downloadURL, + Err: err, + } + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return 0, nil, &HttpRequestError{ + URL: downloadURL, + Err: fmt.Errorf("HTTP %d for package download", resp.StatusCode), + } + } + + contentLength := resp.ContentLength + var downloaded int64 + buf := make([]byte, 512*1024) + lastProgress := time.Now() + + if verbose { + if contentLength > 0 { + fmt.Printf("Package size: %.2f MB\n", float64(contentLength)/1024/1024) + } + fmt.Printf("Downloading for up to %d seconds...\n", timeout) + } + + for { + select { + case <-ctx.Done(): + if verbose { + fmt.Printf("\nTimeout reached after %d seconds\n", timeout) + } + goto calculateSpeed + default: + n, err := resp.Body.Read(buf) + if n > 0 { + downloaded += int64(n) + + if verbose && time.Since(lastProgress) > 500*time.Millisecond { + elapsed := time.Since(startZip).Seconds() + speedMBps := (float64(downloaded) / 1024 / 1024) / elapsed + + if contentLength > 0 { + percent := float64(downloaded) / float64(contentLength) * 100 + fmt.Printf("\r[%ds] %.1f%% (%.2f/%.2f MB) - %.2f MB/s", + int(elapsed), percent, + float64(downloaded)/1024/1024, + float64(contentLength)/1024/1024, + speedMBps) + } else { + fmt.Printf("\r[%ds] Downloaded: %.2f MB - %.2f MB/s", + int(elapsed), + float64(downloaded)/1024/1024, + speedMBps) + } + lastProgress = time.Now() + } + } + + if err != nil { + if err == io.EOF { + if verbose { + fmt.Println("\nReached end of file") + } + goto calculateSpeed + } + if ctx.Err() == context.DeadlineExceeded { + goto calculateSpeed + } + return 0, nil, &HttpRequestError{ + URL: downloadURL, + Err: err, + } + } + } + } + +calculateSpeed: + duration := time.Since(startZip).Seconds() + + if verbose { + fmt.Printf("\nDownloaded %.2f MB in %.2f seconds\n", + float64(downloaded)/1024/1024, duration) + } + + if duration > 0 && downloaded > 0 { + speedMBps := (float64(downloaded) / 1024 / 1024) / duration + + if verbose { + fmt.Printf("Average speed: %.2f MB/s\n", speedMBps) + fmt.Printf("Rating: %s\n", getNuGetSpeedRating(speedMBps)) + } + + info := NuGetCheckSpeedData{ + DownloadMb: float64(downloaded) / 1024 / 1024, + DurationSec: duration, + TimeoutSec: timeout, + SpeedMBps: speedMBps, + SpeedRating: getNuGetSpeedRating(speedMBps), + BytesDownloaded: downloaded, + ContentLength: contentLength, + Package: packageName, + Version: packageVersion, + MirrorURL: baseURL, + } + + return speedMBps, &info, nil + } + + return 0, nil, &HttpRequestError{ + URL: downloadURL, + Err: fmt.Errorf("speed test failed (downloaded %d bytes in %.2fs)", downloaded, duration), + } +} + +func (m *NuGetMirrorService) CheckPackage( + mirrorURL string, + packageName string, + verbose bool, +) (bool, *NuGetCheckPackageData, error) { + + baseURL := strings.TrimSuffix(mirrorURL, "/") + + // Fetch the directory listing to find versions + browseURL := fmt.Sprintf("%s/service/rest/repository/browse/nuget/%s/", baseURL, packageName) + + if verbose { + fmt.Printf("Fetching package versions from: %s\n", browseURL) + } + + resp, err := m.HttpClient.Get(browseURL) + if err != nil { + return false, nil, fmt.Errorf("failed to fetch package listing: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false, nil, fmt.Errorf("package not found: HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return false, nil, fmt.Errorf("failed to read package listing: %w", err) + } + + // Parse HTML to find version directories + // Looking for patterns like: 8.0.23 + versionRegex := regexp.MustCompile(``) + matches := versionRegex.FindAllStringSubmatch(string(body), -1) + + if len(matches) == 0 { + if verbose { + fmt.Printf("No versions found for package '%s'\n", packageName) + } + return false, nil, nil + } + + // Collect all versions + var versions []string + for _, match := range matches { + if len(match) > 1 { + versions = append(versions, match[1]) + } + } + + if len(versions) == 0 { + return false, nil, nil + } + + // Sort versions (newest first) + sort.Slice(versions, func(i, j int) bool { + return versions[i] > versions[j] + }) + + latestVersion := versions[0] + + if verbose { + fmt.Printf("Found package '%s' with %d versions, latest: %s\n", + packageName, len(versions), latestVersion) + } + + info := &NuGetCheckPackageData{ + Package: packageName, + Version: latestVersion, + Versions: versions, + LatestVersion: latestVersion, + } + + return true, info, nil +} + +func (m *NuGetMirrorService) CheckStatus( + url string, + verbose bool, +) (bool, *NuGetCheckStatusData, error) { + + baseURL := strings.TrimSuffix(url, "/") + + // Test if the repository is accessible + testURL := fmt.Sprintf("%s/service/rest/repository/browse/nuget/", baseURL) + + if verbose { + fmt.Printf("Testing NuGet mirror endpoint: %s\n", testURL) + } + + req, err := http.NewRequest("GET", testURL, nil) + if err != nil { + return false, nil, &HttpRequestError{ + URL: testURL, + Err: err, + } + } + + req.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate") + req.Header.Set("User-Agent", USER_AGENT) + + resp, err := m.HttpClient.Do(req) + if err != nil { + if verbose { + fmt.Printf("Failed: %v\n", err) + } + return false, nil, &HttpRequestError{ + URL: testURL, + Err: err, + } + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if verbose { + fmt.Printf("HTTP %d from NuGet mirror\n", resp.StatusCode) + } + return false, nil, &HttpRequestError{ + URL: testURL, + Err: fmt.Errorf("HTTP %d for repository browse", resp.StatusCode), + } + } + + if verbose { + fmt.Printf("Mirror responded successfully with status %d\n", resp.StatusCode) + } + + info := NuGetCheckStatusData{ + Status: true, + Repository: testURL, + StatusCode: resp.StatusCode, + } + + return true, &info, nil +} + +func getNuGetSpeedRating(speedMBps float64) string { + switch { + case speedMBps > 50: + return "Excellent" + case speedMBps > 20: + return "Good" + case speedMBps > 10: + return "Average" + case speedMBps > 5: + return "Slow" + default: + return "Very Slow" + } +} + +// NewNuGetMirrorService creates a new NuGet mirror service instance +func NewNuGetMirrorService() *NuGetMirrorService { + return &NuGetMirrorService{ + HttpClient: &http.Client{ + Timeout: 60 * time.Second, + Transport: &http.Transport{ + DisableCompression: false, + DisableKeepAlives: false, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + }, + }, + } +} diff --git a/pkg/type.go b/pkg/type.go index f3d5bb5..3d7a691 100644 --- a/pkg/type.go +++ b/pkg/type.go @@ -26,4 +26,5 @@ type MiravaService struct { Pacman *PacmanMirrorService Go *GoMirrorService Composer *ComposerMirrorService + Nuget *NuGetMirrorService }