# moveFilesToZip.ps1
# Version 0.02
# This little function is to automate the process of
# a. Creating an empty zip file
# b. Gathering source files to move
# c. Adding those files into zip archive
# d. Purging the source files
# e. Keeping track of memory utilization so that program would not exceed available RAM and crash
# f. Ensure that accidential directories such as C:\Windows and C:\ProgramData are detected as inputs to cancel execution
# Notes:
# To reduce memory consumption, the workflow is sequential so that only one file is processed per iteration
# This comes at the cost of performance efficiency, although it must be so to overcome a host's memory limitations.
# Optimization has been done by keeping the zip file locked during operations to save io cycles of instantiating the zip update command per iteration.

# Credentials
$username='intranet\backupadmin'
$password='somecomplexpassword'
$encryptedPassword=ConvertTo-SecureString $password -AsPlainText -Force
$credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $userName,$encryptedPassword;

$computerNames=@(
    'server001',   
    'server002'
)

$parentDirectory='C:\Program Files\Path\To\Logs'
$compressFilesOlderThanDays=7
$destinationZipFile="B:\oldLogFiles.zip"
$maxZipSize='1GB'
$recurse=$true
 
function moveFilesToZip ($parentDirectory,$filesDaysOlderThan,$zipFile,$maxZipSize,$recurse=$true,$verbose=$true){
    # Built-in safety check
    $hardStopLocations=@(
        'C:\Windows',
        'C:\ProgramData',
        'C:\Users'
        )
    $ErrorActionPreference='ignore'
    if($hardStopLocations|?{$parentDirectory -like "$_*"}){
    write-warning "$parentDirectory is not a safe location to compress files."
    return $false
    }
 
    function displaySessionInfoInTitleBar{    
        $thisProcessName=.{
            $psProcess = get-process -id $pid # Accessing the system reserved variable as pointer to this object's PID
            $psInstances = (get-process -Name $psProcess.name).count # gathering all instances of this process name
            if ($psInstances -gt 1){ # Distinguish this PS Session from other sessions, if any
                return $("{0}#{1}" -f $psProcess.name,$($psInstances-1))
            }else{            
                return $psProcess.name            
            } 
        }               
        $GLOBAL:psCpuPerf = new-object System.Diagnostics.PerformanceCounter("Process","% Processor Time",$thisProcessName) # Create the Performance Counter Object             
        $GLOBAL:timer   = New-Object System.Timers.Timer # Create a timer with 1000ms interval
        $timer.Interval = 1000            
        $null=Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {            
            $psInfo = Get-Process -id $pid           
            [int]$memory = $psInfo.workingset/1MB            
            [int]$cpu = $psCpuPerf.NextValue()/$env:NUMBER_OF_PROCESSORS
            #[string]$currentDir=(get-location).ProviderPath
            [string]$psVersion=$Host.Version
            [string]$hostName=$Host.Name
            $Host.UI.RawUI.WindowTitle="$hostName`: $ENV:Username | PSVersion: $psVersion | MemoryUtilization: $memory`MB | CpuUtilization: $cpu%"
            #$Host.UI.RawUI.WindowTitle="SessionType: $hostName | User: $ENV:Username | PSVersion: $psVersion | CurrentDirectory: $currentDir | CpuUtilization: $cpu% | MemoryUtilization: $memory`MB"            
        }        
        $timer.start() # kickoff timer
    }
 
    function isMemoryAvailable($acceptableUtilization=0.75,$verbose=$false){        
        $memory=Get-CIMInstance Win32_OperatingSystem
        $availableMemory=$memory.FreePhysicalMemory
        $systemMemory=$memory.TotalVisibleMemorySize
        if($verbose){
            write-host "Memory check: $([math]::round($systemMemory/1048576,2)) GB detected, and $([math]::round($availableMemory/1048576,2)) GB is currently free."
            }
        if(($availableMemory/$systemMemory) -ge $acceptableUtilization){
            return $false
        }else{
            return $true
            }
    }
 
    # Start displaying session stats
    displaySessionInfoInTitleBar
 
    # Check RAM
    isMemoryAvailable -verbose $true
    #$powerShellMaxRamPerSession=Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB
 
    # Fastest way to list all files in a directory
    if(($filesDaysOlderThan -eq 0) -and $recurse){
        $getFiles = "cmd.exe /C dir '$parentDirectory' /S /B /W /A:-D"   
        $filesToCompress = Invoke-Expression -Command:$getFiles
    }else{
        $filesToCompress=(gci $parentDirectory|?{!$_.PSIsContainer -and ($_.LastWriteTime -lt (Get-Date).AddDays(-$filesDaysOlderThan))}).FullName
        }
 
    if(!(test-path $zipFile)){
        #New-Item -Name $zipFile -ItemType File -Force # Error: cannot create file with this extension using command
        $fileIo=[System.IO.File]::Create($zipFile) # invoke dotnet to overcome PShell filetype checking
        $fileIo.Close()
        } 

    function toRotateFilename($fileName,$maxGb){
        $size=(Get-Item $fileName).length
        if($size -gt $maxGb/1){
            return $true
        }else{
            return $false
        }
    }        

    Add-Type -Assembly 'System.IO.Compression.FileSystem' #including .NET 3.x or 4.x assembly
    $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
    $zipIo = [System.IO.Compression.ZipFile]::Open($zipFile, 'update')
    foreach ($file in $filesToCompress){
        #$validLocation=!($hardStopLocations|?{$file -like "$_*"}) # perform location safety check for each file i case symlinks were used
        $toRotateFileName=toRotateFilename $zipFile $maxZipSize
        if($toRotateFileName){
            #$randomSuffix=-join ((65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_})
            $filenameArray=[regex]::match($(split-path $zipFile -Leaf),'(.*)\.(.*)').groups
            $extractedFilename=$filenameArray[1].Value
            $suffix=$filenameArray[2].Value
            $timeStamp=get-date -Format yyyyMMdd-HHmm
            $newFileName=$($extractedFilename+"_$timeStamp."+$suffix)
            $newFilePath=join-path $(split-path $zipFile -parent) $newFileName
            $zipIo.Dispose()
            # rename-item $zipFile $newFileName -force
            # the move-item method would consistently override duplicate file names
            Move-Item -Path $zipFile -Destination $newFilePath -Force
            new-item $zipFile
            $zipIo = [System.IO.Compression.ZipFile]::Open($zipFile, 'update')
        }
        $validLocation=$true
        if((isMemoryAvailable $systemMemory) -and $validLocation){
            try{
                $null=[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipIo,$file,$(split-path $file -leaf),$compressionLevel)
                $addedSuccessfully=$true
            }catch{
                Write-Error $_
                $addedSuccessfully=$false
                }
        }elseif($validLocation){ # Releases memory
            $zipIo.Dispose()
            [GC]::Collect() # enforces garbage collection
            $zipIo = [System.IO.Compression.ZipFile]::Open($zipFile, 'update')
            try{
                $null=[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipIo,$file,$(split-path $file -leaf),$compressionLevel)
                $addedSuccessfully=$true
            }catch{
                Write-Error $_
                $addedSuccessfully=$false
                }
            }
        if($addedSuccessfully -and $validLocation){
            remove-item $file -Force
            write-host "$file => moved to zip successfully." -ForegroundColor Green
        }elseif($validLocation){
            write-host "$file => NOT moved to zip successfully" -ForegroundColor Yellow
            }
    }
    if($zipIo){$zipIo.Dispose()}
}

