├── LICENSE ├── README.md └── Sources ├── WatchFolder.ahk └── WatchFolder_sample.ahk /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WatchFolder 2 | -------------------------------------------------------------------------------- /Sources/WatchFolder.ahk: -------------------------------------------------------------------------------- 1 | ; ================================================================================================================================== 2 | ; Function: Notifies about changes within folders. 3 | ; This is a rewrite of HotKeyIt's WatchDirectory() released at 4 | ; http://www.autohotkey.com/board/topic/60125-ahk-lv2-watchdirectory-report-directory-changes/ 5 | ; Tested with: AHK 1.1.23.01 (A32/U32/U64) 6 | ; Tested on: Win 10 Pro x64 7 | ; Usage: WatchFolder(Folder, UserFunc[, SubTree := False[, Watch := 3]]) 8 | ; Parameters: 9 | ; Folder - The full qualified path of the folder to be watched. 10 | ; Pass the string "**PAUSE" and set UserFunc to either True or False to pause respectively resume watching. 11 | ; Pass the string "**END" and an arbitrary value in UserFunc to completely stop watching anytime. 12 | ; If not, it will be done internally on exit. 13 | ; UserFunc - The name of a user-defined function to call on changes. The function must accept at least two parameters: 14 | ; 1: The path of the affected folder. The final backslash is not included even if it is a drive's root 15 | ; directory (e.g. C:). 16 | ; 2: An array of change notifications containing the following keys: 17 | ; Action: One of the integer values specified as FILE_ACTION_... (see below). 18 | ; In case of renaming Action is set to FILE_ACTION_RENAMED (4). 19 | ; Name: The full path of the changed file or folder. 20 | ; OldName: The previous path in case of renaming, otherwise not used. 21 | ; IsDir: True if Name is a directory; otherwise False. In case of Action 2 (removed) IsDir is always False. 22 | ; Pass the string "**DEL" to remove the directory from the list of watched folders. 23 | ; SubTree - Set to true if you want the whole subtree to be watched (i.e. the contents of all sub-folders). 24 | ; Default: False - sub-folders aren't watched. 25 | ; Watch - The kind of changes to watch for. This can be one or any combination of the FILE_NOTIFY_CHANGES_... 26 | ; values specified below. 27 | ; Default: 0x03 - FILE_NOTIFY_CHANGE_FILE_NAME + FILE_NOTIFY_CHANGE_DIR_NAME 28 | ; Return values: 29 | ; Returns True on success; otherwise False. 30 | ; Change history: 31 | ; 1.0.03.00/2021-10-14/just me - bug-fix for addding, removing, or updating folders. 32 | ; 1.0.02.00/2016-11-30/just me - bug-fix for closing handles with the '**END' option. 33 | ; 1.0.01.00/2016-03-14/just me - bug-fix for multiple folders 34 | ; 1.0.00.00/2015-06-21/just me - initial release 35 | ; License: 36 | ; The Unlicense -> http://unlicense.org/ 37 | ; Remarks: 38 | ; Due to the limits of the API function WaitForMultipleObjects() you cannot watch more than MAXIMUM_WAIT_OBJECTS (64) 39 | ; folders simultaneously. 40 | ; MSDN: 41 | ; ReadDirectoryChangesW msdn.microsoft.com/en-us/library/aa365465(v=vs.85).aspx 42 | ; FILE_NOTIFY_CHANGE_FILE_NAME = 1 (0x00000001) : Notify about renaming, creating, or deleting a file. 43 | ; FILE_NOTIFY_CHANGE_DIR_NAME = 2 (0x00000002) : Notify about creating or deleting a directory. 44 | ; FILE_NOTIFY_CHANGE_ATTRIBUTES = 4 (0x00000004) : Notify about attribute changes. 45 | ; FILE_NOTIFY_CHANGE_SIZE = 8 (0x00000008) : Notify about any file-size change. 46 | ; FILE_NOTIFY_CHANGE_LAST_WRITE = 16 (0x00000010) : Notify about any change to the last write-time of files. 47 | ; FILE_NOTIFY_CHANGE_LAST_ACCESS = 32 (0x00000020) : Notify about any change to the last access time of files. 48 | ; FILE_NOTIFY_CHANGE_CREATION = 64 (0x00000040) : Notify about any change to the creation time of files. 49 | ; FILE_NOTIFY_CHANGE_SECURITY = 256 (0x00000100) : Notify about any security-descriptor change. 50 | ; FILE_NOTIFY_INFORMATION msdn.microsoft.com/en-us/library/aa364391(v=vs.85).aspx 51 | ; FILE_ACTION_ADDED = 1 (0x00000001) : The file was added to the directory. 52 | ; FILE_ACTION_REMOVED = 2 (0x00000002) : The file was removed from the directory. 53 | ; FILE_ACTION_MODIFIED = 3 (0x00000003) : The file was modified. 54 | ; FILE_ACTION_RENAMED = 4 (0x00000004) : The file was renamed (not defined by Microsoft). 55 | ; FILE_ACTION_RENAMED_OLD_NAME = 4 (0x00000004) : The file was renamed and this is the old name. 56 | ; FILE_ACTION_RENAMED_NEW_NAME = 5 (0x00000005) : The file was renamed and this is the new name. 57 | ; GetOverlappedResult msdn.microsoft.com/en-us/library/ms683209(v=vs.85).aspx 58 | ; CreateFile msdn.microsoft.com/en-us/library/aa363858(v=vs.85).aspx 59 | ; FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 60 | ; FILE_FLAG_OVERLAPPED = 0x40000000 61 | ; ================================================================================================================================== 62 | WatchFolder(Folder, UserFunc, SubTree := False, Watch := 0x03) { 63 | Static DummyObject := {Base: {__Delete: Func("WatchFolder").Bind("**END", "")}} 64 | Static TimerID := "**" . A_TickCount 65 | Static TimerFunc := Func("WatchFolder").Bind(TimerID, "") 66 | Static MAXIMUM_WAIT_OBJECTS := 64 67 | Static MAX_DIR_PATH := 260 - 12 + 1 68 | Static SizeOfLongPath := MAX_DIR_PATH << !!A_IsUnicode 69 | Static SizeOfFNI := 0xFFFF ; size of the FILE_NOTIFY_INFORMATION structure buffer (64 KB) 70 | Static SizeOfOVL := 32 ; size of the OVERLAPPED structure (64-bit) 71 | Static WatchedFolders := {} 72 | Static EventArray := [] 73 | Static WaitObjects := 0 74 | Static BytesRead := 0 75 | Static Paused := False 76 | ; =============================================================================================================================== 77 | If (Folder = "") 78 | Return False 79 | SetTimer, % TimerFunc, Off 80 | RebuildWaitObjects := False 81 | ; =============================================================================================================================== 82 | If (Folder = TimerID) { ; called by timer 83 | If (ObjCount := EventArray.Count()) && !Paused { 84 | ObjIndex := DllCall("WaitForMultipleObjects", "UInt", ObjCount, "Ptr", &WaitObjects, "Int", 0, "UInt", 0, "UInt") 85 | While (ObjIndex >= 0) && (ObjIndex < ObjCount) { 86 | Event := NumGet(WaitObjects, ObjIndex * A_PtrSize, "UPtr") 87 | Folder := EventArray[Event] 88 | If DllCall("GetOverlappedResult", "Ptr", Folder.Handle, "Ptr", Folder.OVLAddr, "UIntP", BytesRead, "Int", True) { 89 | Changes := [] 90 | FNIAddr := Folder.FNIAddr 91 | FNIMax := FNIAddr + BytesRead 92 | OffSet := 0 93 | PrevIndex := 0 94 | PrevAction := 0 95 | PrevName := "" 96 | Loop { 97 | FNIAddr += Offset 98 | OffSet := NumGet(FNIAddr + 0, "UInt") 99 | Action := NumGet(FNIAddr + 4, "UInt") 100 | Length := NumGet(FNIAddr + 8, "UInt") // 2 101 | Name := Folder.Name . "\" . StrGet(FNIAddr + 12, Length, "UTF-16") 102 | IsDir := InStr(FileExist(Name), "D") ? 1 : 0 103 | If (Name = PrevName) { 104 | If (Action = PrevAction) 105 | Continue 106 | If (Action = 1) && (PrevAction = 2) { 107 | PrevAction := Action 108 | Changes.RemoveAt(PrevIndex--) 109 | Continue 110 | } 111 | } 112 | If (Action = 4) 113 | PrevIndex := Changes.Push({Action: Action, OldName: Name, IsDir: 0}) 114 | Else If (Action = 5) && (PrevAction = 4) { 115 | Changes[PrevIndex, "Name"] := Name 116 | Changes[PrevIndex, "IsDir"] := IsDir 117 | } 118 | Else 119 | PrevIndex := Changes.Push({Action: Action, Name: Name, IsDir: IsDir}) 120 | PrevAction := Action 121 | PrevName := Name 122 | } Until (Offset = 0) || ((FNIAddr + Offset) > FNIMax) 123 | If (Changes.Length() > 0) 124 | Folder.Func.Call(Folder.Name, Changes) 125 | DllCall("ResetEvent", "Ptr", Event) 126 | DllCall("ReadDirectoryChangesW", "Ptr", Folder.Handle, "Ptr", Folder.FNIAddr, "UInt", SizeOfFNI 127 | , "Int", Folder.SubTree, "UInt", Folder.Watch, "UInt", 0 128 | , "Ptr", Folder.OVLAddr, "Ptr", 0) 129 | } 130 | ObjIndex := DllCall("WaitForMultipleObjects", "UInt", ObjCount, "Ptr", &WaitObjects, "Int", 0, "UInt", 0, "UInt") 131 | Sleep, 0 132 | } 133 | } 134 | } 135 | ; =============================================================================================================================== 136 | Else If (Folder = "**PAUSE") { ; called to pause/resume watching 137 | Paused := !!UserFunc 138 | RebuildObjects := Paused 139 | } 140 | ; =============================================================================================================================== 141 | Else If (Folder = "**END") { ; called to stop watching 142 | For Event, Folder In EventArray { 143 | DllCall("CloseHandle", "Ptr", Folder.Handle) 144 | DllCall("CloseHandle", "Ptr", Event) 145 | } 146 | WatchedFolders := {} 147 | EventArray := [] 148 | Paused := False 149 | Return True 150 | } 151 | ; =============================================================================================================================== 152 | Else { ; called to add, update, or remove folders 153 | Folder := RTrim(Folder, "\") 154 | VarSetCapacity(LongPath, MAX_DIR_PATH << !!A_IsUnicode, 0) 155 | If !DllCall("GetLongPathName", "Str", Folder, "Ptr", &LongPath, "UInt", MAX_DIR_PATH) 156 | Return False 157 | VarSetCapacity(LongPath, -1) 158 | Folder := LongPath 159 | If (WatchedFolders.HasKey(Folder)) { ; update or remove 160 | Event := WatchedFolders[Folder] 161 | FolderObj := EventArray[Event] 162 | DllCall("CloseHandle", "Ptr", FolderObj.Handle) 163 | DllCall("CloseHandle", "Ptr", Event) 164 | EventArray.Delete(Event) 165 | WatchedFolders.Delete(Folder) 166 | RebuildWaitObjects := True 167 | } 168 | If InStr(FileExist(Folder), "D") && (UserFunc <> "**DEL") && (EventArray.Count() < MAXIMUM_WAIT_OBJECTS) { 169 | If (IsFunc(UserFunc) && (UserFunc := Func(UserFunc)) && (UserFunc.MinParams >= 2)) && (Watch &= 0x017F) { 170 | Handle := DllCall("CreateFile", "Str", Folder . "\", "UInt", 0x01, "UInt", 0x07, "Ptr",0, "UInt", 0x03 171 | , "UInt", 0x42000000, "Ptr", 0, "UPtr") 172 | If (Handle > 0) { 173 | Event := DllCall("CreateEvent", "Ptr", 0, "Int", 1, "Int", 0, "Ptr", 0) 174 | FolderObj := {Name: Folder, Func: UserFunc, Handle: Handle, SubTree: !!SubTree, Watch: Watch} 175 | FolderObj.SetCapacity("FNIBuff", SizeOfFNI) 176 | FNIAddr := FolderObj.GetAddress("FNIBuff") 177 | DllCall("RtlZeroMemory", "Ptr", FNIAddr, "Ptr", SizeOfFNI) 178 | FolderObj["FNIAddr"] := FNIAddr 179 | FolderObj.SetCapacity("OVLBuff", SizeOfOVL) 180 | OVLAddr := FolderObj.GetAddress("OVLBuff") 181 | DllCall("RtlZeroMemory", "Ptr", OVLAddr, "Ptr", SizeOfOVL) 182 | NumPut(Event, OVLAddr + 8, A_PtrSize * 2, "Ptr") 183 | FolderObj["OVLAddr"] := OVLAddr 184 | DllCall("ReadDirectoryChangesW", "Ptr", Handle, "Ptr", FNIAddr, "UInt", SizeOfFNI, "Int", SubTree 185 | , "UInt", Watch, "UInt", 0, "Ptr", OVLAddr, "Ptr", 0) 186 | EventArray[Event] := FolderObj 187 | WatchedFolders[Folder] := Event 188 | RebuildWaitObjects := True 189 | } 190 | } 191 | } 192 | If (RebuildWaitObjects) { 193 | VarSetCapacity(WaitObjects, MAXIMUM_WAIT_OBJECTS * A_PtrSize, 0) 194 | OffSet := &WaitObjects 195 | For Event In EventArray 196 | Offset := NumPut(Event, Offset + 0, 0, "Ptr") 197 | } 198 | } 199 | ; =============================================================================================================================== 200 | If (EventArray.Count() > 0) 201 | SetTimer, % TimerFunc, -100 202 | Return (RebuildWaitObjects) ; returns True on success, otherwise False 203 | } -------------------------------------------------------------------------------- /Sources/WatchFolder_sample.ahk: -------------------------------------------------------------------------------- 1 | #NoEnv 2 | #Warn 3 | #Include WatchFolder.ahk 4 | SetBatchLines, -1 5 | ; ---------------------------------------------------------------------------------------------------------------------------------- 6 | Gui, Margin, 20, 20 7 | Gui, Add, Text, , Watch Folder: 8 | Gui, Add, Edit, xm y+3 w730 vWatchedFolder cGray +ReadOnly, Select a folder ... 9 | Gui, Add, Button, x+m yp w50 hp +Default vSelect gSelectFolder, ... 10 | Gui, Add, Text, xm y+5, Watch Changes: 11 | Gui, Add, Checkbox, xm y+3 vSubTree, In Sub-Tree 12 | Gui, Add, Checkbox, x+5 yp vFiles Checked, Files 13 | Gui, Add, Checkbox, x+5 yp vFolders Checked, Folders 14 | Gui, Add, Checkbox, x+5 yp vAttr, Attributes 15 | Gui, Add, Checkbox, x+5 yp vSize, Size 16 | Gui, Add, Checkbox, x+5 yp vWrite, Last Write 17 | Gui, Add, Checkbox, x+5 yp vAccess, Last Access 18 | Gui, Add, Checkbox, x+5 yp vCreation, Creation 19 | Gui, Add, Checkbox, x+5 yp vSecurity, Security 20 | Gui, Add, ListView, xm w800 r15 vLV, TickCount|Folder|Action|Name|IsDir|OldName|%A_Space% 21 | Gui, Add, Button, xm w100 gStartStop vAction +Disabled, Start 22 | Gui, Add, Button, x+m yp wp gPauseResume vPause +Disabled, Pause 23 | Gui, Add, Button, x+m yp wp gCLear, Clear 24 | Gui, Show, , Watch Folder 25 | GuiControl, Focus, Select 26 | Return 27 | ; ---------------------------------------------------------------------------------------------------------------------------------- 28 | GuiClose: 29 | ExitApp 30 | ; ---------------------------------------------------------------------------------------------------------------------------------- 31 | Clear: 32 | LV_Delete() 33 | Return 34 | ; ---------------------------------------------------------------------------------------------------------------------------------- 35 | PauseResume: 36 | GuiControlGet, Caption, , Pause 37 | If (Caption = "Pause") { 38 | WatchFolder("**PAUSE", True) 39 | GuiControl, Disable, Action 40 | GuiControl, , Pause, Resume 41 | } 42 | ELse { 43 | WatchFolder("**PAUSE", False) 44 | GuiControl, Enable, Action 45 | GuiControl, , Pause, Pause 46 | } 47 | Return 48 | ; ---------------------------------------------------------------------------------------------------------------------------------- 49 | StartStop: 50 | Gui, +OwnDialogs 51 | Gui, Submit, NoHide 52 | If !InStr(FileExist(WatchedFolder), "D") { 53 | MsgBox, 0, Error, "%WatchedFolder%" isn't a valid folder name! 54 | Return 55 | } 56 | GuiControlGet, Caption, , Action 57 | If (Caption = "Start") { 58 | Watch := 0 59 | Watch |= Files ? 1 : 0 60 | Watch |= Folders ? 2 : 0 61 | Watch |= Attr ? 4 : 0 62 | Watch |= Size ? 8 : 0 63 | Watch |= Write ? 16 : 0 64 | Watch |= Access ? 32 : 0 65 | Watch |= Creation ? 64 : 0 66 | Watch |= Security ? 256 : 0 67 | If (Watch = 0) { 68 | GuiControl, , Files, 1 69 | GuiControl, , Folders, 1 70 | Watch := 3 71 | } 72 | If !WatchFolder(WatchedFolder, "MyUserFunc", SubTree, Watch) { 73 | MsgBox, 0, Error, Call of WatchFolder() failed! 74 | Return 75 | } 76 | GuiControl, , Action, Stop 77 | GuiControl, Disable, Select 78 | GuiControl, Enable, Pause 79 | } 80 | Else { 81 | WatchFolder(WatchedFolder, "**DEL") 82 | GuiControl, , Action, Start 83 | GuiControl, Enable, Select 84 | GuiControl, Disable, Pause 85 | } 86 | Return 87 | ; ---------------------------------------------------------------------------------------------------------------------------------- 88 | SelectFolder: 89 | FileSelectFolder, WatchedFolder 90 | If !(ErrorLevel) { 91 | GuiControl, +cDefault, WatchedFolder 92 | GuiControl, , WatchedFolder, %WatchedFolder% 93 | GuiControl, Enable, Action 94 | } 95 | Return 96 | ; ---------------------------------------------------------------------------------------------------------------------------------- 97 | MyUserFunc(Folder, Changes) { 98 | Static Actions := ["1 (added)", "2 (removed)", "3 (modified)", "4 (renamed)"] 99 | TickCount := A_TickCount 100 | GuiControl, -ReDraw, LV 101 | For Each, Change In Changes 102 | LV_Modify(LV_Add("", TickCount, Folder, Actions[Change.Action], Change.Name, Change.IsDir, Change.OldName, ""), "Vis") 103 | Loop, % LV_GetCount("Columns") 104 | LV_ModifyCol(A_Index, "AutoHdr") 105 | GuiControl, +Redraw, LV 106 | } --------------------------------------------------------------------------------