From 305ec3c2fb5e39e60d26e98a572eea0b79f57e44 Mon Sep 17 00:00:00 2001 From: Raymond LaRose Date: Thu, 24 Apr 2025 15:41:46 -0400 Subject: [PATCH] Add Get-GiteaChildItem, Config management, LFS detection. Increase usability with pipes. --- PS-GiteaUtilities/PS-GiteaUtilities.psm1 | 446 +++++++++++++++++++++-- 1 file changed, 417 insertions(+), 29 deletions(-) diff --git a/PS-GiteaUtilities/PS-GiteaUtilities.psm1 b/PS-GiteaUtilities/PS-GiteaUtilities.psm1 index 57a5b6a..c82182e 100644 --- a/PS-GiteaUtilities/PS-GiteaUtilities.psm1 +++ b/PS-GiteaUtilities/PS-GiteaUtilities.psm1 @@ -1,3 +1,49 @@ +Function Set-GiteaConfiguration { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$giteaURL, + [Parameter(Mandatory)] + [string]$token, + [string]$defaultOwner, + [string]$defaultRepo, + [string]$defaultBranch = "main" + ) + + # Create configuration directory if it doesn't exist + $configDir = Join-Path -Path $env:USERPROFILE -ChildPath ".giteautils" + if (-not (Test-Path -Path $configDir)) { + New-Item -Path $configDir -ItemType Directory | Out-Null + } + + # Save configuration + $config = @{ + giteaURL = $giteaURL + token = $token + defaultOwner = $defaultOwner + defaultRepo = $defaultRepo + defaultBranch = $defaultBranch + } + + $configPath = Join-Path -Path $configDir -ChildPath "config.xml" + $config | Export-Clixml -Path $configPath -Force +} + +Function Get-GiteaConfiguration { + [CmdletBinding()] + param() + + $configPath = Join-Path -Path $env:USERPROFILE -ChildPath ".giteautils\config.xml" + + if (Test-Path -Path $configPath) { + return Import-Clixml -Path $configPath + } + else { + Write-Warning "Gitea configuration not found. Use Set-GiteaConfiguration to set up." + return $null + } +} + Function Get-GiteaFileContent { <# @@ -42,21 +88,51 @@ Function Get-GiteaFileContent { [CmdletBinding()] param( + [Parameter(ValueFromPipelineByPropertyName)] [string]$giteaURL = "https://gitea.norwichct.tech", - [Parameter(Mandatory)] + [Parameter(ValueFromPipelineByPropertyName)] [string]$repoOwner, - [Parameter(Mandatory)] + [Parameter(ValueFromPipelineByPropertyName)] [string]$repoName, + [Parameter(ValueFromPipelineByPropertyName)] [string]$branch = "main", - [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName', 'Path')] [string[]]$filePath, - [Parameter(Mandatory)] [string]$token, [switch]$decode ) begin { + # Use configuration if parameters aren't provided + if (-not $PSBoundParameters.ContainsKey('giteaURL') -or + -not $PSBoundParameters.ContainsKey('repoOwner') -or + -not $PSBoundParameters.ContainsKey('repoName') -or + -not $PSBoundParameters.ContainsKey('branch') -or + -not $PSBoundParameters.ContainsKey('token')) { + + $config = Get-GiteaConfiguration + if ($config) { + if (-not $PSBoundParameters.ContainsKey('giteaURL')) { $giteaURL = $config.giteaURL } + if (-not $PSBoundParameters.ContainsKey('repoOwner')) { $repoOwner = $config.defaultOwner } + if (-not $PSBoundParameters.ContainsKey('repoName')) { $repoName = $config.defaultRepo } + if (-not $PSBoundParameters.ContainsKey('branch')) { $branch = $config.defaultBranch } + if (-not $PSBoundParameters.ContainsKey('token')) { $token = $config.token } + } + } + + # Validate that we have all required parameters + $missingParams = @() + if (-not $giteaURL) { $missingParams += "giteaURL" } + if (-not $repoOwner) { $missingParams += "repoOwner" } + if (-not $repoName) { $missingParams += "repoName" } + if (-not $token) { $missingParams += "token" } + + if ($missingParams.Count -gt 0) { + throw "Missing required parameters: $($missingParams -join ', '). Either provide them directly or set them with Set-GiteaConfiguration." + } + + $results = @() Write-Verbose "Parameters:" Write-Verbose "giteaURL: $giteaURL" @@ -123,6 +199,82 @@ Function Get-GiteaFileContent { } } +Function Get-GiteaLFSConfiguration { + <# + .SYNOPSIS + Retrieves the LFS configuration for a Gitea repository. + + .DESCRIPTION + This function retrieves the LFS configuration for a Gitea repository using the Gitea API. The function requires the URL of the Gitea server, the owner of the repository, the name of the repository, and a personal access token. + The function returns the LFS configuration details. + #> + [cmdletbinding()] + param( + [string]$giteaURL, + [string]$repoOwner, + [string]$repoName, + [string]$token + ) + + begin { + # Use configuration if parameters aren't provided + if (-not $PSBoundParameters.ContainsKey('giteaURL') -or + -not $PSBoundParameters.ContainsKey('repoOwner') -or + -not $PSBoundParameters.ContainsKey('repoName') -or + -not $PSBoundParameters.ContainsKey('token')) { + + $config = Get-GiteaConfiguration + if ($config) { + if (-not $PSBoundParameters.ContainsKey('giteaURL')) { $giteaURL = $config.giteaURL } + if (-not $PSBoundParameters.ContainsKey('repoOwner')) { $repoOwner = $config.defaultOwner } + if (-not $PSBoundParameters.ContainsKey('repoName')) { $repoName = $config.defaultRepo } + if (-not $PSBoundParameters.ContainsKey('token')) { $token = $config.token } + } + } + + # Validate that we have all required parameters + $missingParams = @() + if (-not $giteaURL) { $missingParams += "giteaURL" } + if (-not $repoOwner) { $missingParams += "repoOwner" } + if (-not $repoName) { $missingParams += "repoName" } + if (-not $token) { $missingParams += "token" } + + if ($missingParams.Count -gt 0) { + throw "Missing required parameters: $($missingParams -join ', '). Either provide them directly or set them with Set-GiteaConfiguration." + } + } + + process { + Write-Verbose "Parameters:" + Write-Verbose "giteaURL: $giteaURL" + Write-Verbose "repoOwner: $repoOwner" + Write-Verbose "repoName: $repoName" + Write-Verbose "token: $token" + + $filePath = ".gitattributes" + + try { + $LFSConfig = (Get-GiteaFileContent -filePath $filePath -giteaURL $giteaURL -repoOwner $repoOwner -repoName $repoName -branch "main" -token $token -decode -ErrorAction SilentlyContinue).content + $LFSConfig = $LFSConfig -replace "\r?\n", "`n" # Normalize line endings + # Get the extensions of files which are tracked by LFS + # Parse the LFS configuration file and extract extensions + $lfsExtensions = @() + foreach ($line in $LFSConfig.Split("`n")) { + if ($line -match '^\*\.([^\s]+)\s+filter=lfs') { + $extension = "." + $matches[1] + $lfsExtensions += $extension + } + } + $lfsExtensions = $lfsExtensions | Sort-Object -Unique + return $lfsExtensions + } + catch { + Write-Error "Failed to retrieve LFS configuration: $_" + return @() + } + } +} + Function Invoke-GiteaFileDownload { <# .SYNOPSIS @@ -131,6 +283,9 @@ Function Invoke-GiteaFileDownload { .DESCRIPTION This function downloads a file from a Gitea repository using a direct download URL. The function requires the download URL and a personal access token. You can optionally specify an output path and use the force switch to overwrite existing files. + .PARAMETER giteaURL + The URL of the Gitea server. + .PARAMETER downloadURL The direct download URL for the file from the Gitea server. @@ -150,46 +305,279 @@ Function Invoke-GiteaFileDownload { .EXAMPLE # Example 2: Download a file to a specific location with force overwrite Invoke-GiteaFileDownload -downloadURL "https://gitea.example.com/api/v1/repos/owner/repo/raw/path/to/file.txt" -token "your_token" -outputPath "C:\Downloads\file.txt" -force + + .EXAMPLE + # Example 3: Download files in pipeline from Get-GiteaChildItem + Get-GiteaChildItem -path "docs" | Where-Object { $_.type -eq 'file' } | Invoke-GiteaFileDownload #> [cmdletbinding()] param( - [Parameter(Mandatory)] + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [string]$downloadURL, - [Parameter(Mandatory)] [string]$token, + [Parameter(ValueFromPipelineByPropertyName)] [string]$outputPath, [switch]$force ) - # If the output path is not specified, use the current directory and the file name from the download URL (everything after the last '/' and before the last character after the last '.', inclusive of the extension) - if (-not $outputPath) { - # Get the file name from the download URL - $outputFileName = $downloadURL.Substring($downloadURL.LastIndexOf("/") + 1) - # Clean the file name by removing any query string parameters and HTML encoded characters - $outputFileName = [System.Uri]::UnescapeDataString($outputFileName.Split("?")[0]) + begin { + # Use configuration if parameters aren't provided + if (-not $PSBoundParameters.ContainsKey('token')) { + + $config = Get-GiteaConfiguration + if ($config) { + if (-not $PSBoundParameters.ContainsKey('token')) { $token = $config.token } + } + } + + # Validate that we have the required token + if (-not $token) { + throw "Missing token parameter. Either provide it directly or set it with Set-GiteaConfiguration." + } + + Write-Verbose "Parameters:" + Write-Verbose "token: $token (length: $(if($token){$token.Length}else{0}))" - # Append the outputFileName to the current location - $outputPath = Join-Path -Path (Get-Location) -ChildPath $outputFileName + # Create a WebClient to be reused + $webClient = New-Object System.Net.WebClient + $webClient.Headers.Add("Authorization", "token $token") } - if((Test-Path -Path $outputPath -PathType Leaf) -and (-not $force)) { - Write-Error "The file '$outputPath' already exists. Use the -Force switch to overwrite the file." - return $false | Out-Null + process { + Write-Verbose "Processing download URL: $downloadURL" + + # If the output path is not specified, use the current directory and the file name from the download URL + $currentOutputPath = $outputPath + if (-not $currentOutputPath) { + # Get the file name from the download URL + $outputFileName = $downloadURL.Substring($downloadURL.LastIndexOf("/") + 1) + # Clean the file name by removing any query string parameters and HTML encoded characters + $outputFileName = [System.Uri]::UnescapeDataString($outputFileName.Split("?")[0]) + + # Append the outputFileName to the current location + $currentOutputPath = Join-Path -Path (Get-Location) -ChildPath $outputFileName + } + + Write-Verbose "Output path: $currentOutputPath" + + if((Test-Path -Path $currentOutputPath -PathType Leaf) -and (-not $force)) { + Write-Error "The file '$currentOutputPath' already exists. Use the -Force switch to overwrite the file." + return $false + } + + try { + # Create the directory structure if it doesn't exist + $directory = Split-Path -Path $currentOutputPath -Parent + if (-not (Test-Path -Path $directory -PathType Container) -and $directory) { + New-Item -Path $directory -ItemType Directory -Force | Out-Null + } + + # Download the file + Write-Verbose "Downloading from $downloadURL to $currentOutputPath" + + # Use synchronous download + $webClient.DownloadFile($downloadURL, $currentOutputPath) + + Write-Verbose "File downloaded successfully to $currentOutputPath" + return $true + } + catch { + Write-Error "Failed to download file from Gitea: $_" + return $false + } } - $headers = @{ - "Authorization" = "token $token" - } - - try { - Invoke-RestMethod -Uri $downloadURL -Method Get -Headers $headers -OutFile $outputPath - return $true | Out-Null - } - catch { - Write-Error "Failed to download file from Gitea: $_" - return $false | Out-Null + end { + # Clean up resources + if ($webClient) { + $webClient.Dispose() + } } } -Export-ModuleMember -Function Get-GiteaFileContent, Invoke-GiteaFileDownload \ No newline at end of file +Function Get-GiteaChildItem { + <# + .SYNOPSIS + Lists files and directories in a Gitea repository path. + + .DESCRIPTION + This function retrieves a list of files and directories from a specified path in a Gitea repository. + The results can be directly piped to Get-GiteaFileContent to retrieve file contents. + + .PARAMETER giteaURL + The URL of the Gitea server. + + .PARAMETER repoOwner + The owner of the repository. + + .PARAMETER repoName + The name of the repository. + + .PARAMETER Path + The path to list items from. This parameter accepts pipeline input. + + .PARAMETER branch + The branch to retrieve the items from. The default value is 'main'. + + .PARAMETER token + A personal access token for the Gitea server. + + .EXAMPLE + # List items in the root directory of a repository + Get-GiteaChildItem -repoOwner "owner" -repoName "repo" -Path "" -token "your_token" + + .EXAMPLE + # List items and pipe files to Get-GiteaFileContent to get their content + Get-GiteaChildItem -repoOwner "owner" -repoName "repo" -Path "docs" -token "your_token" | + Where-Object { $_.type -eq "file" } | + Get-GiteaFileContent -token "your_token" -decode + #> + + [CmdletBinding()] + param( + [string]$giteaURL, + [Parameter(ValueFromPipelineByPropertyName)] + [string]$repoOwner, + [Parameter(ValueFromPipelineByPropertyName)] + [string]$repoName, + [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] + [Alias('FullName')] + [string[]]$Path, + [Parameter(ValueFromPipelineByPropertyName)] + [string]$branch = "main", + [string]$token + ) + + begin { + # Initialize results array + # This is used to store the results of the API calls later + # It is initialized here to avoid re-initializing it in the process block + $results = @() + + # Use configuration if parameters aren't provided + if (-not $PSBoundParameters.ContainsKey('giteaURL') -or + -not $PSBoundParameters.ContainsKey('repoOwner') -or + -not $PSBoundParameters.ContainsKey('repoName') -or + -not $PSBoundParameters.ContainsKey('branch') -or + -not $PSBoundParameters.ContainsKey('token')) { + + $config = Get-GiteaConfiguration + if ($config) { + if (-not $PSBoundParameters.ContainsKey('giteaURL')) { $giteaURL = $config.giteaURL } + if (-not $PSBoundParameters.ContainsKey('repoOwner')) { $repoOwner = $config.defaultOwner } + if (-not $PSBoundParameters.ContainsKey('repoName')) { $repoName = $config.defaultRepo } + if (-not $PSBoundParameters.ContainsKey('branch')) { $branch = $config.defaultBranch } + if (-not $PSBoundParameters.ContainsKey('token')) { $token = $config.token } + } + } + + # Validate that we have all required parameters + $missingParams = @() + if (-not $giteaURL) { $missingParams += "giteaURL" } + if (-not $repoOwner) { $missingParams += "repoOwner" } + if (-not $repoName) { $missingParams += "repoName" } + if (-not $token) { $missingParams += "token" } + + if ($missingParams.Count -gt 0) { + throw "Missing required parameters: $($missingParams -join ', '). Either provide them directly or set them with Set-GiteaConfiguration." + } + + Write-Verbose "Parameters:" + Write-Verbose "giteaURL: $giteaURL" + Write-Verbose "repoOwner: $repoOwner" + Write-Verbose "repoName: $repoName" + Write-Verbose "Path: $Path" + Write-Verbose "token: $token" + + $headers = @{ + "Authorization" = "token $token" + "Accept" = "application/json" + } + + # Load the GIT LFS configuration if needed + $lfsExtensions = Get-GiteaLFSConfiguration -giteaURL $giteaURL -repoOwner $repoOwner -repoName $repoName -token $token + } + + process { + $paths = $path + foreach ($path in $paths) { + Write-Verbose "Processing path: $path" + $encodedPath = [System.Uri]::EscapeDataString($path) + Write-Verbose "Encoded path: $encodedPath" + $url = "$giteaURL" + $url += "/api/v1/repos" + $url += "/$repoOwner" + $url += "/$repoName" + $url += "/contents" + $url += "/$encodedPath" + $url += "?ref=$branch" + Write-Verbose "URL: $url" + + try { + $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers + + # Handle both single items and arrays + $items = if ($response -is [array]) { $response } else { @($response) } + + foreach ($item in $items) { + # Check if the name ends with any of the LFS extensions + $isLFS = $false + foreach ($ext in $lfsExtensions) { + if ($item.name.EndsWith($ext, [System.StringComparison]::InvariantCultureIgnoreCase)) { + $isLFS = $true + break + } + } + if ($isLFS) { + $item.type = "lfs" + $item.download_url = "$giteaURL/api/v1/repos/$repoOwner/$repoName/media/$($item.path)" + } + + $results += [PSCustomObject]@{ + # Properties for direct pipeline binding with other functions in this module + filePath = $item.path # Maps to -filePath parameter + Path = $item.path # Also include original name (alias) + repoOwner = $repoOwner # Maps to -repoOwner parameter + repoName = $repoName # Maps to -repoName parameter + giteaURL = $giteaURL # Maps to -giteaURL parameter + downloadURL = $item.download_url # Maps to -downloadURL parameter + branch = $branch # Maps to -branch parameter + + # Additional useful properties + type = $item.type # 'file' or 'dir' + name = $item.name + size = $item.size + sha = $item.sha + Success = $true + Error = $null + } + } + } + catch { + Write-Error "Failed to retrieve '$path' from Gitea: $_" + $results += [PSCustomObject]@{ + filePath = $path + Path = $path + repoOwner = $repoOwner + repoName = $repoName + giteaURL = $giteaURL + branch = $branch + type = $null + name = $null + size = $null + sha = $null + downloadURL = $null + Success = $false + Error = $_.Exception.Message + } + } + } + } + + end { + return $results + } +} + +Export-ModuleMember -Function Set-GiteaConfiguration, Get-GiteaConfiguration, Get-GiteaFileContent, Invoke-GiteaFileDownload, Get-GiteaChildItem \ No newline at end of file