Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 97 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,102 @@ 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(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
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(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
}
}
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
}
}