﻿;-- TIME_STAMP   2022-09-02 09:56:19   v 0.3

#include <Array.au3>
#include <File.au3>
#include <ProgressConstants.au3>
#include <GuiStatusBar.au3>
#include <StaticConstants.au3>
#include <WinAPIError.au3>
#include <WinAPIFiles.au3>

Opt('TrayAutoPause', 0)
OnAutoItExitRegister('_Exit')

; additional flag constants
Global Const $FC_LOGCOPY = 16   ; creates a file "TARGET\FOLDER\progresscopy.log", overwrites a possibly existing old log file
Global Const $FC_OPENLOG = 32   ; ! sets automatically the $FC_LOGCOPY flag ! - shows the logfile (if created) after the operation is finished

Global $hProgressProc = Null, $hGui, $lbPercent, $lbSpeed, $lbFile, $lbRestFiles, $lbRestSize, $lbRestTime, $hStatus, $idProgress
Global $iSumFileSize, $iTotalFileCount
Global $TransferSum = 0, $BytesTransLast = 0
Global $Transfered_halfSecond = 0
Global $TimerStart, $Timer05, $Timer1
Global $hLog = Null



; #FUNCTION# =======================================================================================
; Name ..........: _FileCopyProgress
; Description ...: Copies folders/files with graphical display
; Parameter(s)...: $_sSourcePath         The file(s)/folder to copy. Wildcards allowed.
; ...............: $_sDestinationFolder  The destination folder
; ....[optional].: $_vFlag               Copy flags, known from "FileCopy" (Default: $FC_NOOVERWRITE)
; ...............:                       NEW flags:
; ...............:                       $FC_LOGCOPY, creates a file "TARGET\FOLDER\progresscopy.log", overwrites a possibly existing old log file
; ...............:                       $FC_OPENLOG, ! sets automatically the $FC_LOGCOPY flag ! - shows the logfile (if created) after the operation is finished
; ....[optional].: $_sAttribExclude      Excludes files from copying by attribute (Default: 'SH')
; Return values .: Success               Value > 0
; ...............: Failure               0, set @error  1 - destination folder doesn't exist
; ...............:                                      2 - source file has exclude attribute
; ...............:                                      3 - source path includes none files
; Author ........: BugFix
; Remarks .......:
; ==================================================================================================
Func _FileCopyProgress($_sSourcePath, $_sDestinationFolder, $_vFlag=0, $_sAttribExclude='SH')
    Local $aExclude = StringSplit($_sAttribExclude, '', 2)

    If BitAND($_vFlag, $FC_OPENLOG) Then
        If Not BitAND($_vFlag, $FC_LOGCOPY) Then $_vFlag = BitOR($_vFlag,$FC_LOGCOPY)
    EndIf

    ; Zielordner?
    If Not FileExists($_sDestinationFolder) Then
        If BitAND($_vFlag, $FC_CREATEPATH) Then
            DirCreate($_sDestinationFolder)
        Else
            Return SetError(1,0,0)
        EndIf
    EndIf

    ; Log?
    If BitAND($_vFlag, $FC_LOGCOPY) Then
        If FileExists($_sDestinationFolder & '\progresscopy.log') Then FileDelete($_sDestinationFolder & '\progresscopy.log')
        $hLog = FileOpen($_sDestinationFolder & '\progresscopy.log', BitOR($FO_UTF8_NOBOM, $FO_APPEND))
        _Log($hLog, -1, StringFormat('[%4d-%02d-%02d %02d:%02d:%02d] Start Copy "%s" --> "%s"', @YEAR, @MON, @MDAY, @HOUR, @MIN, @SEC, $_sSourcePath, $_sDestinationFolder))
    EndIf

    ; Zusammenstellen Datei(en) für Copy
    Local $aFiles[2] = [1], $sAttrib = FileGetAttrib($_sSourcePath)
    If ($sAttrib <> '') And (Not StringInStr($sAttrib, 'D')) Then   ; is file or wildcard
        Local $posBS = StringInStr($_sSourcePath, '\', 0, -1)
        Local $sFileOrWildCard = StringTrimLeft($_sSourcePath, $posBS)
        If StringInStr($sFileOrWildCard, '*') Or StringInStr($sFileOrWildCard, '?') Then
            Local $sRoot = StringLeft($_sSourcePath, $posBS-1)
            $aFiles = _FileListToArray($sRoot, $sFileOrWildCard, $FLTA_FILES, True)
        Else
            For $i = 0 To UBound($aExclude) -1
                If StringInStr($sAttrib, $aExclude[$i]) Then Return SetError(2,0,0)
            Next
            $aFiles[1] = $_sSourcePath
        EndIf
    ElseIf ($sAttrib <> '') And (StringInStr($sAttrib, 'D')) Then   ; is directory
        $aFiles = _FileListToArray($_sSourcePath, '*', $FLTA_FILES, True)
    EndIf
    If $aFiles[0] = 0 Then Return SetError(3,0,0)

    ; Überprüfung und Entfernen Dateien mit Exclude-Attrib und Bestimmen Summe Dateigröße
    $iSumFileSize = 0
    Local $bDel
    For $i = $aFiles[0] To 1 Step -1
        $bDel = False
        For $j = 0 To UBound($aExclude) -1
            If StringInStr(FileGetAttrib($aFiles[$i]), $aExclude[$j]) Then
                _ArrayDelete($aFiles, $i)
                $aFiles[0] -= 1
                $bDel = True
                ExitLoop
            EndIf
        Next
        If Not $bDel Then $iSumFileSize += FileGetSize($aFiles[$i])
    Next
    $iTotalFileCount = $aFiles[0]
    If $iTotalFileCount = 0 Then Return SetError(3,0,0)

    If BitAND($_vFlag, $FC_LOGCOPY) Then
        _Log($hLog, -1, StringFormat('Anzahl Dateien: %d%sGesamtgröße: %s', $iTotalFileCount, @CRLF, _FormatByte($iSumFileSize, '', False, '1')))
    EndIf

    ; Erstellen GUI
    _CreateCopyGui()
    GUISetState(@SW_SHOW, $hGui)

    ; callback für progress
    ; https://docs.microsoft.com/en-us/windows/win32/api/winbase/nc-winbase-lpprogress_routine
    $hProgressProc = DllCallbackRegister('_ProgressProc', 'bool', 'uint64;uint64;uint64;uint64;dword;dword;handle;handle;ptr')

    ; Starte Kopieren
    GUICtrlSetData($lbRestSize, _FormatByte($iSumFileSize, '', False, '1'))
    GUICtrlSetData($lbRestFiles, $aFiles[0])
    $TimerStart = TimerInit()
    $Timer05 = TimerInit()
    $Timer1 = TimerInit()
    Local $sFileName
    For $i = 1 To $aFiles[0]
        $sFileName = StringTrimLeft($aFiles[$i], StringInStr($aFiles[$i], '\', 0, -1))
        GUICtrlSetData($lbFile, $sFileName)
        GUICtrlSetData($lbRestFiles, $aFiles[0] -($i-1))
        $BytesTransLast = 0
        If Not BitAND($_vFlag, $FC_OVERWRITE) Then
            If FileExists($_sDestinationFolder & '\' & $sFileName) Then
                If BitAND($_vFlag, $FC_LOGCOPY) Then
                    _Log($hLog, 0, $_sDestinationFolder & '\' & $sFileName, 'exists')
                EndIf
                ; Korrektur Anzeige
                $iSumFileSize -= FileGetSize($aFiles[$i])
                GUICtrlSetData($lbRestSize, _FormatByte(($iSumFileSize - $TransferSum), '', False, '1'))
                Local $iPercentTotal = Round($TransferSum / $iSumFileSize * 100)
                GUICtrlSetData($idProgress, $iPercentTotal)
                GUICtrlSetData($lbPercent, $iPercentTotal)
                Local $iTimeRemaining = (100 - $iPercentTotal) * ((TimerDiff($TimerStart) / 1000) / $iPercentTotal)
                GUICtrlSetData($lbRestTime, _FormatSeconds($iTimeRemaining))
                ContinueLoop
            EndIf
        EndIf
        If _Copy_w_Progress($aFiles[$i], $_sDestinationFolder & '\' & $sFileName, $_vFlag) Then
            If BitAND($_vFlag, $FC_LOGCOPY) Then
                _Log($hLog, 1, $_sDestinationFolder & '\' & $sFileName, _FormatByte(FileGetSize($aFiles[$i]) , '', False, '1'))
            EndIf
        EndIf
    Next
    If BitAND($_vFlag, $FC_LOGCOPY) Then
        _Log($hLog, -1, StringFormat('[%4d-%02d-%02d %02d:%02d:%02d] End Copy', @YEAR, @MON, @MDAY, @HOUR, @MIN, @SEC))
        FileClose($hLog)
        $hLog = Null
    EndIf
    GUICtrlSetData($lbFile, '')
    GUICtrlSetData($lbPercent, 100)
    GUICtrlSetData($lbSpeed, 0)
    GUICtrlSetData($lbRestFiles, 0)
    GUICtrlSetData($lbRestSize, 0)
    GUICtrlSetData($lbRestTime, 0)
    GUICtrlSetData($idProgress, 100)

    While True
        Switch GUIGetMsg()
            Case -3
                GUIDelete($hGui)
                If BitAND($_vFlag, $FC_OPENLOG) Then
                    Return ShellExecute($_sDestinationFolder & '\progresscopy.log')
                Else
                    Return 1
                EndIf
        EndSwitch
    WEnd
EndFunc


Func _Log($_hFile, $_iSucc=-1, $_sLeft='', $_sRight='')
    Local $sSucc = $_iSucc = 0 ? '[ERROR]   ' : ($_iSucc = -1 ? '' : '[SUCCESS] ')
    If $_sRight <> '' Then $_sLeft = StringFormat('%-70s', $_sLeft)
    If $_sRight = 'exists' Then $_sRight = 'Die Datei ist bereits vorhanden.'
    Return FileWrite($_hFile, $sSucc & $_sLeft & $_sRight & @CRLF)
EndFunc


Func _CreateCopyGui()
    $hGui = GUICreate('Copy with Progress', 300, 170)
    GUISetBkColor(0xF0F8FF)
    GUISetFont(10, 400, Default, 'Courier New')

    ; Prozent
    GUICtrlCreateLabel('Fortschritt:', 10, 10, 115, 17)
    $lbPercent = GUICtrlCreateLabel('0', 220, 10, 60, 17, BitOR($GUI_SS_DEFAULT_LABEL, $SS_RIGHT))
    GUICtrlSetColor(-1, 0x000080)
    GUICtrlCreateLabel('%', 280, 10, 10, 17, BitOR($GUI_SS_DEFAULT_LABEL, $SS_RIGHT))
    GUICtrlSetColor(-1, 0x000080)

    ; Speed
    GUICtrlCreateLabel('Geschwindigkeit:', 10, 32, 125, 17)
    $lbSpeed = GUICtrlCreateLabel('0 MB', 200, 32, 70, 17, BitOR($GUI_SS_DEFAULT_LABEL, $SS_RIGHT))
    GUICtrlSetColor(-1, 0x000080)
    GUICtrlCreateLabel('/s', 270, 32, 20, 17, BitOR($GUI_SS_DEFAULT_LABEL, $SS_RIGHT))
    GUICtrlSetColor(-1, 0x000080)

    ; akt. Datei
    GUICtrlCreateLabel('Name:', 10, 54, 40, 17)
    $lbFile = GUICtrlCreateLabel('', 55, 54, 235, 17, BitOR($GUI_SS_DEFAULT_LABEL, $SS_RIGHT))
    GUICtrlSetColor(-1, 0x000080)

    ; verbleibende Elemente
    GUICtrlCreateLabel('Verbleibende Elemente:', 10, 76, 180, 17)
    $lbRestFiles = GUICtrlCreateLabel('', 230, 76, 60, 17, BitOR($GUI_SS_DEFAULT_LABEL, $SS_RIGHT))
    GUICtrlSetColor(-1, 0x000080)

    ; verbleibende Größe
    GUICtrlCreateLabel('Verbleibende Größe:', 10, 98, 180, 17)
    $lbRestSize = GUICtrlCreateLabel('', 230, 98, 60, 17, BitOR($GUI_SS_DEFAULT_LABEL, $SS_RIGHT))
    GUICtrlSetColor(-1, 0x000080)

    ; verbleibende Zeit
    GUICtrlCreateLabel('Restdauer ungefähr:', 10, 120, 180, 17)
    $lbRestTime = GUICtrlCreateLabel('', 200, 120, 90, 17, BitOR($GUI_SS_DEFAULT_LABEL, $SS_RIGHT))
    GUICtrlSetColor(-1, 0x000080)

    ; Statusbar
    $hStatus = _GUICtrlStatusBar_Create($hGui)
    _GUICtrlStatusBar_SetMinHeight($hStatus, 20)
    $idProgress = GUICtrlCreateProgress(0, 0, -1, -1, $PBS_SMOOTH)
    GUICtrlSetColor(-1, 0x008B00) ; grün
    _GUICtrlStatusBar_EmbedControl($hStatus, 0, GUICtrlGetHandle($idProgress))
EndFunc


Func _Exit()
	If $hProgressProc <> Null Then DllCallbackFree($hProgressProc)
    If $hLog <> Null Then FileClose($hLog)
EndFunc


Func _Copy_w_Progress($_source, $_destination, $_vFlag)
    Local $flag = $COPY_FILE_FAIL_IF_EXISTS
    If BitAND($_vFlag, $FC_OVERWRITE) Then $flag = 0
	If Not _WinAPI_CopyFileEx($_source, $_destination, $flag, DllCallbackGetPtr($hProgressProc)) Then
        If BitAND($_vFlag, $FC_LOGCOPY) Then
            _Log($hLog, 0, $_source, _WinAPI_GetLastErrorMessage())
        Else
            _WinAPI_ShowLastError('Beim Kopieren der Datei "' & $_source & '" ist ein Fehler aufgetreten.')
        EndIf
        Return False
	EndIf
    Return True
EndFunc


Volatile Func _ProgressProc($iTotalFileSize, $iTotalBytesTransferred, $iStreamSize, $iStreamBytesTransferred, $iStreamNumber, $iCallbackReason, $hSourceFile, $hDestinationFile, $pData)
	#forceref $iStreamSize, $iStreamBytesTransferred, $iStreamNumber, $iCallbackReason, $hSourceFile, $hDestinationFile, $pData
	_ProgressSetCopyData($iTotalBytesTransferred)
	Sleep(10)
	Return $PROGRESS_CONTINUE
EndFunc


Func _ProgressSetCopyData($_BytesTrans=0)
    $TransferDiff = $_BytesTrans - $BytesTransLast
    $TransferSum += $TransferDiff
    $BytesTransLast = $_BytesTrans
    $Transfered_halfSecond += $TransferDiff

    If TimerDiff($Timer05) < 500 Then Return
    Local $iPercentTotal = Round($TransferSum / $iSumFileSize * 100)

    If TimerDiff($Timer05) >= 500 Then ; alle 1/2 Sekunde aktualisieren
        GUICtrlSetData($idProgress, $iPercentTotal)
        GUICtrlSetData($lbSpeed, _FormatByte($Transfered_halfSecond * 2, '', False, '1')) ; *2: Angabe pro Sekunde
        $Transfered_halfSecond = 0
        $Timer05 = TimerInit()
    EndIf

    ; andere Daten nur jede Sekunde aktualisieren
    If TimerDiff($Timer1) < 1000 Then Return
    GUICtrlSetData($lbPercent, $iPercentTotal)
    GUICtrlSetData($lbRestSize, _FormatByte($iSumFileSize - $TransferSum, '', False, '1'))
    ; Restzeit: Zeit für bisher kopierte Datenmenge prozentual auf Restdatenmenge anrechnen
    Local $iTimeTotal = TimerDiff($TimerStart) / 1000
    Local $iTimeRemaining = (100 - $iPercentTotal) * ($iTimeTotal / $iPercentTotal)
    GUICtrlSetData($lbRestTime, _FormatSeconds($iTimeRemaining))
    $Timer1 = TimerInit()
EndFunc


; #FUNCTION# ====================================================================================================================
; Name ..........: _FormatSeconds
; Description ...: Returns a given value of seconds in the format
; ...............:        <24h: "hh:mm:ss", >=24h: "x d / hh:mm:ss h"
; Syntax ........: _FormatSeconds($_sec)
; Parameters ....: $_sec                - The number of seconds.
; Return values .: The formatted string.
; Author ........: BugFix
; ===============================================================================================================================
Func _FormatSeconds($_sec)
    Return ( $_sec < 60 ? StringFormat('00:00:%02u', $_sec) : _
             $_sec < 60*60 ? StringFormat('00:%02u', Floor($_sec/60)) & ':' & _
                    StringFormat('%02u', Mod($_sec,60)) : _
             $_sec < 60*60*24 ? StringFormat('%02u', Floor($_sec/3600)) & ':' & _
                    StringFormat('%02u', Floor(Mod($_sec,3600)/60)) & ':' & _
                    StringFormat('%02u', Mod(Mod($_sec,3600),60)) : _
             ( $_sec = 86400 ? "24:00:00" : Floor($_sec/86400) & ' d / ' & _
                    StringFormat('%02u', Floor(Mod($_sec,86400)/3600)) & ':' & _
                    StringFormat('%02u', Floor(Mod(Mod($_sec,86400),3600)/60)) & ':' & _
                    StringFormat('%02u', Mod(Mod(Mod($_sec,86400),3600),60)) & ' h') )
EndFunc  ;==>_FormatSeconds


; #FUNCTION# ====================================================================================================================
; Name ..........: _FormatByte
; Description ...: Formats a given value of bytes with highest or given unit, optional as structure with all units
; Parameters ....: $_iByte    The value of bytes to format
; ...............: $_sUnit    (Default = '', unit of highest value) or count of given unit (TB, GB, MB, KB, Byte)
; ...............: $_bStruct  Returns a structure with .TB .GB .MB .KB .Byte (Default = False)
; ...............: $_sDigit   Number of decimal digits (Default = '3') as string!
; Return values .: The formatted string or the structure.
; Author ........: BugFix
; ===============================================================================================================================
Func _FormatByte($_iByte, $_sUnit='', $_bStruct=False, $_sDigit='3')
    Local Static $aByte[5][2] = [[0x10000000000],[0x40000000],[0x100000],[0x400],[0x1]]
    Local Static $tBytes = DllStructCreate('int TB;int GB;int MB;int KB;int Byte;')
    Local Static $aUnit[5] = ['TB','GB','MB','KB','Byte']
    Local $iModulo = $_iByte, $iHighest = 4
    For $i = 0 To 3
        $aByte[$i][1] = $iModulo >= $aByte[$i][0] ? Floor($iModulo/$aByte[$i][0]) : 0
        $iModulo = $aByte[$i][1] > 0 ? Mod($iModulo,$aByte[$i][0]) : $iModulo
        $iHighest = $aByte[$i][1] > 0 ? ($i < $iHighest ? $i : $iHighest) : $iHighest
    Next
    $aByte[4][1] = $iModulo
    If $_bStruct Then
        $tBytes.TB   = $aByte[0][1]
        $tBytes.GB   = $aByte[1][1]
        $tBytes.MB   = $aByte[2][1]
        $tBytes.KB   = $aByte[3][1]
        $tBytes.Byte = $aByte[4][1]
        Return $tBytes
    EndIf
    $_sUnit = StringInStr('TB GB MB KB Byte', $_sUnit) ? $_sUnit : ''
    $_sUnit = $_sUnit = '' ? $aUnit[$iHighest] : $_sUnit
    Local $iUserUnit = Floor(StringInStr('TB GB MB KB Byte', $_sUnit)/3)
    If Number($_sDigit) < 0 Then $_sDigit = '0'
    Local $sFormat = '%.' & $_sDigit & 'f %s'
    Return StringFormat($sFormat, $_iByte/$aByte[$iUserUnit][0], $aUnit[$iUserUnit])
EndFunc  ;==>_FormatByte


