Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

[AHK_L/v2] WatchDirectory - Report Directory Changes


  • Please log in to reply
158 replies to this topic
HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008

This functionality already exist in Crazy Scripting : FolderSpy v0.96 [ Synchronous ] and WatchDirectory()
Again many thanks to SKAN and Lexikos wink.png

Note icon_exclaim.gif You will need latest AutoHotkey_L or AutoHotkey_H to use these functions.

Also latest _Struct() class is required.

It's now better, faster, more accurate and has got great filtering options. Enjoy wink.png

 

Download (for AutoHotkey_L++)

AHKv2 version is available here

All you need is a function in your script that accepts 2 parameters

- When a file is changed, both parameters will contain same filename/path
- When a file is deleted, it is passed in first parameter and second parameter will be empty
- When a new file is created, first parameter is empty and second contains filename/path
- When a file is renamed, first parameter contains old filename/path and second new filename/path

WatchDirectory(dir[\*|...],func,watchfor) accepts up to 3 parameters
- dir, can be a folder/directory (last backslash is optional)
- - include * in the end of your directory to watch in sub folders
- - after that you can include file name filters separated by pipe '|', here you have following options:
- - - use * and ? wildcard
- - - include Back Slash in front = match beginning of filename
- - - include Back Slash at the end = match end of filename
- - - include Back Slash anywhere in the middle to search in complete file path
- - - E.g. following will search for files that start with a and end with .txt or .ini

WatchDirectory("C:\temp\*|.txt\|.ini\|\a","ReportChanges")

- func, a function to be launched when changes in corresponding folder occur
- - when you omit func, last used function will be used
- Your function must accept 2 parameters
- - When new file is created:
- - - first parameter will be empty
- - - second parameter will contain the filepath
- - When file is deleted
- - - second parameter will be empty
- - - first parameter will contain the filepath
- - When file is modified both parameters contain same filepath
- - When file is renamed
- - - first parameter will contain old filename
- - - second parameter will contain new filename
- watchfor can be used to report only specific changes
- - therefore include one or several of following values i
- - FILE_NOTIFY_CHANGE_FILE_NAME=0x1
- - FILE_NOTIFY_CHANGE_DIR_NAME=0x2
- - FILE_NOTIFY_CHANGE_ATTRIBUTES=0x4
- - FILE_NOTIFY_CHANGE_SIZE=0x8
- - FILE_NOTIFY_CHANGE_LAST_WRITE=0x10
- - FILE_NOTIFY_CHANGE_CREATION=0x40
- - FILE_NOTIFY_CHANGE_SECURITY=0x100
- - these values must be separated by |, by default all changes are reported.

WatchDirectory("C:\Temp","ReportFunction",0x1|0x2|0x8|0x40)

To stop watching Directory, at least when script exits WatchDirectory needs to be called with empty parameter

WatchDirectory("")

To stop watching a specific Directory, include "" for second parameter. E.g.

WatchDirectory("C:\Temp","")

 

Example and WatchDirectory() Function

#Persistent
SetBatchLines,-1
SetWinDelay,-1
OnExit, GuiClose

WatchFolders=C:\Temp*|%A_Temp%*|%A_Desktop%|%A_DesktopCommon%|%A_MyDocuments%*|%A_ScriptDir%|%A_WinDir%*
Gui,+Resize
Gui,Add,ListView,r10 w800 vWatchingDirectoriesList HWNDhList1 gShow,WatchingDirectories|WatchingSubdirs
Loop,Parse,WatchFolders,|
WatchDirectory(A_LoopField,"ReportChanges")
,LV_Add("",SubStr(A_LoopField,0)="*" ? (SubStr(A_LoopField,1,StrLen(A_LoopField)-1)) : A_LoopField
,SubStr(A_LoopField,0)="*" ? 1 : 0)
LV_ModifyCol(1,"AutoHdr")
Gui,Add,ListView,r30 w800 vChangesList HWNDhList2 gShow,Time|FileChangedFrom - Double click to show in Explorer|FileChangedTo - Double click to show in Explorer
Gui,Add,Button,gAdd Default,Watch new directory
Gui,Add,Button,gDelete x+1,Stop watching all directories
Gui,Add,Button,gClear x+1,Clear List
Gui,Add,StatusBar,,Changes Registered
Gui, Show

