public/Import-Bucket.ps1

function Import-Bucket {
    <#
    .SYNOPSIS
    Imports objects from an archive file into a bucket.
    .DESCRIPTION
    Reads objects from a JSON or CLIXML archive file and stores them in a bucket.
    Format is auto-detected by file extension (.json = JSON, otherwise = CLIXML/binary).
    Use -AsBinary to force CLIXML/binary format regardless of extension.
    Preserves original keys if objects have _BucketKey metadata; otherwise generates new keys.
    .PARAMETER Bucket
    Destination bucket name. Creates the bucket if it doesn't exist.
    .PARAMETER Path
    Root directory for bucket storage. Default: $HOME/.buckets.
    .PARAMETER InputFile
    Path to the archive file to import.
    .PARAMETER AsBinary
    Force import from CLIXML/binary format (auto-detected by file extension if omitted).
    .PARAMETER Overwrite
    Overwrite existing objects with the same key.
    .PARAMETER Quiet
    Suppress all output.
    .EXAMPLE
    Import-Bucket -Bucket users -InputFile "./users-backup.json"
    .EXAMPLE
    Import-Bucket -Bucket config -InputFile "./config-backup.clixml" -AsBinary
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$Bucket,
        [Parameter(Mandatory = $true)][string]$InputFile,
        [string]$Path,
[switch]$AsBinary,
        [switch]$Overwrite,
        [switch]$Quiet
    )

    if ([string]::IsNullOrWhiteSpace($Path)) { $Path = Get-DefaultPath }
    if (-not [System.IO.File]::Exists($InputFile)) {
        throw "Input file '$InputFile' not found"
    }

    $rawBytes = [System.IO.File]::ReadAllBytes($InputFile)
    $useJson = -not $AsBinary -and $InputFile -like "*.json"

    if ($useJson) {
        $content = [System.IO.File]::ReadAllText($InputFile, [System.Text.Encoding]::UTF8)
        $objects = $content | ConvertFrom-Json
    }
    else {
        try {
            $isCompressed = $rawBytes.Length -ge 2 -and $rawBytes[0] -eq 0x1F -and $rawBytes[1] -eq 0x8B
            if ($isCompressed) {
                $ms = [System.IO.MemoryStream]::new($rawBytes)
                try {
                    $decompressed = [System.IO.Compression.GZipStream]::new($ms, [System.IO.Compression.CompressionMode]::Decompress)
                    try {
                        $reader = [System.IO.StreamReader]::new($decompressed)
                        try { $content = $reader.ReadToEnd() }
                        finally { $reader.Close() }
                    }
                    finally { $decompressed.Close() }
                }
                finally { $ms.Dispose() }
                $objects = Read-SafeClixml -Clixml $content
            }
            else {
                $objects = Read-SafeClixml -Clixml ([System.Text.Encoding]::UTF8.GetString($rawBytes))
            }
        }
        catch {
            throw "Failed to deserialize archive file '$InputFile': $_"
        }
    }

    if ($null -eq $objects) { throw "Failed to deserialize archive file '$InputFile'" }

    $objectArray = @($objects)
    Write-Verbose "Loaded $($objectArray.Count) objects from '$InputFile'"

    $bucketPath = Ensure-BucketExists -Name $Bucket -Path $Path
    $importedCount = 0; $skippedCount = 0
    $skippedKeys = [System.Collections.ArrayList]::new()

    foreach ($obj in $objectArray) {
        $key = if ($obj.PSObject.Properties['_BucketKey']) { $obj._BucketKey } else { [Guid]::NewGuid().ToString() }
        $safeKey = $key -replace '[\\/:\*\?"<>\|\[\]]', '_'
        if ([string]::IsNullOrWhiteSpace($safeKey) -or $safeKey -match '^_+$') {
            Write-Warning "Key '$key' in imported object is invalid after sanitization, using GUID instead"
            $safeKey = [Guid]::NewGuid().ToString()
        }

        $jsonPath = Join-Path $bucketPath "${safeKey}.json"
        $datPath = Join-Path $bucketPath "${safeKey}.dat"
        $filePath = $null
        if ([System.IO.File]::Exists($jsonPath)) { $filePath = $jsonPath }
        elseif ([System.IO.File]::Exists($datPath)) { $filePath = $datPath }

        if ($filePath -and -not $Overwrite) {
            Write-Verbose "Object with key '$safeKey' already exists in bucket '$Bucket'. Use -Overwrite to replace."
            $skippedCount++
            $null = $skippedKeys.Add($safeKey)
            continue
        }

        $ext = if ($filePath) { [System.IO.Path]::GetExtension($filePath) } else { ".dat" }
        $finalPath = Join-Path $bucketPath "${safeKey}${ext}"

        if ($ext -eq ".json") {
            $json = ConvertTo-Json -InputObject $obj -Depth 20 -Compress
            [System.IO.File]::WriteAllText($finalPath, $json, [System.Text.Encoding]::UTF8)
        }
        else {
            $xml = [System.Management.Automation.PSSerializer]::Serialize($obj, 5)
            $rawBytes = [System.Text.Encoding]::UTF8.GetBytes($xml)
            [System.IO.File]::WriteAllBytes($finalPath, $rawBytes)
        }
        $importedCount++
    }

    if (-not $Quiet) {
        Write-Host "$([System.IO.Path]::GetFileName($InputFile))" -NoNewline -ForegroundColor $script:CAction
        Write-Host " → " -NoNewline -ForegroundColor $script:CMuted
        Write-Host "$Bucket" -NoNewline -ForegroundColor $script:CPath
        Write-Host " · " -NoNewline -ForegroundColor $script:CMuted
        Write-Host $importedCount -NoNewline -ForegroundColor $script:CNum
        Write-Host " objects" -ForegroundColor $script:CMuted
        if ($skippedCount -gt 0) {
            Write-Host " " -NoNewline
            Write-Host $skippedCount -NoNewline -ForegroundColor $script:CNum
            $skipDisplay = if ($skippedKeys.Count -le 5) { $skippedKeys -join ", " } else { ($skippedKeys | Select-Object -First 5) -join ", " + " ... +$($skippedKeys.Count - 5) more" }
            Write-Host " skipped (existing keys: $skipDisplay)" -ForegroundColor $script:CSkip
        }
    }
}