foreach($computer in $computernames){
    $session=try{
        New-PSSession -ComputerName $computer -Credential $credentials -EA Stop
    }catch{
        New-PSSession -ComputerName $computer -Credential $credentials -SessionOption $(new-pssessionoption -IncludePortInSPN)
    }
    if($session.State -eq 'Opened'){
        write-host "Invoking functions on $computer`r`n----------------------------------------"        
        invoke-command -Session $session -ScriptBlock{
            param ($moveFilesToZip,$parentDirectory,$compressFilesOlderThanDays,$destinationZipFile,$maxZipSize)
                return [ScriptBlock]::Create($moveFilesToZip).invoke($parentDirectory,$compressFilesOlderThanDays,$destinationZipFile,$maxZipSize);
            } -Args ${function:moveFilesToZip},$parentDirectory,$compressFilesOlderThanDays,$destinationZipFile,$maxZipSize
        Remove-PSSession $session
    }else{
        write-warning "Unable to connect to $computer"
    }
}
# moveFilesToZip.ps1
# Version 0.01
# This little function is to automate the process of
# a. Creating an empty zip file
# b. Gathering source files to move
# c. Adding those files into zip archive
# d. Purging the source files
# e. Keeping track of memory utilization so that program would not exceed available RAM and crash
# f. Ensure that accidential directories such as C:\Windows and C:\ProgramData are detected as inputs to cancel execution
# Notes:
# To reduce memory consumption, the workflow is sequential so that only one file is processed per iteration
# This comes at the cost of performance efficiency, although it must be so to overcome a host's memory limitations.
# Optimization has been done by keeping the zip file locked during operations to save io cycles of instantiating the zip update command per iteration.

