diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs index 39f129006..3626f71ce 100644 --- a/src/code/RepositorySettings.cs +++ b/src/code/RepositorySettings.cs @@ -72,7 +72,7 @@ public static void CheckRepositoryStore() } catch (Exception e) { - throw new PSInvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Repository store may be corrupted, file reading failed with error: {0}.", e.Message)); + throw new PSInvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Repository store may be corrupted, file reading failed with error: {0}. Try running 'Reset-PSResourceRepository' to reset the repository store.", e.Message)); } } @@ -845,6 +845,102 @@ public static List Read(string[] repoNames, out string[] error return reposToReturn.ToList(); } + /// + /// Reset the repository store by creating a new PSRepositories.xml file with PSGallery registered. + /// This creates a temporary new file first, and only replaces the old file if creation succeeds. + /// If creation fails, the old file is restored. + /// Returns: PSRepositoryInfo for the PSGallery repository + /// + public static PSRepositoryInfo Reset(PSCmdlet cmdletPassedIn, out string errorMsg) + { + errorMsg = string.Empty; + string tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".xml"); + string backupFilePath = string.Empty; + + try + { + // Ensure the repository directory exists + if (!Directory.Exists(RepositoryPath)) + { + Directory.CreateDirectory(RepositoryPath); + } + + // Create new repository XML in a temporary location + XDocument newRepoXML = new XDocument( + new XElement("configuration")); + newRepoXML.Save(tempFilePath); + + // Validate that the temporary file can be loaded + LoadXDocument(tempFilePath); + + // Back up the existing file if it exists + if (File.Exists(FullRepositoryPath)) + { + backupFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + "_backup.xml"); + File.Copy(FullRepositoryPath, backupFilePath, overwrite: true); + + // Delete the old file + File.Delete(FullRepositoryPath); + } + + // Move the temporary file to the actual location + File.Copy(tempFilePath, FullRepositoryPath, overwrite: true); + + // Add PSGallery to the newly created store + Uri psGalleryUri = new Uri(PSGalleryRepoUri); + PSRepositoryInfo psGalleryRepo = Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.V2, force: false); + + // Clean up temporary and backup files on success + if (File.Exists(tempFilePath)) + { + File.Delete(tempFilePath); + } + if (!string.IsNullOrEmpty(backupFilePath) && File.Exists(backupFilePath)) + { + File.Delete(backupFilePath); + } + + return psGalleryRepo; + } + catch (Exception e) + { + // Restore the backup file if it exists + if (!string.IsNullOrEmpty(backupFilePath) && File.Exists(backupFilePath)) + { + try + { + if (File.Exists(FullRepositoryPath)) + { + File.Delete(FullRepositoryPath); + } + File.Copy(backupFilePath, FullRepositoryPath, overwrite: true); + File.Delete(backupFilePath); + } + catch (Exception restoreEx) + { + errorMsg = string.Format(CultureInfo.InvariantCulture, "Repository store reset failed with error: {0}. An attempt to restore the old repository store also failed with error: {1}", e.Message, restoreEx.Message); + return null; + } + } + + // Clean up temporary file + if (File.Exists(tempFilePath)) + { + try + { + File.Delete(tempFilePath); + } + catch + { + // Ignore cleanup errors + } + } + + errorMsg = string.Format(CultureInfo.InvariantCulture, "Repository store reset failed with error: {0}.", e.Message); + return null; + } + } + #endregion #region Private methods diff --git a/src/code/ResetPSResourceRepository.cs b/src/code/ResetPSResourceRepository.cs new file mode 100644 index 000000000..59d40c407 --- /dev/null +++ b/src/code/ResetPSResourceRepository.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PSResourceGet.UtilClasses; +using System; +using System.Management.Automation; + +namespace Microsoft.PowerShell.PSResourceGet.Cmdlets +{ + /// + /// The Reset-PSResourceRepository cmdlet resets the repository store by creating a new PSRepositories.xml file. + /// This is useful when the repository store becomes corrupted. + /// It will create a new repository store with only the PSGallery repository registered. + /// + [Cmdlet(VerbsCommon.Reset, + "PSResourceRepository", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(PSRepositoryInfo))] + public sealed class ResetPSResourceRepository : PSCmdlet + { + #region Parameters + + /// + /// When specified, displays the PSGallery repository that was registered after reset + /// + [Parameter] + public SwitchParameter PassThru { get; set; } + + #endregion + + #region Methods + + protected override void ProcessRecord() + { + string repositoryStorePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "PSResourceGet", + "PSResourceRepository.xml"); + + WriteVerbose($"Resetting repository store at: {repositoryStorePath}"); + + if (!ShouldProcess(repositoryStorePath, "Reset repository store and create new PSRepositories.xml file with PSGallery registered")) + { + return; + } + + PSRepositoryInfo psGalleryRepo = RepositorySettings.Reset(this, out string errorMsg); + + if (!string.IsNullOrEmpty(errorMsg)) + { + WriteError(new ErrorRecord( + new PSInvalidOperationException(errorMsg), + "ErrorResettingRepositoryStore", + ErrorCategory.InvalidOperation, + this)); + return; + } + + WriteVerbose("Repository store reset successfully. PSGallery has been registered."); + + if (PassThru && psGalleryRepo != null) + { + WriteObject(psGalleryRepo); + } + } + + #endregion + } +} diff --git a/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 b/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 new file mode 100644 index 000000000..f49570229 --- /dev/null +++ b/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 @@ -0,0 +1,149 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$modPath = "$psscriptroot/../PSGetTestUtils.psm1" +Write-Verbose -Verbose -Message "PSGetTestUtils path: $modPath" +Import-Module $modPath -Force -Verbose + +Describe "Test Reset-PSResourceRepository" -tags 'CI' { + BeforeEach { + $PSGalleryName = Get-PSGalleryName + $PSGalleryUri = Get-PSGalleryLocation + Get-NewPSResourceRepositoryFile + } + + AfterEach { + Get-RevertPSResourceRepositoryFile + } + + It "Reset repository store without PassThru parameter" { + # Arrange: Add a test repository + $TestRepoName = "testRepository" + $tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1" + New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null + Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath + + # Verify repository was added + $repos = Get-PSResourceRepository + $repos.Count | Should -BeGreaterThan 1 + + # Act: Reset repository store + Reset-PSResourceRepository -Confirm:$false + + # Assert: Only PSGallery should exist + $repos = Get-PSResourceRepository + $repos.Count | Should -Be 1 + $repos.Name | Should -Be $PSGalleryName + $repos.Uri | Should -Be $PSGalleryUri + } + + It "Reset repository store with PassThru parameter returns PSGallery" { + # Arrange: Add a test repository + $TestRepoName = "testRepository" + $tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1" + New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null + Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath + + # Act: Reset repository store with PassThru + $result = Reset-PSResourceRepository -Confirm:$false -PassThru + + # Assert: Result should be PSGallery repository info + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $PSGalleryName + $result.Uri | Should -Be $PSGalleryUri + $result.Trusted | Should -Be $false + $result.Priority | Should -Be 50 + + # Verify only PSGallery exists + $repos = Get-PSResourceRepository + $repos.Count | Should -Be 1 + } + + It "Reset repository store should support -WhatIf" { + # Arrange: Add a test repository + $TestRepoName = "testRepository" + $tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1" + New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null + Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath + + # Capture repository count before WhatIf + $reposBefore = Get-PSResourceRepository + $countBefore = $reposBefore.Count + + # Act: Run with WhatIf + Reset-PSResourceRepository -WhatIf + + # Assert: Repositories should not have changed + $reposAfter = Get-PSResourceRepository + $reposAfter.Count | Should -Be $countBefore + } + + It "Reset repository store when corrupted should succeed" { + # Arrange: Corrupt the repository file + $powerShellGetPath = Join-Path -Path ([Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)) -ChildPath "PSResourceGet" + $repoFilePath = Join-Path -Path $powerShellGetPath -ChildPath "PSResourceRepository.xml" + + # Write invalid XML to corrupt the file + "This is not valid XML" | Set-Content -Path $repoFilePath -Force + + # Act: Reset the repository store + $result = Reset-PSResourceRepository -Confirm:$false -PassThru + + # Assert: Should successfully reset and return PSGallery + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $PSGalleryName + + # Verify we can now read repositories + $repos = Get-PSResourceRepository + $repos.Count | Should -Be 1 + $repos.Name | Should -Be $PSGalleryName + } + + It "Reset repository store when file doesn't exist should succeed" { + # Arrange: Delete the repository file + $powerShellGetPath = Join-Path -Path ([Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)) -ChildPath "PSResourceGet" + $repoFilePath = Join-Path -Path $powerShellGetPath -ChildPath "PSResourceRepository.xml" + + if (Test-Path -Path $repoFilePath) { + Remove-Item -Path $repoFilePath -Force + } + + # Act: Reset the repository store + $result = Reset-PSResourceRepository -Confirm:$false -PassThru + + # Assert: Should successfully reset and return PSGallery + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $PSGalleryName + + # Verify PSGallery is registered + $repos = Get-PSResourceRepository + $repos.Count | Should -Be 1 + $repos.Name | Should -Be $PSGalleryName + } + + It "Reset repository store with multiple repositories should only keep PSGallery" { + # Arrange: Register multiple repositories + $tmpDir1Path = Join-Path -Path $TestDrive -ChildPath "tmpDir1" + $tmpDir2Path = Join-Path -Path $TestDrive -ChildPath "tmpDir2" + $tmpDir3Path = Join-Path -Path $TestDrive -ChildPath "tmpDir3" + New-Item -ItemType Directory -Path $tmpDir1Path -Force | Out-Null + New-Item -ItemType Directory -Path $tmpDir2Path -Force | Out-Null + New-Item -ItemType Directory -Path $tmpDir3Path -Force | Out-Null + + Register-PSResourceRepository -Name "testRepo1" -Uri $tmpDir1Path + Register-PSResourceRepository -Name "testRepo2" -Uri $tmpDir2Path + Register-PSResourceRepository -Name "testRepo3" -Uri $tmpDir3Path + + # Verify multiple repositories exist + $reposBefore = Get-PSResourceRepository + $reposBefore.Count | Should -BeGreaterThan 1 + + # Act: Reset repository store + Reset-PSResourceRepository -Confirm:$false + + # Assert: Only PSGallery should remain + $reposAfter = Get-PSResourceRepository + $reposAfter.Count | Should -Be 1 + $reposAfter.Name | Should -Be $PSGalleryName + } +}