Skip to content

Commit e2bf4ee

Browse files
authored
Add Reset-PSResourceRepository cmdlet to recover from corrupted repository store (#1895)
1 parent 5da84fa commit e2bf4ee

File tree

4 files changed

+331
-1
lines changed

4 files changed

+331
-1
lines changed

src/Microsoft.PowerShell.PSResourceGet.psd1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
'Get-PSScriptFileInfo',
2424
'Install-PSResource',
2525
'Register-PSResourceRepository',
26+
'Reset-PSResourceRepository',
2627
'Save-PSResource',
2728
'Set-PSResourceRepository',
2829
'New-PSScriptFileInfo',

src/code/RepositorySettings.cs

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public static void CheckRepositoryStore()
7272
}
7373
catch (Exception e)
7474
{
75-
throw new PSInvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Repository store may be corrupted, file reading failed with error: {0}.", e.Message));
75+
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));
7676
}
7777
}
7878

@@ -845,6 +845,116 @@ public static List<PSRepositoryInfo> Read(string[] repoNames, out string[] error
845845
return reposToReturn.ToList();
846846
}
847847

848+
/// <summary>
849+
/// Reset the repository store by creating a new PSRepositories.xml file with PSGallery registered.
850+
/// This creates a temporary new file first, and only replaces the old file if creation succeeds.
851+
/// If creation fails, the old file is restored.
852+
/// Returns: PSRepositoryInfo for the PSGallery repository
853+
/// </summary>
854+
public static PSRepositoryInfo Reset(out string errorMsg)
855+
{
856+
errorMsg = string.Empty;
857+
string tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".xml");
858+
string backupFilePath = string.Empty;
859+
860+
try
861+
{
862+
// Ensure the repository directory exists
863+
if (!Directory.Exists(RepositoryPath))
864+
{
865+
Directory.CreateDirectory(RepositoryPath);
866+
}
867+
868+
// Create new repository XML in a temporary location
869+
XDocument newRepoXML = new XDocument(
870+
new XElement("configuration"));
871+
newRepoXML.Save(tempFilePath);
872+
873+
// Validate that the temporary file can be loaded
874+
try
875+
{
876+
LoadXDocument(tempFilePath);
877+
}
878+
catch (Exception loadEx)
879+
{
880+
// Clean up temp file on validation failure
881+
if (File.Exists(tempFilePath))
882+
{
883+
try
884+
{
885+
File.Delete(tempFilePath);
886+
}
887+
catch (Exception cleanupEx)
888+
{
889+
errorMsg = string.Format(CultureInfo.InvariantCulture, "Failed to validate newly created repository store file with error: {0}. Additionally, cleanup of temporary file failed with error: {1}", loadEx.Message, cleanupEx.Message);
890+
return null;
891+
}
892+
}
893+
errorMsg = string.Format(CultureInfo.InvariantCulture, "Failed to validate newly created repository store file with error: {0}.", loadEx.Message);
894+
return null;
895+
}
896+
897+
// Back up the existing file if it exists
898+
if (File.Exists(FullRepositoryPath))
899+
{
900+
backupFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + "_backup.xml");
901+
Utils.MoveFiles(FullRepositoryPath, backupFilePath, overwrite: true);
902+
}
903+
904+
// Move the temporary file to the actual location
905+
Utils.MoveFiles(tempFilePath, FullRepositoryPath, overwrite: true);
906+
907+
// Add PSGallery to the newly created store
908+
Uri psGalleryUri = new Uri(PSGalleryRepoUri);
909+
PSRepositoryInfo psGalleryRepo = Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.V2, force: false);
910+
911+
// Clean up backup file on success
912+
if (!string.IsNullOrEmpty(backupFilePath) && File.Exists(backupFilePath))
913+
{
914+
File.Delete(backupFilePath);
915+
}
916+
917+
return psGalleryRepo;
918+
}
919+
catch (Exception e)
920+
{
921+
// Restore the backup file if it exists
922+
if (!string.IsNullOrEmpty(backupFilePath) && File.Exists(backupFilePath))
923+
{
924+
try
925+
{
926+
if (File.Exists(FullRepositoryPath))
927+
{
928+
File.Delete(FullRepositoryPath);
929+
}
930+
Utils.MoveFiles(backupFilePath, FullRepositoryPath, overwrite: true);
931+
}
932+
catch (Exception restoreEx)
933+
{
934+
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);
935+
return null;
936+
}
937+
}
938+
939+
// Clean up temporary file
940+
if (File.Exists(tempFilePath))
941+
{
942+
try
943+
{
944+
File.Delete(tempFilePath);
945+
}
946+
catch (Exception cleanupEx)
947+
{
948+
errorMsg = string.Format(CultureInfo.InvariantCulture, "Repository store reset failed with error: {0}. Additionally, cleanup of temporary file failed with error: {1}", e.Message, cleanupEx.Message);
949+
return null;
950+
}
951+
}
952+
953+
errorMsg = string.Format(CultureInfo.InvariantCulture, "Repository store reset failed with error: {0}.", e.Message);
954+
return null;
955+
}
956+
}
957+
848958
#endregion
849959

