Skip to content
Open
1 change: 1 addition & 0 deletions src/Microsoft.PowerShell.PSResourceGet.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'Get-PSScriptFileInfo',
'Install-PSResource',
'Register-PSResourceRepository',
'Reset-PSResourceRepository',
'Save-PSResource',
'Set-PSResourceRepository',
'New-PSScriptFileInfo',
Expand Down
112 changes: 111 additions & 1 deletion src/code/RepositorySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

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

/// <summary>
/// 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
/// </summary>
public static PSRepositoryInfo Reset(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
try
{
LoadXDocument(tempFilePath);
}
catch (Exception loadEx)
{
// Clean up temp file on validation failure
if (File.Exists(tempFilePath))
{
try
{
File.Delete(tempFilePath);
}
catch (Exception cleanupEx)
{
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);
return null;
}
}
errorMsg = string.Format(CultureInfo.InvariantCulture, "Failed to validate newly created repository store file with error: {0}.", loadEx.Message);
return null;
}

// Back up the existing file if it exists
if (File.Exists(FullRepositoryPath))
{
backupFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + "_backup.xml");
Utils.MoveFiles(FullRepositoryPath, backupFilePath, overwrite: true);
}

// Move the temporary file to the actual location
Utils.MoveFiles(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 backup file on success
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);
}
Utils.MoveFiles(backupFilePath, FullRepositoryPath, overwrite: true);
}
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 (Exception cleanupEx)
{
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);
return null;
}
}

errorMsg = string.Format(CultureInfo.InvariantCulture, "Repository store reset failed with error: {0}.", e.Message);
return null;
}
}

#endregion

#region Private methods
Expand Down
70 changes: 70 additions & 0 deletions src/code/ResetPSResourceRepository.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
[Cmdlet(VerbsCommon.Reset,
"PSResourceRepository",
SupportsShouldProcess = true,
ConfirmImpact = ConfirmImpact.High)]
[OutputType(typeof(PSRepositoryInfo))]
public sealed class ResetPSResourceRepository : PSCmdlet
{
#region Parameters

/// <summary>
/// When specified, displays the PSGallery repository that was registered after reset
/// </summary>
[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(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)
{
WriteObject(psGalleryRepo);
}
}

#endregion
}
}
149 changes: 149 additions & 0 deletions test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading