The module above is a "wrapper" to easily use ReadDirectoryChangesW (Windows) or Inotify (Linux) to observe changes in a selected Directory.
It will recognize most common operations like:
- file deletion
- file changeing
- file adding
You can of course observe multiple directorys if you create a new "instance".
Note:
You'll need libnotify-dev on Linux in order to compile it.
$ sudo apt-get install libnotify-dev
Example included.
Code: Select all
;Supports x86, x64 - Ascii & unicode.
DeclareModule Storage
Structure File
Name.s
Size.i
Date.i
ID.i
EndStructure
Structure Storage_Settings
Folder.s
Filter.i
AllowedFiles.s
List FileType.s() ; allowed file types (allowedfiles.s parsed) --> Content as example jpg
; will be empty if default search "*.*"
*CallBack
*UserData
Mutex.i
LastID.i
ScanThread.i
EndStructure
Structure Storage
Settings.Storage_Settings
List File.File()
EndStructure
;AllowedFiles.s Example (not case sensitive!):
; "*.*" --> get everything (default)
; "*.jpg" --> only jpg
; "*.png;*.jpg;*.jpeg" --> get jpg, jpeg & png
Declare Init(Folder.s, AllowedFiles.s = "*.*", *DirectoryChangeCallBack = #Null, *UserData = #Null)
Declare ExamineStorage(Handle.i)
Declare NextStorageItem(Handle.i)
Declare LastStorageItem(Handle.i)
Declare GetItemSize(Handle.i,ItemID.i = -1)
Declare.s GetItemFile(Handle.i,ItemID.i = -1)
Declare GetItemID(Handle.i)
Declare GetStorageSize(Handle.i)
EndDeclareModule
Module Storage
;{ - private
Prototype DirectoryChange(Event.s,File.s,StorageID.i,*UserData = 0)
Procedure NewID(*Storage.Storage)
If Not *Storage
Debug "STORAGE!"
ProcedureReturn #False
EndIf
LockMutex(*Storage\Settings\Mutex.i)
*Storage\Settings\LastID.i + 1
Protected LastID.i = *Storage\Settings\LastID.i
UnlockMutex(*Storage\Settings\Mutex.i)
ProcedureReturn LastID.i
EndProcedure
Procedure RemoveFile(*Storage.Storage,File.s)
Debug "trying to remove "+File.s
If Not *Storage
Debug "STORAGE!"
ProcedureReturn #False
EndIf
Protected FilePart.s = GetFilePart(File.s)
LockMutex(*Storage\Settings\Mutex.i)
ForEach *Storage\File()
If *Storage\File()\Name.s = FilePart.s
DeleteElement(*Storage\File())
Protected DeleteDone.b = #True
Break
EndIf
Next
UnlockMutex(*Storage\Settings\Mutex.i)
If DeleteDone.b
Debug "Deleted: "+File.s
EndIf
ProcedureReturn DeleteDone.b
EndProcedure
Procedure AddFile(*Storage.Storage,File.s)
If Not FileSize(File.s) >= 0
ProcedureReturn #False
EndIf
If Not *Storage
ProcedureReturn #False
EndIf
Protected FileExt.s = LCase(GetExtensionPart(File.s))
Protected FileID.i
If ListSize(*Storage\Settings\FileType()) >= 1
ForEach *Storage\Settings\FileType()
If *Storage\Settings\FileType() = FileExt.s
LockMutex(*Storage\Settings\Mutex.i)
If AddElement(*Storage\File())
*Storage\File()\Name.s = GetFilePart(File.s)
*Storage\File()\Size.i = FileSize(File.s)
*Storage\File()\Date.i = GetFileDate(File.s,#PB_Date_Created)
*Storage\File()\ID.i = NewID(*Storage)
FileID.i = *Storage\File()\ID.i
EndIf
UnlockMutex(*Storage\Settings\Mutex.i)
ProcedureReturn FileID.i
EndIf
Next
Else
LockMutex(*Storage\Settings\Mutex.i)
If AddElement(*Storage\File())
*Storage\File()\Name.s = GetFilePart(File.s)
*Storage\File()\Size.i = FileSize(File.s)
*Storage\File()\Date.i = GetFileDate(File.s,#PB_Date_Created)
*Storage\File()\ID.i = NewID(*Storage)
FileID.i = *Storage\File()\ID.i
UnlockMutex(*Storage\Settings\Mutex.i)
ProcedureReturn FileID.i
EndIf
UnlockMutex(*Storage\Settings\Mutex.i)
EndIf
ProcedureReturn #False
EndProcedure
Procedure InitialScan(*Storage.Storage)
Protected Directory.i, File.s
If Not *Storage
ProcedureReturn #False
EndIf
If Not FileSize(*Storage\Settings\Folder.s) -1
ProcedureReturn #False
EndIf
If ListSize(*Storage\Settings\FileType()) = 1
Directory.i = ExamineDirectory(#PB_Any,*Storage\Settings\Folder.s,"*."+*Storage\Settings\FileType())
Else
Directory.i = ExamineDirectory(#PB_Any,*Storage\Settings\Folder.s,"*.*")
EndIf
If Directory.i
While NextDirectoryEntry(Directory.i)
If DirectoryEntryType(Directory.i) = #PB_DirectoryEntry_File
File.s = *Storage\Settings\Folder.s + DirectoryEntryName(Directory.i)
AddFile(*Storage,File.s)
EndIf
Wend
FinishDirectory(Directory.i)
Else
ProcedureReturn #False
EndIf
Debug "Inital Directory scan found "+ListSize(*Storage\File())+" files.."
ProcedureReturn #True
EndProcedure
CompilerIf #PB_Compiler_OS = #PB_OS_Windows
;{ -import stuff
; Notify filter values
#FILE_NOTIFY_CHANGE_FILE_NAME = 1
#FILE_NOTIFY_CHANGE_DIR_NAME = 2
#FILE_NOTIFY_CHANGE_ATTRIBUTES = 4
#FILE_NOTIFY_CHANGE_SIZE = 8
#FILE_NOTIFY_CHANGE_LAST_WRITE = $10
#FILE_NOTIFY_CHANGE_LAST_ACCESS = $20
#FILE_NOTIFY_CHANGE_CREATION = $40
#FILE_NOTIFY_CHANGE_SECURITY = $100
#FILE_NOTIFY_CHANGE_ALL = $17F
; not defined in purebasic
#FILE_SHARE_DELETE = 4
; Notify events
Enumeration
#FILE_ACTION_ADDED = 1
#FILE_ACTION_REMOVED
#FILE_ACTION_MODIFIED
#FILE_ACTION_RENAMED_OLD_NAME
#FILE_ACTION_RENAMED_NEW_NAME
EndEnumeration
; Needed for creating a watching thread
Structure WatchFSParam_t
Directory.s
Filter.l
EndStructure
; structure for the needed
Structure FILE_NOTIFY_INFORMATION
NextEntryOffset.l
Action.l
FileNameLength.l
Filename.s{512}
EndStructure
Import "kernel32.lib"
ReadDirectoryChangesW(hDirectory.l, *lpBuffer, nbBufferLen.l, bWatchSubTree.b, dwNotifyFilter.l, *lpBytesReturned, *lpOverlapped.OVERLAPPED, lpCompletitionRoutine)
EndImport
;} - import stuff
Procedure WatchDirOrFile(*Storage.Storage)
If Not *Storage
ProcedureReturn #False
EndIf
Protected FileID.i
Protected DirectoryChange.DirectoryChange = *Storage\Settings\CallBack
Protected *buffer = AllocateMemory(32*1024)
Protected *ovlp.OVERLAPPED = AllocateMemory(SizeOf(OVERLAPPED))
Protected dwOffset.l = 0
Protected *pInfo.FILE_NOTIFY_INFORMATION
hDir = CreateFile_(*Storage\Settings\Folder.s, #FILE_LIST_DIRECTORY, #FILE_SHARE_READ | #FILE_SHARE_WRITE | #FILE_SHARE_DELETE, #Null, #OPEN_EXISTING, #FILE_FLAG_BACKUP_SEMANTICS, #Null)
While ReadDirectoryChangesW(hDir, *buffer, MemorySize(*buffer), #True, *Storage\Settings\Filter, bytesRead, *ovlp, #Null)
dwOffset = 0
Repeat
FileID.i = 0
*pInfo = *buffer + dwOffset
filename$ = Trim(PeekS(@*pInfo\Filename, *pInfo\FileNameLength, #PB_Unicode))
action$ = #NULL$
Select *pInfo\Action
Case #FILE_ACTION_ADDED
action$ = "ADDED"
FileID.i = AddFile(*Storage, *Storage\Settings\Folder.s + filename$)
Case #FILE_ACTION_MODIFIED
action$ = "MODIFIED"
RemoveFile(*Storage, *Storage\Settings\Folder.s + filename$)
FileID.i = AddFile(*Storage, *Storage\Settings\Folder.s + filename$)
Case #FILE_ACTION_REMOVED
action$ = "REMOVED"
RemoveFile(*Storage, *Storage\Settings\Folder.s + filename$)
FileID.i = -1
Case #FILE_ACTION_RENAMED_NEW_NAME
action$ = "RENAMED_NEWNAME"
FileID.i = AddFile(*Storage, *Storage\Settings\Folder.s + filename$)
Case #FILE_ACTION_RENAMED_OLD_NAME
action$ = "RENAMED_OLDNAME"
RemoveFile(*Storage, *Storage\Settings\Folder.s + filename$)
FileID.i = -1
EndSelect
If *Storage\Settings\CallBack
DirectoryChange(action$,filename$,FileID.i,*Storage\Settings\UserData)
EndIf
dwOffset + *pInfo\NextEntryOffset
Until *pInfo\NextEntryOffset = 0
FillMemory(*buffer, MemorySize(*Buffer))
Wend
EndProcedure
CompilerElseIf #PB_Compiler_OS = #PB_OS_Linux
;you need to install libnotify-dev
; debian/ubuntu based distros
; $ apt-get install libnotify-dev
ImportC "-lnotify"
inotify_init.i (app_name.p-utf8)
inotify_add_watch.i (fd.i, pathname.p-utf8, mask.l)
inotify_rm_watch.i (fd.i, wd.i)
;inotify_uninit()
EndImport
Structure inotify_event
wd.i
mask.i
cookie.i
len.i
*name
EndStructure
#IN_ACCESS = $00000001
#IN_MODIFY = $00000002
#IN_ATTRIB = $00000004
#IN_CLOSE_WRITE = $00000008
#IN_CLOSE_NOWRITE = $00000010
#IN_CLOSE = (#IN_CLOSE_WRITE | #IN_CLOSE_NOWRITE)
#IN_OPEN = $00000020
#IN_MOVED_FROM = $00000040
#IN_MOVED_TO = $00000080
#IN_MOVE = (#IN_MOVED_FROM | #IN_MOVED_TO)
#IN_CREATE = $00000100
#IN_DELETE = $00000200
#IN_DELETE_SELF = $00000400
#IN_MOVE_SELF = $00000800
#IN_UNMOUNT = $00002000
#IN_Q_OVERFLOW = $00004000
#IN_IGNORED = $00008000
#IN_CLOSE = (#IN_CLOSE_WRITE | #IN_CLOSE_NOWRITE)
#IN_MOVE = (#IN_MOVED_FROM | #IN_MOVED_TO)
#IN_ONLYDIR = $01000000
#IN_DONT_FOLLOW = $02000000
#IN_EXCL_UNLINK = $04000000
#IN_MASK_ADD = $20000000
#IN_ISDIR = $40000000
#IN_ONESHOT = $80000000
;you may edit this if you don't want to get notifyed on all events
#IN_ALL_EVENTS = (#IN_ACCESS | #IN_MODIFY | #IN_ATTRIB | #IN_CLOSE_WRITE | #IN_CLOSE_NOWRITE | #IN_OPEN | #IN_MOVED_FROM | #IN_MOVED_TO | #IN_CREATE | #IN_DELETE | #IN_DELETE_SELF | #IN_MOVE_SELF)
;max buff
#INotify_Buff = 16384
Macro isMask(_MASK,_ISMASK)
_MASK & _ISMASK = _ISMASK
EndMacro
Procedure WatchDirOrFile(*Storage.Storage)
If Not *Storage
ProcedureReturn #False
EndIf
Protected DirectoryChange.DirectoryChange = *Storage\Settings\CallBack
Protected inotify.i = inotify_init(#NUL$)
Protected length.i
Protected action$, FileID.i, filename$
Protected *temp.inotify_event
If Not inotify.i
Debug "could not init inotify!"
ProcedureReturn #False
EndIf
Protected *ReadBuff = AllocateMemory(#INotify_Buff)
If Not *ReadBuff
ProcedureReturn #False
EndIf
Protected wd.l = inotify_add_watch(inotify.i, *Storage\Settings\Folder.s, *Storage\Settings\Filter.i)
Repeat
length.i = read_(inotify.i, *ReadBuff, #INotify_Buff)
If (length.i <= 0)
Debug "read error @ "+*Storage\Settings\Folder.s+ "! terminiating!"
FreeMemory(*ReadBuff)
End
EndIf
If length.i > 0
*temp = *ReadBuff
While *temp < *ReadBuff + length
If Not *temp\name > 0
*temp = *temp + SizeOf(inotify_event) + *temp\len
Continue
EndIf
filename$ = PeekS(@*temp\name,-1,#PB_Ascii)
If isMask(*temp\mask,#IN_CREATE) Or isMask(*temp\mask,#IN_MOVED_TO) ;file created
If FileSize(*Storage\Settings\Folder.s + filename$) > 0 Or FileSize(*Storage\Settings\Folder.s + filename$) = -2
action$ = "ADDED"
FileID.i = AddFile(*Storage, *Storage\Settings\Folder.s + filename$)
EndIf
ElseIf isMask(*temp\mask,#IN_ATTRIB) Or isMask(*temp\mask,#IN_MODIFY) ;file modified
If FileSize(*Storage\Settings\Folder.s + filename$) > 0 Or FileSize(*Storage\Settings\Folder.s + filename$) = -2
action$ = "MODIFIED"
RemoveFile(*Storage, *Storage\Settings\Folder.s + filename$)
FileID.i = AddFile(*Storage, *Storage\Settings\Folder.s + filename$)
EndIf
ElseIf isMask(*temp\mask,#IN_DELETE) Or isMask(*temp\mask,#IN_MOVED_FROM) ;file deleted / moved to diff dir
action$ = "REMOVED"
RemoveFile(*Storage, *Storage\Settings\Folder.s + filename$)
FileID.i = -1
Else
*temp = *temp + SizeOf(inotify_event) + *temp\len
Continue
EndIf
If *Storage\Settings\CallBack
DirectoryChange(action$,filename$,FileID.i,*Storage\Settings\UserData)
EndIf
*temp = *temp + SizeOf(inotify_event) + *temp\len
Wend
EndIf
ForEver
EndProcedure
CompilerEndIf
;} - private
Procedure Init(Folder.s, AllowedFiles.s = "*.*", *DirectoryChangeCallBack = #Null, *UserData = #Null)
Protected Counter.i, StringPart.s
If Not FileSize(Folder.s) = -2
ProcedureReturn #False
EndIf
Protected *Storage.Storage = AllocateMemory(SizeOf(Storage))
If Not *Storage
ProcedureReturn #False
EndIf
InitializeStructure(*Storage,Storage)
CompilerIf #PB_Compiler_OS = #PB_OS_Windows
If Right(Folder.s,1) <> "\"
Folder.s + "\"
EndIf
CompilerElse
If Right(Folder.s,1) <> "/"
Folder.s + "/"
EndIf
CompilerEndIf
If AllowedFiles.s = "*.*"
Else
If CountString(AllowedFiles.s,";") = 0
;only one specific search string
If AllowedFiles.s <> "*.*"
If AddElement(*Storage\Settings\FileType())
*Storage\Settings\FileType() = LCase(StringField(AllowedFiles.s,2,"."))
EndIf
EndIf
Else
For Counter.i = 1 To CountString(AllowedFiles.s,";") + 1
StringPart.s = StringField(AllowedFiles.s,Counter.i,";")
If StringPart.s <> "*.*"
If AddElement(*Storage\Settings\FileType())
*Storage\Settings\FileType() = LCase(StringField(StringPart.s,2,"."))
EndIf
EndIf
Next
EndIf
EndIf
Debug "Got "+ListSize(*Storage\Settings\FileType())+" different file type to look for.."
*Storage\Settings\Folder.s = Folder.s
*Storage\Settings\AllowedFiles.s = AllowedFiles.s
*Storage\Settings\CallBack = *DirectoryChangeCallBack
*Storage\Settings\UserData = *UserData
*Storage\Settings\Mutex.i = CreateMutex()
InitialScan(*Storage)
CompilerSelect #PB_Compiler_OS
CompilerCase #PB_OS_Windows
*Storage\Settings\Filter.i = #FILE_NOTIFY_CHANGE_ALL
CompilerCase #PB_OS_Linux
*Storage\Settings\Filter.i = #IN_ALL_EVENTS ;// you may edit the constant to filter a bit more
CompilerEndSelect
*Storage\Settings\ScanThread.i = CreateThread(@WatchDirOrFile(),*Storage)
ProcedureReturn *Storage
EndProcedure
Procedure ExamineStorage(Handle.i)
Protected *Storage.Storage = Handle.i
If Not *Storage
ProcedureReturn #False
EndIf
If ListSize(*Storage\File())
ProcedureReturn ResetList(*Storage\File())
EndIf
EndProcedure
Procedure NextStorageItem(Handle.i)
Protected *Storage.Storage = Handle.i
If Not *Storage
ProcedureReturn #False
EndIf
If ListSize(*Storage\File())
ProcedureReturn NextElement(*Storage\File())
EndIf
EndProcedure
Procedure LastStorageItem(Handle.i)
Protected *Storage.Storage = Handle.i
If Not *Storage
ProcedureReturn #False
EndIf
If ListSize(*Storage\File())
ProcedureReturn LastElement(*Storage\File())
EndIf
EndProcedure
Procedure GetItemSize(Handle.i,ItemID.i = -1)
Protected *Storage.Storage = Handle.i
If Not *Storage
ProcedureReturn #False
EndIf
If ListSize(*Storage\File())
If ItemID.i > -1
Protected *Curr = @*Storage\File()
ForEach *Storage\File()
If *Storage\File()\ID.i = ItemID.i
Protected Size.i = *Storage\File()\Size.i
If *Curr
ChangeCurrentElement(*Storage\File(),*Curr)
EndIf
ProcedureReturn Size.i
EndIf
Next
EndIf
ProcedureReturn *Storage\File()\Size.i
EndIf
EndProcedure
Procedure.s GetItemFile(Handle.i,ItemID.i = -1)
Protected *Storage.Storage = Handle.i
If Not *Storage
ProcedureReturn #NULL$
EndIf
If ListSize(*Storage\File())
If ItemID.i > -1
; Protected *Curr = @*Storage\File()
ForEach *Storage\File()
If *Storage\File()\ID.i = ItemID.i
Protected File.s = *Storage\Settings\Folder.s +*Storage\File()\Name.s
Debug "Asked to provide "+FileID.i+ " is "+File.s
ProcedureReturn File.s
EndIf
Next
EndIf
ProcedureReturn *Storage\Settings\Folder.s + *Storage\File()\Name.s
EndIf
EndProcedure
Procedure GetItemID(Handle.i)
Protected *Storage.Storage = Handle.i
If Not *Storage
ProcedureReturn #False
EndIf
If ListSize(*Storage\File())
ProcedureReturn *Storage\File()\ID.i
EndIf
EndProcedure
Procedure GetStorageSize(Handle.i)
Protected *Storage.Storage = Handle.i
If Not *Storage
ProcedureReturn #False
EndIf
ProcedureReturn ListSize(*Storage\File())
EndProcedure
EndModule
CompilerIf #PB_Compiler_IsMainFile
Procedure DirCallBack(Event.s,File.s,StorageID.i,*UserData)
Debug "Event = "+Event.s + " @ "+File.s + " --> "+StorageID.i
EndProcedure
Define Path.s = PathRequester("Choose path to observe", #NUL$)
If Path.s = ""
End
EndIf
Define Storage.i = Storage::Init(Path.s,"*.*",@DirCallBack())
;Define Storage.i = Storage::Init("C:\Development\XAMPP\htdocs\images","*.jpg;*.png;*.jpeg",@DirCallBack())
If Storage.i
Repeat
If Storage::ExamineStorage(Storage.i)
While Storage::NextStorageItem(Storage.i)
Debug "> "+Storage::GetItemFile(Storage.i) + "(" + Storage::GetItemSize(Storage.i) + ") = "+Storage::GetItemID(Storage.i)
Wend
EndIf
Debug "--------------------------------"
Delay(8000)
ForEver
EndIf
CompilerEndIf
Thank you!
//12/08/14 Fixed issues (Windows) & added linux support ( thanks to infratec & electrochrisso)