850960
#region Private methods
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.PowerShell.PSResourceGet.UtilClasses;
5+
using System;
6+
using System.Management.Automation;
7+
8+
namespace Microsoft.PowerShell.PSResourceGet.Cmdlets
9+
{
10+
/// <summary>
11+
/// The Reset-PSResourceRepository cmdlet resets the repository store by creating a new PSRepositories.xml file.
12+
/// This is useful when the repository store becomes corrupted.
13+
/// It will create a new repository store with only the PSGallery repository registered.
14+
/// </summary>
15+
[Cmdlet(VerbsCommon.Reset,
16+
"PSResourceRepository",
17+
SupportsShouldProcess = true,
18+
ConfirmImpact = ConfirmImpact.High)]
19+
[OutputType(typeof(PSRepositoryInfo))]
20+
public sealed class ResetPSResourceRepository : PSCmdlet
21+
{
22+
#region Parameters
23+
24+
/// <summary>
25+
/// When specified, displays the PSGallery repository that was registered after reset
26+
/// </summary>
27+
[Parameter]
28+
public SwitchParameter PassThru { get; set; }
29+
30+
#endregion
31+
32+
#region Methods
33+
34+
protected override void ProcessRecord()
35+
{
36+
string repositoryStorePath = System.IO.Path.Combine(
37+
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
38+
"PSResourceGet",
39+
"PSResourceRepository.xml");
40+
41+
WriteVerbose($"Resetting repository store at: {repositoryStorePath}");
42+
43+
if (!ShouldProcess(repositoryStorePath, "Reset repository store and create new PSRepositories.xml file with PSGallery registered"))
44+
{
45+
return;
46+
}
47+
48+
PSRepositoryInfo psGalleryRepo = RepositorySettings.Reset(out string errorMsg);
49+
50+
if (!string.IsNullOrEmpty(errorMsg))
51+
{
52+
WriteError(new ErrorRecord(
53+
new PSInvalidOperationException(errorMsg),
54+
"ErrorResettingRepositoryStore",
55+
ErrorCategory.InvalidOperation,
56+
this));
57+
return;
58+
}
59+
60+
WriteVerbose("Repository store reset successfully. PSGallery has been registered.");
61+
62+
if (PassThru)
63+
{
64+
WriteObject(psGalleryRepo);
65+
}
66+
}
67+
68+
#endregion
69+
}
70+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
$modPath = "$psscriptroot/../PSGetTestUtils.psm1"
5+
Write-Verbose -Verbose -Message "PSGetTestUtils path: $modPath"
6+
Import-Module $modPath -Force -Verbose
7+
8+
Describe "Test Reset-PSResourceRepository" -tags 'CI' {
9+
BeforeEach {
10+
$PSGalleryName = Get-PSGalleryName
11+
$PSGalleryUri = Get-PSGalleryLocation
12+
Get-NewPSResourceRepositoryFile
13+
}
14+
15+
AfterEach {
16+
Get-RevertPSResourceRepositoryFile
17+
}
18+
19+
It "Reset repository store without PassThru parameter" {
20+
# Arrange: Add a test repository
21+
$TestRepoName = "testRepository"
22+
$tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1"
23+
New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null
24+
Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath
25+
26+
# Verify repository was added
27+
$repos = Get-PSResourceRepository
28+
$repos.Count | Should -BeGreaterThan 1
29+
30+
# Act: Reset repository store
31+
Reset-PSResourceRepository -Confirm:$false
32+
33+
# Assert: Only PSGallery should exist
34+
$repos = Get-PSResourceRepository
35+
$repos.Count | Should -Be 1
36+
$repos.Name | Should -Be $PSGalleryName
37+
$repos.Uri | Should -Be $PSGalleryUri
38+
}
39+
40+
It "Reset repository store with PassThru parameter returns PSGallery" {
41+
# Arrange: Add a test repository
42+
$TestRepoName = "testRepository"
43+
$tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1"
44+
New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null
45+
Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath
46+
47+
# Act: Reset repository store with PassThru
48+
$result = Reset-PSResourceRepository -Confirm:$false -PassThru
49+
50+
# Assert: Result should be PSGallery repository info
51+
$result | Should -Not -BeNullOrEmpty
52+
$result.Name | Should -Be $PSGalleryName
53+
$result.Uri | Should -Be $PSGalleryUri
54+
$result.Trusted | Should -Be $false
55+
$result.Priority | Should -Be 50
56+
57+
# Verify only PSGallery exists
58+
$repos = Get-PSResourceRepository
59+
$repos.Count | Should -Be 1
60+
}
61+
62+
It "Reset repository store should support -WhatIf" {
63+
# Arrange: Add a test repository
64+
$TestRepoName = "testRepository"
65+
$tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1"
66+
New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null
67+
Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath
68+
69+
# Capture repository count before WhatIf
70+
$reposBefore = Get-PSResourceRepository
71+
$countBefore = $reposBefore.Count
72+
73+
# Act: Run with WhatIf
74+
Reset-PSResourceRepository -WhatIf
75+
76+
# Assert: Repositories should not have changed
77+
$reposAfter = Get-PSResourceRepository
78+
$reposAfter.Count | Should -Be $countBefore
79+
}
80+
81+
It "Reset repository store when corrupted should succeed" {
82+
# Arrange: Corrupt the repository file
83+
$powerShellGetPath = Join-Path -Path ([Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)) -ChildPath "PSResourceGet"
84+
$repoFilePath = Join-Path -Path $powerShellGetPath -ChildPath "PSResourceRepository.xml"
85+
86+
# Write invalid XML to corrupt the file
87+
"This is not valid XML" | Set-Content -Path $repoFilePath -Force
88+
89+
# Act: Reset the repository store
90+
$result = Reset-PSResourceRepository -Confirm:$false -PassThru
91+
92+
# Assert: Should successfully reset and return PSGallery
93+
$result | Should -Not -BeNullOrEmpty
94+
$result.Name | Should -Be $PSGalleryName
95+
96+
# Verify we can now read repositories
97+
$repos = Get-PSResourceRepository
98+
$repos.Count | Should -Be 1
99+
$repos.Name | Should -Be $PSGalleryName
100+
}
101+
102+
It "Reset repository store when file doesn't exist should succeed" {
103+
# Arrange: Delete the repository file
104+
$powerShellGetPath = Join-Path -Path ([Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)) -ChildPath "PSResourceGet"
105+
$repoFilePath = Join-Path -Path $powerShellGetPath -ChildPath "PSResourceRepository.xml"
106+
107+
if (Test-Path -Path $repoFilePath) {
108+
Remove-Item -Path $repoFilePath -Force
109+
}
110+
111+
# Act: Reset the repository store
112+
$result = Reset-PSResourceRepository -Confirm:$false -PassThru
113+
114+
# Assert: Should successfully reset and return PSGallery
115+
$result | Should -Not -BeNullOrEmpty
116+
$result.Name | Should -Be $PSGalleryName
117+
118+
# Verify PSGallery is registered
119+
$repos = Get-PSResourceRepository
120+
$repos.Count | Should -Be 1
121+
$repos.Name | Should -Be $PSGalleryName
122+
}
123+
124+
It "Reset repository store with multiple repositories should only keep PSGallery" {
125+
# Arrange: Register multiple repositories
126+
$tmpDir1Path = Join-Path -Path $TestDrive -ChildPath "tmpDir1"
127+
$tmpDir2Path = Join-Path -Path $TestDrive -ChildPath "tmpDir2"
128+
$tmpDir3Path = Join-Path -Path $TestDrive -ChildPath "tmpDir3"
129+
New-Item -ItemType Directory -Path $tmpDir1Path -Force | Out-Null
130+
New-Item -ItemType Directory -Path $tmpDir2Path -Force | Out-Null
131+
New-Item -ItemType Directory -Path $tmpDir3Path -Force | Out-Null
132+
133+
Register-PSResourceRepository -Name "testRepo1" -Uri $tmpDir1Path
134+
Register-PSResourceRepository -Name "testRepo2" -Uri $tmpDir2Path
135+
Register-PSResourceRepository -Name "testRepo3" -Uri $tmpDir3Path
136+
137+
# Verify multiple repositories exist
138+
$reposBefore = Get-PSResourceRepository
139+
$reposBefore.Count | Should -BeGreaterThan 1
140+
141+
# Act: Reset repository store
142+
Reset-PSResourceRepository -Confirm:$false
143+
144+
# Assert: Only PSGallery should remain
145+
$reposAfter = Get-PSResourceRepository
146+
$reposAfter.Count | Should -Be 1
147+
$reposAfter.Name | Should -Be $PSGalleryName
148+
}
149+
}

0 commit comments

Comments
 (0)