$parentDirectory='C:\Program Files\Dynamics 365\Trace'
$compressFilesOlderThanDays=0
$destinationZipFile="\\Archive\crmLogs\$ENV:ComputerName\oldLogFiles.zip"
$recurse=$true

function moveFilesToZip ($parentDirectory,$filesDaysOlderThan,$zipFile,$recurse=$false,$verbose=$true){
    # Built-in safety check
    $hardStopLocations=@(
        'C:\Windows',
        'C:\ProgramData',
        'C:\Users'
        )
    if($hardStopLocations|?{$parentDirectory -like "$_*"}){
        write-warning "$parentDirectory is not a safe location to compress files."
        return $false
        }

    function displaySessionInfoInTitleBar{    
        $thisProcessName=.{
            $psProcess = get-process -id $pid # Accessing the system reserved variable as pointer to this object's PID
            $psInstances = (get-process -Name $psProcess.name).count # gathering all instances of this process name
            if ($psInstances -gt 1){ # Distinguish this PS Session from other sessions, if any
                return $("{0}#{1}" -f $psProcess.name,$($psInstances-1))
            }else{            
                return $psProcess.name            
            } 
        }               
        $GLOBAL:psCpuPerf = new-object System.Diagnostics.PerformanceCounter("Process","% Processor Time",$thisProcessName) # Create the Performance Counter Object             
        $GLOBAL:timer   = New-Object System.Timers.Timer # Create a timer with 1000ms interval
        $timer.Interval = 1000            
        $null=Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action {            
            $psInfo = Get-Process -id $pid            
            [int]$memory = $psInfo.workingset/1MB            
            [int]$cpu = $psCpuPerf.NextValue()/$env:NUMBER_OF_PROCESSORS
            #[string]$currentDir=(get-location).ProviderPath
            [string]$psVersion=$Host.Version
            [string]$hostName=$Host.Name
            $Host.UI.RawUI.WindowTitle="$hostName`: $ENV:Username | PSVersion: $psVersion | MemoryUtilization: $memory`MB | CpuUtilization: $cpu%" 
            #$Host.UI.RawUI.WindowTitle="SessionType: $hostName | User: $ENV:Username | PSVersion: $psVersion | CurrentDirectory: $currentDir | CpuUtilization: $cpu% | MemoryUtilization: $memory`MB"            
        }        
        $timer.start() # kickoff timer
    }

    function isMemoryAvailable($acceptableUtilization=0.75,$verbose=$false){        
        $memory=Get-CIMInstance Win32_OperatingSystem
        $availableMemory=$memory.FreePhysicalMemory
        $systemMemory=$memory.TotalVisibleMemorySize
        if($verbose){
            write-host "Memory check: $([math]::round($systemMemory/1048576,2)) GB detected, and $([math]::round($availableMemory/1048576,2)) GB is currently free."
            }
        if(($availableMemory/$systemMemory) -ge $acceptableUtilization){
            return $false
        }else{
            return $true
            }
    }

    # Start displaying session stats
    displaySessionInfoInTitleBar

    # Check RAM
    isMemoryAvailable -verbose $true
    #$powerShellMaxRamPerSession=Get-Item WSMan:\localhost\Shell\MaxMemoryPerShellMB

    # Fastest way to list all files in a directory
    if(($filesDaysOlderThan -eq 0) -and $recurse){
        $getFiles = "cmd.exe /C dir '$parentDirectory' /S /B /W /A:-D"    
        $filesToCompress = Invoke-Expression -Command:$getFiles
    }else{
        $filesToCompress=(gci $parentDirectory|?{!$_.PSIsContainer -and ($_.LastWriteTime -lt (Get-Date).AddDays(-$filesDaysOlderThan))}).FullName
        }

    if(!(test-path $zipFile)){
        #New-Item -Name $zipFile -ItemType File -Force # Error: cannot create file with this extension using command
        $fileIo=[System.IO.File]::Create($zipFile) # invoke dotnet to overcome PShell filetype checking
        $fileIo.Close()
        } 

    Add-Type -Assembly 'System.IO.Compression.FileSystem' #including .NET 3.x or 4.x assembly
    $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
    $zipIo = [System.IO.Compression.ZipFile]::Open($zipFile, 'update')
    foreach ($file in $filesToCompress){
        #$validLocation=!($hardStopLocations|?{$file -like "$_*"}) # perform location safety check for each file i case symlinks were used
        $validLocation=$true
        if((isMemoryAvailable $systemMemory) -and $validLocation){
            try{
                $null=[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipIo,$file,$(split-path $file -leaf),$compressionLevel)
                $addedSuccessfully=$true
            }catch{
                Write-Error $_
                $addedSuccessfully=$false
                }
        }elseif($validLocation){ # Releases memory
            $zipIo.Dispose()
            [GC]::Collect() # enforces garbage collection
            $zipIo = [System.IO.Compression.ZipFile]::Open($zipFile, 'update')
            try{
                $null=[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipIo,$file,$(split-path $file -leaf),$compressionLevel)
                $addedSuccessfully=$true
            }catch{
                Write-Error $_
                $addedSuccessfully=$false
                }
            }
        if($addedSuccessfully -and $validLocation){
            remove-item $file -Force
            write-host "$file => moved to zip successfully." -ForegroundColor Green
        }elseif($validLocation){
            write-host "$file => NOT moved to zip successfully" -ForegroundColor Yellow
            }
    }
    if($zipIo){$zipIo.Dispose()}
}