Return

Clear:
Gui,ListView, ChangesList
LV_Delete()
Return

Delete:
WatchDirectory("")
Gui,ListView, WatchingDirectoriesList
LV_Delete()
Gui,ListView, ChangesList
TotalChanges:=0
SB_SetText("Changes Registered")
Return

Show:
If A_GuiEvent!=DoubleClick
Return
Gui,ListView,%A_GuiControl%
LV_GetText(file,A_EventInfo,2)
If file=
LV_GetText(file,A_EventInfo,3)
Run,% "explorer.exe /e`, /n`, /select`," . file
Return

Add:
Gui,+OwnDialogs
dir=
FileSelectFolder,dir,,3,Select directory to watch for
If !dir
Return
SetTimer,SetMsgBoxButtons,-10
MsgBox, 262146,Add directory,Would you like to watch for changes in:`n%dir%

Gui,ListView, WatchingDirectoriesList
IfMsgBox Retry
WatchDirectory(dir "*"),LV_Add("",dir,1)
IfMsgBox Ignore
WatchDirectory(dir),LV_Add("",dir,0)
LV_ModifyCol(1,"AutoHdr")
Gui,ListView, ChangesList
Return

SetMsgBoxButtons:
WinWait, ahk_class #32770
WinActivate
WinWaitActive
ControlSetText,Button2,&Incl. subdirs, ahk_class #32770
ControlSetText,Button3,&Excl. subdirs, ahk_class #32770
Return

ReportChanges(from,to){
global TotalChanges
Gui,ListView, ChangesList
LV_Insert(1,"",A_Hour ":" A_Min ":" A_Sec ":" A_MSec,from,to)
LV_ModifyCol()
LV_ModifyCol(1,"AutoHdr"),LV_ModifyCol(2,"AutoHdr")
TotalChanges++
SB_SetText("Changes Registered " . TotalChanges)
}

GuiClose:
WatchDirectory("") ;Stop Watching Directory = delete all directories
ExitApp
END:
return

#include <_Struct>
WatchDirectory(p*){
;Structures
static FILE_NOTIFY_INFORMATION:="DWORD NextEntryOffset,DWORD Action,DWORD FileNameLength,WCHAR FileName[1]"
static OVERLAPPED:="ULONG_PTR Internal,ULONG_PTR InternalHigh,{struct{DWORD offset,DWORD offsetHigh},PVOID Pointer},HANDLE hEvent"
;Variables
static running,sizeof_FNI=65536,temp1:=VarSetCapacity(nReadLen,8),WatchDirectory:=RegisterCallback("WatchDirectory","F",0,0)
static timer,ReportToFunction,LP,temp2:=VarSetCapacity(LP,(260)*(A_PtrSize/2),0)
static @:=Object(),reconnect:=Object(),#:=Object(),DirEvents,StringToRegEx="\\\|.\.|+\+|[\[|{\{|(\(|)\)|^\^|$\$|?\.?|*.*"
;ReadDirectoryChanges related
static FILE_NOTIFY_CHANGE_FILE_NAME=0x1,FILE_NOTIFY_CHANGE_DIR_NAME=0x2,FILE_NOTIFY_CHANGE_ATTRIBUTES=0x4
,FILE_NOTIFY_CHANGE_SIZE=0x8,FILE_NOTIFY_CHANGE_LAST_WRITE=0x10,FILE_NOTIFY_CHANGE_CREATION=0x40
,FILE_NOTIFY_CHANGE_SECURITY=0x100
static FILE_ACTION_ADDED=1,FILE_ACTION_REMOVED=2,FILE_ACTION_MODIFIED=3
,FILE_ACTION_RENAMED_OLD_NAME=4,FILE_ACTION_RENAMED_NEW_NAME=5
static OPEN_EXISTING=3,FILE_FLAG_BACKUP_SEMANTICS=0x2000000,FILE_FLAG_OVERLAPPED=0x40000000
,FILE_SHARE_DELETE=4,FILE_SHARE_WRITE=2,FILE_SHARE_READ=1,FILE_LIST_DIRECTORY=1
If p.MaxIndex(){
If (p.MaxIndex()=1 && p.1=""){
for i,folder in #
DllCall("CloseHandle","Uint",@[folder].hD),DllCall("CloseHandle","Uint",@[folder].O.hEvent)
,@.Remove(folder)
#:=Object()
DirEvents:=new _Struct("HANDLE[1000]")
DllCall("KillTimer","Uint",0,"Uint",timer)
timer=
Return 0
} else {
if p.2
ReportToFunction:=p.2
If !IsFunc(ReportToFunction)
Return -1 ;DllCall("MessageBox","Uint",0,"Str","Function " ReportToFunction " does not exist","Str","Error Missing Function","UInt",0)
RegExMatch(p.1,"^([^/\*\?<>\|""]+)(\*)?(\|.+)?$",dir)
if (SubStr(dir1,0)="\")
StringTrimRight,dir1,dir1,1
StringTrimLeft,dir3,dir3,1
If (p.MaxIndex()=2 && p.2=""){
for i,folder in #
If (dir1=SubStr(folder,1,StrLen(folder)-1))
Return 0 ,DirEvents[i]:=DirEvents[#.MaxIndex()],DirEvents[#.MaxIndex()]:=0
@.Remove(folder),#[i]:=#[#.MaxIndex()],#.Remove(i)
Return 0
}
}
if !InStr(FileExist(dir1),"D")
Return -1 ;DllCall("MessageBox","Uint",0,"Str","Folder " dir1 " does not exist","Str","Error Missing File","UInt",0)
for i,folder in #
{
If (dir1=SubStr(folder,1,StrLen(folder)-1) || (InStr(dir1,folder) && @[folder].sD))
Return 0
else if (InStr(SubStr(folder,1,StrLen(folder)-1),dir1 "\") && dir2){ ;replace watch
DllCall("CloseHandle","Uint",@[folder].hD),DllCall("CloseHandle","Uint",@[folder].O.hEvent),reset:=i
}
}
LP:=SubStr(LP,1,DllCall("GetLongPathName","Str",dir1,"Uint",&LP,"Uint",VarSetCapacity(LP))) "\"
If !(reset && @[reset]:=LP)
#.Insert(LP)
@[LP,"dir"]:=LP
@[LP].hD:=DllCall("CreateFile","Str",StrLen(LP)=3?SubStr(LP,1,2):LP,"UInt",0x1,"UInt",0x1|0x2|0x4
,"UInt",0,"UInt",0x3,"UInt",0x2000000|0x40000000,"UInt",0)
@[LP].sD:=(dir2=""?0:1)

Loop,Parse,StringToRegEx,|
StringReplace,dir3,dir3,% SubStr(A_LoopField,1,1),% SubStr(A_LoopField,2),A
StringReplace,dir3,dir3,%A_Space%,\s,A
Loop,Parse,dir3,|
{
If A_Index=1
dir3=
pre:=(SubStr(A_LoopField,1,2)="\\"?2:0)
succ:=(SubStr(A_LoopField,-1)="\\"?2:0)
dir3.=(dir3?"|":"") (pre?"\\\K":"")
. SubStr(A_LoopField,1+pre,StrLen(A_LoopField)-pre-succ)
. ((!succ && !InStr(SubStr(A_LoopField,1+pre,StrLen(A_LoopField)-pre-succ),"\"))?"[^\\]*$":"") (succ?"$":"")
}
@[LP].FLT:="i)" dir3
@[LP].FUNC:=ReportToFunction
@[LP].CNG:=(p.3?p.3:(0x1|0x2|0x4|0x8|0x10|0x40|0x100))
If !reset {
@[LP].SetCapacity("pFNI",sizeof_FNI)
@[LP].FNI:=new _Struct(FILE_NOTIFY_INFORMATION,@[LP].GetAddress("pFNI"))
@[LP].O:=new _Struct(OVERLAPPED)
}
@[LP].O.hEvent:=DllCall("CreateEvent","Uint",0,"Int",1,"Int",0,"UInt",0)
If (!DirEvents)
DirEvents:=new _Struct("HANDLE[1000]")
DirEvents[reset?reset:#.MaxIndex()]:=@[LP].O.hEvent
DllCall("ReadDirectoryChangesW","UInt",@[LP].hD,"UInt",@[LP].FNI[""],"UInt",sizeof_FNI
,"Int",@[LP].sD,"UInt",@[LP].CNG,"UInt",0,"UInt",@[LP].O[""],"UInt",0)
Return timer:=DllCall("SetTimer","Uint",0,"UInt",timer,"Uint",50,"UInt",WatchDirectory)
} else {
Sleep, 0
for LP in reconnect
{
If (FileExist(@[LP].dir) && reconnect.Remove(LP)){
DllCall("CloseHandle","Uint",@[LP].hD)
@[LP].hD:=DllCall("CreateFile","Str",StrLen(@[LP].dir)=3?SubStr(@[LP].dir,1,2):@[LP].dir,"UInt",0x1,"UInt",0x1|0x2|0x4
,"UInt",0,"UInt",0x3,"UInt",0x2000000|0x40000000,"UInt",0)
DllCall("ResetEvent","UInt",@[LP].O.hEvent)
DllCall("ReadDirectoryChangesW","UInt",@[LP].hD,"UInt",@[LP].FNI[""],"UInt",sizeof_FNI
,"Int",@[LP].sD,"UInt",@[LP].CNG,"UInt",0,"UInt",@[LP].O[""],"UInt",0)
}
}
if !( (r:=DllCall("MsgWaitForMultipleObjectsEx","UInt",#.MaxIndex()
,"UInt",DirEvents[""],"UInt",0,"UInt",0x4FF,"UInt",6))>=0
&& r<#.MaxIndex() ){
return
}
DllCall("KillTimer", UInt,0, UInt,timer)
LP:=#[r+1],DllCall("GetOverlappedResult","UInt",@[LP].hD,"UInt",@[LP].O[""],"UIntP",nReadLen,"Int",1)
If (A_LastError=64){ ; ERROR_NETNAME_DELETED - The specified network name is no longer available.
If !FileExist(@[LP].dir) ; If folder does not exist add to reconnect routine
reconnect.Insert(LP,LP)
} else
Loop {
FNI:=A_Index>1?(new _Struct(FILE_NOTIFY_INFORMATION,FNI[""]+FNI.NextEntryOffset)):(new _Struct(FILE_NOTIFY_INFORMATION,@[LP].FNI[""]))
If (FNI.Action < 0x6){
FileName:=@[LP].dir . StrGet(FNI.FileName[""],FNI.FileNameLength/2,"UTF-16")
If (FNI.Action=FILE_ACTION_RENAMED_OLD_NAME)
FileFromOptional:=FileName
If (@[LP].FLT="" || RegExMatch(FileName,@[LP].FLT) || FileFrom)
If (FNI.Action=FILE_ACTION_ADDED){
FileTo:=FileName
} else If (FNI.Action=FILE_ACTION_REMOVED){
FileFrom:=FileName
} else If (FNI.Action=FILE_ACTION_MODIFIED){
FileFrom:=FileTo:=FileName
} else If (FNI.Action=FILE_ACTION_RENAMED_OLD_NAME){
FileFrom:=FileName
} else If (FNI.Action=FILE_ACTION_RENAMED_NEW_NAME){
FileTo:=FileName
}
If (FNI.Action != 4 && (FileTo . FileFrom) !="")
@[LP].Func(FileFrom=""?FileFromOptional:FileFrom,FileTo)
,FileFrom:="",FileFromOptional:="",FileTo:=""
}
} Until (!FNI.NextEntryOffset || ((FNI[""]+FNI.NextEntryOffset) > (@[LP].FNI[""]+sizeof_FNI-12)))
DllCall("ResetEvent","UInt",@[LP].O.hEvent)
DllCall("ReadDirectoryChangesW","UInt",@[LP].hD,"UInt",@[LP].FNI[""],"UInt",sizeof_FNI
,"Int",@[LP].sD,"UInt",@[LP].CNG,"UInt",0,"UInt",@[LP].O[""],"UInt",0)
timer:=DllCall("SetTimer","Uint",0,"UInt",timer,"Uint",50,"UInt",WatchDirectory)
Return
}
Return
}

Changes:

10.05.2015

- Always report directory changes
18.07.2012
- Empty variables after calling Reporting function.
- - This lead to incorrect results when for example several files were copied at once, a rename instead of create was reoted.
30.05.2012
- Fix for filters
18.02.2012
- AHKv2 version
05.09.2011
- Fixed OVERLAPED structure and _Struct
31.08.2011
- Updated to use new _Struct class
11.03.2011
- Fixed a bug for StrGet FileName (FNI.FileNameLength/2)
02.03.2011
- Fixed a bug for ANSI AHK_L
08.01.2011
- Fixed a bug in filtering option when * was used.
- Fixed to report a change when a file is renamed and new or old filename do not match the filter.
12.11.2010
- Small bug fixes and a routine to reconnect network shared drives
11.11.2010
- Changed sizeof_FNI back to 65536 because a higher buffer seems to be not valid for network drives



Smurth
  • Members
  • 120 posts
  • Last active: Feb 23 2014 09:58 PM
  • Joined: 13 Dec 2006
Very nice. Thanks for the good work ;)

olfen
  • Members
  • 115 posts
  • Last active: Dec 25 2012 09:48 AM
  • Joined: 04 Jun 2005
Very nice work! Thanks. I am using this script daily.

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Thanks guys, I'm glad you like it ;)

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Fixed a bug in filtering option when * was used.
Fixed to report a change when a file is renamed and new or old filename do not match the filter.

capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010
If a file is modified (with notepad) and I initialised with (as example)

WatchDirectory("C:\temp\*|.txt\|.ini\|\a","ReportChanges")

i.e. for all events

then the ReportChanges function is called twice with exactly the same arguments.

Is there any way to either (1) limit it to 1 call
(2) detect the first call so I can do nothing.

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Simply remember previous parameters :)
WatchDirectory("C:\temp\*|.txt\|.ini\|\a","ReportChanges")

ReportChanges(from,to){

	static last

	If (last=from to)

		Return

	MsgBox % from "`n" to 

	last:=from to

}


capitalH
  • Members
  • 64 posts
  • Last active: Apr 05 2012 05:18 AM
  • Joined: 23 Apr 2010
Thanks, it works but it stops the second instance, not the first.

I want to create a copy of the file with the modification made, but the first trigger copies the file prior to the changes.

Also, if you change the same file twice in succession, it will only trigger the first save.

I used your code and modified it with A_TickCount

Report(from, to,action){
   global
   static last
   static ticks

   If (last=from to)
{
  tmp:=A_TickCount - ticks
  ticks:=A_TickCount
  if tmp<= 100
      Return
}
   MsgBox % from "`n" to
   last:=from to
   ticks:=A_TickCount
}

I used a value of 100 - which seems to work (in my testing I got a value of 62 or 63 in about 50 test cases - although all were local HDD)


A user that is very quick might be able to save twice and hence trigger this once (but unlikely to make changes - this will not be used on computer modified files). I will play around with that value for the network though - 100 might be too quick.

Thanks for your help!

Edit: Forgot to add that I modified your original function to send the action code (FNI.Action) to the report function.

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Here is another attempt :)
Report(from, to){

	static _from:=Object(),_to:=Object(), i

	i:=_to.MaxIndex()

	If !(_from[i]=from && _to[i]=to){

		_from.Insert(from),_to.Insert(to)

		i++

		SetTimer,Report,-30

	}

   Return

	Report:

		MsgBox % _from[i] "`n" _to[i]

		_from.Remove(i),_to.Remove(i)

		If i:=_to.MaxIndex()

			SetTimer,Report,-30

	Return

}


sebastian___
  • Members
  • 23 posts
  • Last active: Oct 06 2015 09:38 PM
  • Joined: 05 Oct 2007
I'm not sure how to install this.

I need to make a script which will monitor a folder. Each time a new picture is saved to that folder, the script should pass the name of the image to a program for further processing. And as far as I know this is possible only with the WatchDirectory() function.

Since I have the old/classic Autohotkey installed first I installed
AutoHotkey_L
But how do I install Struct() and WatchDirectory() ?

I downloaded Struct.ahk and copied in AutoHotkey\lib Is that ok ?
So probably I need to do the same with WatchDirectory.ahk but I couldn't find a final WatchDirectory.ahk

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
It is included in above example, just copy above example, delete all but WatchDirectory(p*)... and save as WatchDirectory.ahk in lib folder ;)

sebastian___
  • Members
  • 23 posts
  • Last active: Oct 06 2015 09:38 PM
  • Joined: 05 Oct 2007
I just did that and then I tried you code sample and Scite editor give this error :

(221) : ==> Call to nonexistent function.
Specifically: Until(!FNI.NextEntryOffset || ((FNI[]+FNI.NextEntryOffset) > (@[LP].FNI[]+sizeof_FNI-12)))

I tried also with the whole code - including the WatchDirectory function - and I get the same error.

