public/Set-LimitFileSize.ps1

function Set-LimitFileSize {
    <#
    .SYNOPSIS
        Reduces the size of one or more text files by keeping only the most recent lines until a specified size threshold is met.

    .DESCRIPTION
        This script is designed to automatically manage large files (such as logs). For each specified file, if it exceeds a size threshold in Ko or Mo (defined by the MaxSize parameter), its content is re-read line by line, skipping empty lines. The lines are processed in reverse order (from most recent to oldest) until the accumulated data stays below the threshold.

        The output is written to a temporary file to avoid issues related to file locking by another process, then the temporary file replaces the original. No unwanted trailing newline is added at the end of the rewritten file.

        You can provide one or more file paths to trim multiple files in a single execution.

    .PARAMETER Path
        Specifies one or more absolute or relative paths to the file(s) to be analyzed and potentially trimmed.

    .PARAMETER MaxSize
        The maximum size the file is allowed to reach before triggering a trimming operation.

        The value must be a number followed optionally by a unit :
        - Use "Ko" for kilooctets (e.g., "100000Ko")
        - Use "Mo" for megaoctets (e.g., "150Mo")
        - If no unit is specified, kilooctets are assumed by default (e.g., "200" means "200Ko").

        Default: "256Ko"

    .PARAMETER VerboseLevel
        Controls the script's verbosity level :
        - "Disabled" : no console output.
        - "Normal" : minimal output.
        - "Debug" : displays details such as initial and final file size.

    .EXAMPLE
        Set-LimitFileSize -Path "lorem.txt" -MaxSize 3Ko -VerboseLevel Debug

        This will check if the file "lorem.txt" exceeds 3 Ko. If so, it will delete the oldest lines, keeping only the most recent ones until the size is below 5 Mo.

    .EXAMPLE
        Set-LimitFileSize -Path "C:\Logs\app1.log", "C:\Logs\app2.log" -MaxSize 10Mo -VerboseLevel Debug

        This will check if either app1.log or app2.log exceeds 256 Ko. If so, it will keep only the most recent lines from each file until the size falls below the threshold.

    .NOTES
        Version : 1.0.0
        Author : Frederic PETIT
        Created : 2025-05-22
        Revised : 2025-05-31

        Compatibility: PowerShell 5+
    #>


    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory=$true)][string[]]$Path,
        [Parameter(Mandatory=$false)][string]$MaxSize = "256Ko",
        [Parameter(Mandatory=$false)][ValidateSet("Disabled", "Debug")][string]$VerboseLevel = "Disabled"
    )

    # Boucle : fichiers.
    foreach ($file in $Path) {
        # Test : fichier existe.
        if (Test-Path -LiteralPath $file) {
            $resolvedPath = Resolve-Path -LiteralPath $file | Select-Object -ExpandProperty Path;
            # Récupération de la taille actuelle du fichier en octets.
            $currentSize = (Get-Item $resolvedPath).Length;
            # Test : int ou valeur KO/MO.
            # Conversion de MaxSize en octets.
            if ($MaxSize -match '^(\d+)(Ko|Mo)?$') {
                $sizeValue = [int]$matches[1]
                $unit = if ($matches[2]) { $matches[2] } else { "Ko" }
                $sizeLimit = switch ($unit) {
                    "Ko" { $sizeValue * 1KB }
                    "Mo" { $sizeValue * 1MB }
                }
                # Test : quand le fichier est en-dessous du seuil, il est inutile de continuer.
                if ($currentSize -gt $sizeLimit) {
                    # Génération d'un fichier temporaire pour éviter les conflits avec des processus verrouillant le fichier original.
                    $tmpFile = [System.IO.Path]::GetTempFileName();
                    # Initialisation des compteurs et du tampon mémoire.
                    $accumulatedSize = 0;
                    $linesBuffer = [System.Collections.Generic.List[string]]::new();
                    $utf8 = [Text.Encoding]::UTF8;
                    # L'initialisation explicite des variables $fs et $sr à $null avant leur utilisation permet de garantir leur existence dans la portée du script, même si une erreur survient avant leur affectation réelle. Cela évite que le bloc finally échoue lorsqu'il tente de fermer ces objets avec $fs.Close() ou $sr.Close(). Sans cette précaution, une exception pourrait être levée si les variables n'ont jamais été créées. Cette pratique renforce la robustesse du script en assurant que les ressources sont libérées correctement, même en cas d'erreur, tout en évitant des erreurs de type "variable non définie".
                    $fs = $null;
                    $sr = $null;
                    # Lit le fichier ligne par ligne, stocke uniquement les lignes non vides en mémoire.
                    try {
                        # Lecture optimisée du fichier ligne par ligne (streaming, pas de chargement complet).
                        $fs = [System.IO.File]::OpenRead($resolvedPath);
                        $sr = New-Object System.IO.StreamReader($fs);
                        $allLines = New-Object System.Collections.Generic.List[string];
                        # Chaque ligne non vide est ajoutée pour traitement ultérieur.
                        while (-not $sr.EndOfStream) {
                            $line = $sr.ReadLine();
                            if ($line -ne '') {
                                $allLines.Add($line);
                            }
                        }
                    }
                    finally {
                        # Fermeture systématique des flux pour éviter tout verrouillage ultérieur.
                        if ($sr) { $sr.Close() };
                        if ($fs) { $fs.Close() };
                    }
                    # Boucle : traitement des lignes depuis la plus récente (fin du fichier) vers l'ancienne, ajout des lignes uniquement jusqu'à ce que le total atteigne le seuil.
                    for ($i = $allLines.Count - 1; $i -ge 0; $i--) {
                        $line = $allLines[$i];
                        $lineSize = $utf8.GetByteCount("$line`n");
                        $newTotal = $accumulatedSize + $lineSize;
                        if ($newTotal -le $sizeLimit) {
                            # Insertion en tête pour retrouver l'ordre chronologique.
                            $linesBuffer.Add($line);
                            $accumulatedSize = $newTotal;
                        } else {
                            # Stoppe la boucle sans break.
                            $i = -1;
                        }
                    }
                    # Affichage informatif en mode Debug.
                    if ($VerboseLevel -eq "Debug") {
                        $currentMo = [Math]::Round($currentSize / 1MB, 2);
                        $targetMo  = [Math]::Round($accumulatedSize / 1MB, 2);
                        $currentKo = [Math]::Round($currentSize / 1KB, 2);
                        $targetKo  = [Math]::Round($accumulatedSize / 1KB, 2);
                        Write-Host "Current size : $currentMo Mo ($currentKo Ko) / Target size after reduction : $targetMo Mo ($targetKo Ko).";
                    }
                    # Écriture ligne à ligne sans ajouter de saut de ligne après la dernière en évitant ici WriteAllLines car il force un retour à la ligne final. L'utilisation de Add() à la place de Insert(0, ...) dans la construction de la liste des lignes à conserver est motivée par une amélioration de performance. En effet, Insert(0, ...) insère chaque nouvel élément en début de liste, ce qui implique un décalage de tous les éléments existants à chaque insertion. Cette opération est coûteuse en temps, surtout pour un grand nombre de lignes. À l'inverse, Add(...) ajoute simplement l'élément en fin de liste, ce qui est bien plus efficace. Pour conserver l'ordre chronologique dans le fichier final (des lignes les plus anciennes aux plus récentes), on adapte alors la boucle d'écriture pour parcourir la liste à l'envers. Ce changement améliore la performance globale sans altérer le résultat fonctionnel attendu.
                    $sw = $null;
                    try {
                        $sw = [System.IO.StreamWriter]::new($tmpFile, $false, $utf8);
                        for ($i = $linesBuffer.Count - 1; $i -ge 0; $i--) {
                            if ($i -gt 0) {
                                $sw.WriteLine($linesBuffer[$i]);
                            } else {
                                $sw.Write($linesBuffer[$i]);
                            }
                        }
                        $sw.Close();
                        $sw = $null;
                        # Calcul du hash du fichier temporaire avant de le déplacer.
                        $hashTmp = Get-FileHash -Path $tmpFile -Algorithm MD5;
                        # Remplace le fichier original par le fichier temporaire généré, Move-Item écrase le fichier cible grâce à -Force.
                        Move-Item -Force -Path $tmpFile -Destination $resolvedPath;
                        # Vérification que le fichier déplacé est bien identique.
                        $hashFinal = Get-FileHash -Path $resolvedPath -Algorithm MD5;
                        if ($hashFinal.Hash -ne $hashTmp.Hash) {
                            if ($VerboseLevel -eq "Debug") {
                                Write-Warning "The final file does not match the temporary file. Check permissions or possible locks.";
                            }
                        }
                    } catch {
                        if ($VerboseLevel -eq "Debug") {
                            Write-Warning "An error occurred while writing : $($_.Exception.Message)";
                        }
                        if (Test-Path $tmpFile) {
                            Remove-Item -Force $tmpFile;
                        }
                    } finally {
                            if ($sw) { $sw.Close(); }
                    }
                } else {
                    if ($VerboseLevel -eq "Debug") {
                        Write-Host "Filesize is already good.";
                    }
                }
            } else {
                if ($VerboseLevel -eq "Debug") {
                    Write-Warning "Invalid MaxSize format. Use numeric value optionally followed by Ko or Mo.";
                }
            }
        } else {
            if ($VerboseLevel -eq "Debug") {
                Write-Warning "Source not found : '$file'.";
            }
        }
    }
}