moveFilesToZip $parentDirectory $compressFilesOlderThanDays $destinationZipFile $recurse
# Deprecated version
# moveFilesToZip.ps1
# This little function is to automate the process of
# a. creating an empty zip file
# b. gathering source files to move
# c. adding those files into zip archive
# d.purge the source files
# To reduce memory consumption, the workflow is sequential so that only one file is processed per iteration
# This comes at the cost of performance efficiency. Optimization has been done by keeping the zip file locked
# during operations to save io cycles of instantiating the zip update command per iteration.

$parentDirectory='C:\temp'
$compressFilesOlderThanDays=1
$destinationZipFile='c:\temp\oldFiles.zip'

function moveFilesToZip($parentDirectory,$filesDaysOlderThan,$zipFile,$allFiles=$false,$recurse=$false,$verbose=$true){
    # Fastest way to list all files in a directory
    if($allFiles -and $recurse){
        $getFiles = "cmd.exe /C dir '$parentDirectory' /S /B /W /A:-D"    
        $filesToCompress = Invoke-Expression -Command:$getFiles
    }else{
        $filesToCompress=(gci $parentDirectory|?{!$_.PSIsContainer -and ($_.LastWriteTime -lt (Get-Date).AddDays(-$filesDaysOlderThan))}).FullName
        }

    if(!(test-path $zipFile)){
        #New-Item -Name $zipFile -ItemType File -Force # cannot create file with this extension using command
        $fileIo=[System.IO.File]::Create($zipFile) # invoke dotnet to overcome PShell filetype checking
        $fileIo.Close()
        } 
    Add-Type -Assembly 'System.IO.Compression.FileSystem' #including .NET 4.5 assembly
    $compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
    $zipIo = [System.IO.Compression.ZipFile]::Open($zipFile, 'update')
    foreach ($file in $filesToCompress){
        #$fileFullPath=$file.FullName
        #$fileName=$file.Name
        write-host "Moving $file into zip..."
        try{
            # Compress-Archive -Path $filePath -Update -DestinationPath $zipFile -ea Stop # requires PowerShell 5.0
            #if ($file -notin $zipIo.Entries ){
                $null=[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zipIo,$file,$(split-path $file -leaf),$compressionLevel)
                remove-item $file -Force
            #}else{
            #    write-warning "$file already exists within zip file"
            #    }
        }catch{
            Write-Warning $Error[0].exception.message
            }
    }
    $zipIo.Dispose()
}

moveFilesToZip $parentDirectory $compressFilesOlderThanDays $destinationZipFile