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( [switch]$LoadVariables, [switch]$Force ) $configPath = Join-Path -Path $env:USERPROFILE -ChildPath ".giteautils\config.xml" if (Test-Path -Path $configPath) { $config = Import-Clixml -Path $configPath # If LoadVariables switch is used, set each config value as a variable in the global scope if ($LoadVariables) { foreach ($key in $config.Keys) { # Check if variable exists in global scope $variableExists = $false try { $existingVar = Get-Variable -Name $key -Scope Global -ErrorAction Stop $variableExists = $true } catch { $variableExists = $false } # Set variable if it doesn't exist or if Force is used if (-not $variableExists -or $Force) { Write-Verbose "Loading configuration variable: $key = $($config[$key])" Set-Variable -Name $key -Value $config[$key] -Scope Global Write-Host "Created global variable: `$$key" -ForegroundColor Green } else { Write-Verbose "Skipping existing variable: $key (use -Force to override)" } } } return $config } else { Write-Warning "Gitea configuration not found. Use Set-GiteaConfiguration to set up." return $null } } Function Get-GiteaFileContent { <# .SYNOPSIS Retrieve the content of a file from a Gitea repository using the Gitea API. .DESCRIPTION This function retrieves the content of a file from 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, the branch to retrieve the file from, the path to the file, a personal access token, and an optional switch to decode the content of the file from Base64 encoding. .PARAMETER giteaURL The URL of the Gitea server. .PARAMETER repoOwner The owner of the repository. .PARAMETER repoName The name of the repository. .PARAMETER branch The branch to retrieve the file from. The default value is 'main'. .PARAMETER filePath The path to the file to retrieve. This parameter accepts pipeline input. .PARAMETER token A personal access token for the Gitea server. .PARAMETER decode A switch parameter to decode the content of the file from Base64 encoding. .EXAMPLE # Example 1: Retrieve the content of a single file from a Gitea repository $file = Get-GiteaFileContent -repoOwner "cityofnorwich" -repoName "software-premierone" -filePath "README.md" -token "your_token" .EXAMPLE # Example 2: Retrieve the content of multiple files from a Gitea repository $files = Get-GiteaFileContent -repoOwner "cityofnorwich" -repoName "software-premierone" -filePath @("README.md", "Manually Create Installer Package.ps1") -token "your_token" .EXAMPLE # Example 3: Retrieve the content of multiple files from a Gitea repository using pipeline input $filesArray = @("README.md", "Manually Create Installer Package.ps1") | Get-GiteaFileContent -repoOwner "cityofnorwich" -repoName "software-premierone" -token "your_token" #> [CmdletBinding()] param( [Parameter(ValueFromPipelineByPropertyName)] [string]$giteaURL = "https://gitea.norwichct.tech", [Parameter(ValueFromPipelineByPropertyName)] [string]$repoOwner, [Parameter(ValueFromPipelineByPropertyName)] [string]$repoName, [Parameter(ValueFromPipelineByPropertyName)] [string]$branch = "main", [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)] [Alias('FullName', 'Path')] [string[]]$filePath, [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" Write-Verbose "repoOwner: $repoOwner" Write-Verbose "repoName: $repoName" Write-Verbose "branch: $branch" Write-Verbose "token: $token" Write-Verbose "decode: $decode" $headers = @{ "Authorization" = "token $token" "Accept" = "application/json" } } process { foreach ($file in $filePath) { Write-Verbose "Processing file: $file" $encodedFile = [System.Uri]::EscapeDataString($file) Write-Verbose "Encoded file: $encodedFile" $url = "$giteaURL" $url += "/api/v1/repos" $url += "/$repoOwner" $url += "/$repoName" $url += "/contents" $url += "/$encodedFile" $url += "?ref=$branch" Write-Verbose "URL: $url" try { $fileContent = Invoke-RestMethod -Uri $url -Method Get -Headers $headers $content = if ($decode) { [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($fileContent.content)) } else { $fileContent.content } $results += [PSCustomObject]@{ Path = $file Content = $content Size = $fileContent.size SHA = $fileContent.sha Success = $true Error = $null } } catch { Write-Error "Failed to retrieve file '$file' from Gitea: $_" $results += [PSCustomObject]@{ Path = $file Content = $null Size = $null SHA = $null Success = $false Error = $_.Exception.Message } } } } end { return $results } } 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 Get-GiteaLFSFile { [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 $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" } } process { $paths = $path foreach ($path in $paths) { Write-Verbose "Processing path: $path" # Normalize the path format - replace backslashes with forward slashes and trim trailing slashes $normalizedPath = $path -replace '\\', '/' -replace '/$', '' $encodedPath = [System.Uri]::EscapeDataString($normalizedPath) Write-Verbose "Normalized path: $normalizedPath" Write-Verbose "Encoded path: $encodedPath" $url = "$giteaURL" $url += "/api/v1/repos" $url += "/$repoOwner" $url += "/$repoName" $url += "/media" # Only add the path component if it's not empty if (-not [string]::IsNullOrWhiteSpace($normalizedPath)) { $url += "/$encodedPath" } $url += "?ref=$branch" Write-Verbose "URL: $url" try { $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers -ResponseHeadersVariable ResponseHeaders - } catch { } } } end { return $ResponseHeaders } } Function Invoke-GiteaFileDownload { <# .SYNOPSIS Downloads a file from a Gitea repository using a direct download URL. .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. .PARAMETER token A personal access token for the Gitea server. .PARAMETER outputPath The path where the downloaded file should be saved. If not specified, the file will be saved in the current directory using the filename from the URL. .PARAMETER force A switch parameter to force overwriting of an existing file at the output path. .PARAMETER PreserveRelativePath A switch parameter to preserve the relative path structure from the repository. If used without specifying an outputPath, creates the directory structure in the current location. If used with outputPath, treats the outputPath as the base directory to append the relative path to. .PARAMETER type The type of the item to download (file, dir, lfs). This parameter is typically used with pipeline input from Get-GiteaChildItem. Directories will be skipped with a verbose message. .EXAMPLE # Example 1: Download a file to the current directory Invoke-GiteaFileDownload -downloadURL "https://gitea.example.com/api/v1/repos/owner/repo/raw/path/to/file.txt" -token "your_token" .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 .EXAMPLE # Example 4: Download files preserving their relative paths Get-GiteaChildItem -path "docs" -Recurse | Where-Object { $_.type -eq 'file' } | Invoke-GiteaFileDownload -PreserveRelativePath .EXAMPLE # Example 5: Download files preserving their relative paths with a base output directory Get-GiteaChildItem -path "docs" -Recurse | Where-Object { $_.type -eq 'file' } | Invoke-GiteaFileDownload -PreserveRelativePath -outputPath "C:\Downloads" #> [cmdletbinding()] param( [Parameter(ValueFromPipelineByPropertyName)] [string]$downloadURL, [string]$token, [Parameter(ValueFromPipelineByPropertyName)] [string]$outputPath, [switch]$force, [Parameter(ValueFromPipelineByPropertyName)] [string]$filePath, [switch]$PreserveRelativePath, [Parameter(ValueFromPipelineByPropertyName)] [string]$type ) 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}))" Write-Verbose "PreserveRelativePath: $PreserveRelativePath" # Create a WebClient to be reused $webClient = New-Object System.Net.WebClient $webClient.Headers.Add("Authorization", "token $token") } process { Write-Verbose "File path: $filePath" Write-Verbose "Type: $type" # Skip directories and provide a verbose message if ($type -eq "dir") { Write-Verbose "Skipping directory: $filePath" return [PSCustomObject]@{ Path = $filePath SourceURL = $downloadURL FilePath = $filePath Type = $type Success = $true Error = "Skipped - item is a directory" Timestamp = Get-Date FileSize = $null } } # Handle invalid entry types if ([string]::IsNullOrEmpty($type) -and [string]::IsNullOrEmpty($downloadURL)) { $errorMsg = "Invalid item type or missing download URL for path: $filePath" Write-Error $errorMsg return [PSCustomObject]@{ Path = $filePath SourceURL = $downloadURL FilePath = $filePath Type = $type Success = $false Error = $errorMsg Timestamp = Get-Date FileSize = $null } } Write-Verbose "Processing download URL: $downloadURL" # Set the output path based on parameters $currentOutputPath = "" # Get the file name from the download URL or file path $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]) if ($PreserveRelativePath -and $filePath) { # If PreserveRelativePath is used, set up the directory structure Write-Verbose "Preserving relative path structure for: $filePath" if ($outputPath) { # If outputPath is specified, use it as the base directory $baseDir = $outputPath Write-Verbose "Using base directory: $baseDir" # Combine the base directory with the file path $currentOutputPath = Join-Path -Path $baseDir -ChildPath $filePath } else { # If no outputPath is specified, use the current directory as base $currentOutputPath = Join-Path -Path (Get-Location) -ChildPath $filePath } # Normalize path separators $currentOutputPath = $currentOutputPath.Replace("/", [System.IO.Path]::DirectorySeparatorChar) Write-Verbose "Output path with preserved structure: $currentOutputPath" } else { # Standard path handling (unchanged from original) if ($outputPath) { $currentOutputPath = $outputPath } else { # Append the outputFileName to the current location $currentOutputPath = Join-Path -Path (Get-Location) -ChildPath $outputFileName } Write-Verbose "Output path: $currentOutputPath" } $result = [PSCustomObject]@{ Path = $currentOutputPath SourceURL = $downloadURL FilePath = $filePath Type = $type Success = $false Error = $null Timestamp = Get-Date FileSize = $null } if((Test-Path -Path $currentOutputPath -PathType Leaf) -and (-not $force)) { $errorMsg = "The file '$currentOutputPath' already exists. Use the -Force switch to overwrite the file." Write-Error $errorMsg $result.Error = $errorMsg return $result } 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) { Write-Verbose "Creating directory structure: $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" # Get file info for the result object if (Test-Path -Path $currentOutputPath -PathType Leaf) { $fileInfo = Get-Item -Path $currentOutputPath $result.FileSize = $fileInfo.Length } $result.Success = $true return $result } catch { $errorMsg = "Failed to download file from Gitea: $_" Write-Error $errorMsg $result.Error = $errorMsg return $result } } end { # Clean up resources if ($webClient) { $webClient.Dispose() } } } 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. .PARAMETER Recurse Recursively lists items in all subdirectories. .PARAMETER Depth Specifies the number of subdirectory levels to include in the recursion. Default is unlimited if Recurse is specified. .PARAMETER File Returns only files, omitting directories from the results. .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 .EXAMPLE # Recursively list all items in a repository Get-GiteaChildItem -repoOwner "owner" -repoName "repo" -Path "src" -Recurse -token "your_token" .EXAMPLE # List items with a maximum depth of 2 subdirectories Get-GiteaChildItem -repoOwner "owner" -repoName "repo" -Path "src" -Recurse -Depth 2 -token "your_token" .EXAMPLE # List only files (no directories) in a repository Get-GiteaChildItem -repoOwner "owner" -repoName "repo" -Path "src" -File -token "your_token" #> [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, [switch]$Recurse, [int]$Depth, [switch]$File ) begin { # Initialize results array $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" Write-Verbose "File: $File" $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 # Initialize a queue for directories to process when using recursion $directoryQueue = [System.Collections.Queue]::new() } process { $paths = $path foreach ($path in $paths) { Write-Verbose "Processing path: $path" # Normalize the path format - replace backslashes with forward slashes and trim trailing slashes $normalizedPath = $path -replace '\\', '/' -replace '/$', '' $encodedPath = [System.Uri]::EscapeDataString($normalizedPath) Write-Verbose "Normalized path: $normalizedPath" Write-Verbose "Encoded path: $encodedPath" $url = "$giteaURL" $url += "/api/v1/repos" $url += "/$repoOwner" $url += "/$repoName" $url += "/contents" # Only add the path component if it's not empty if (-not [string]::IsNullOrWhiteSpace($normalizedPath)) { $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)" } $itemObj = [PSCustomObject]@{ filePath = $item.path Path = $item.path repoOwner = $repoOwner repoName = $repoName giteaURL = $giteaURL downloadURL = $item.download_url branch = $branch type = $item.type name = $item.name size = $item.size sha = $item.sha Success = $true Error = $null Level = 0 } $results += $itemObj # If the item is a directory and we're recursing, add it to the queue if ($Recurse -and $item.type -eq 'dir') { $directoryQueue.Enqueue(@{ Path = $item.path Level = 1 }) } } # Process the directory queue if recursion is enabled if ($Recurse) { while ($directoryQueue.Count -gt 0) { $currentDir = $directoryQueue.Dequeue() $currentPath = $currentDir.Path $currentLevel = $currentDir.Level # Check depth limit if specified if ($PSBoundParameters.ContainsKey('Depth') -and $currentLevel -gt $Depth) { continue } Write-Verbose "Recursing into: $currentPath (Level: $currentLevel)" # Normalize the path format for consistent handling $normalizedSubPath = $currentPath -replace '\\', '/' -replace '/$', '' $encodedSubPath = [System.Uri]::EscapeDataString($normalizedSubPath) Write-Verbose "Normalized sub path: $normalizedSubPath" Write-Verbose "Encoded sub path: $encodedSubPath" # Fix the URL construction for subdirectories $subUrl = "$giteaURL/api/v1/repos/$repoOwner/$repoName/contents/$encodedSubPath" # Add the branch reference separately to avoid issues $subUrl += "?ref=$branch" Write-Verbose "Sub URL: $subUrl" try { $subResponse = Invoke-RestMethod -Uri $subUrl -Method Get -Headers $headers -ErrorAction Stop $subItems = if ($subResponse -is [array]) { $subResponse } else { @($subResponse) } foreach ($subItem in $subItems) { # Check if the name ends with any of the LFS extensions $isLFS = $false foreach ($ext in $lfsExtensions) { if ($subItem.name.EndsWith($ext, [System.StringComparison]::InvariantCultureIgnoreCase)) { $isLFS = $true break } } if ($isLFS) { $subItem.type = "lfs" $subItem.download_url = "$giteaURL/api/v1/repos/$repoOwner/$repoName/media/$($subItem.path)" } $subItemObj = [PSCustomObject]@{ filePath = $subItem.path Path = $subItem.path repoOwner = $repoOwner repoName = $repoName giteaURL = $giteaURL downloadURL = $subItem.download_url branch = $branch type = $subItem.type name = $subItem.name size = $subItem.size sha = $subItem.sha Success = $true Error = $null Level = $currentLevel } $results += $subItemObj # If the subitem is a directory, add it to the queue for further processing if ($subItem.type -eq 'dir') { $nextLevel = $currentLevel + 1 # Only add to queue if we haven't hit our depth limit if (-not $PSBoundParameters.ContainsKey('Depth') -or $nextLevel -le $Depth) { $directoryQueue.Enqueue(@{ Path = $subItem.path Level = $nextLevel }) } } } } catch { $errorDetails = if ($_.ErrorDetails) { try { $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue } catch { $_.ErrorDetails.Message } } else { $null } Write-Error "Failed to recursively retrieve path '$normalizedSubPath' from Gitea: $($_.Exception.Message)`n$($errorDetails | ConvertTo-Json -Depth 1 -Compress)" } } } } catch { $errorDetails = if ($_.ErrorDetails) { try { $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue } catch { $_.ErrorDetails.Message } } else { $null } Write-Error "Failed to retrieve '$normalizedPath' from Gitea: $($_.Exception.Message)`n$($errorDetails | ConvertTo-Json -Depth 1 -Compress)" $results += [PSCustomObject]@{ filePath = $normalizedPath Path = $normalizedPath repoOwner = $repoOwner repoName = $repoName giteaURL = $giteaURL branch = $branch type = $null name = $null size = $null sha = $null downloadURL = $null Success = $false Error = $_.Exception.Message Level = 0 } } } } end { # Filter out directories if -File switch is specified if ($File) { Write-Verbose "Filtering results to include only files" $results = $results | Where-Object { $_.type -ne 'dir' } } return $results } } Export-ModuleMember -Function Set-GiteaConfiguration, Get-GiteaConfiguration, Get-GiteaFileContent, Invoke-GiteaFileDownload, Get-GiteaChildItem, Get-GiteaLFSFile