I also tried two Autohotkey : AutoHotkey_L Unicode x86 and ANSI x86

I also compiled manually and after running the exe I get this error :

Error at line 87.

The following variable name contains an illegal character:
"p*"

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Try again now. To compile you will need correct AutoHotkeySC.bin in Compile folder.

sebastian___
  • Members
  • 23 posts
  • Last active: Oct 06 2015 09:38 PM
  • Joined: 05 Oct 2007
Thank you. Now it works.

BUt I tried this and I get multiple message box for just a single file saved to that folder

#Persistent 

WatchDirectory("J:\img Convert\rename*","Report")  
Report(test)
{ 
msgbox % test
}
I also tried your last code but I still get two messages

#Persistent 

WatchDirectory("J:\img Convert\rename*","Report")   
Report(from, to){ 
   static _from:=Object(),_to:=Object(), i 
   i:=_to.MaxIndex() 
   If !(_from[i]=from && _to[i]=to){ 
      _from.Insert(from),_to.Insert(to) 
      i++ 
      SetTimer,Report,-30 
   } 
   Return 
   Report: 
      MsgBox % _from[i] "`n" _to[i] 
      _from.Remove(i),_to.Remove(i) 
      If i:=_to.MaxIndex() 
         SetTimer,Report,-30 
   Return 
}
EDIT :
And another problem seems to be - only the first letter of the file saved appears in msgbox. Like this :
J:\img Convert\rename\W
instead of : J:\img Convert\rename\Winter.jpg

EDIT 2 : I switched to the Unicode x86 Autohotkey and now I get the full name. But I still get multiple msgbox

HotKeyIt
  • Moderators
  • 7439 posts
  • Last active: Jun 22 2016 09:14 PM
  • Joined: 18 Jun 2008
Thanks I have fixed ANSI version.
See above post with regards to multiple messages.