2021年6月28日月曜日

WMIでファイルとフォルダの作成・改名を検知して名前に更新日時を入れる

WMIでファイルやフォルダの作成・改名イベントを検出して、対象アイテムの名前に応じて処理を切り替えるサンプルを作成しました。



動機


あるプログラムが起動すると、その時の年月日にもとづく名前のフォルダが作成されます。(例えば2021.06.28)
そのフォルダの中にファイルが次々と追加されていくのですが、ある操作を行うと出力ファイル名に含まれる連番部分がリセットされてしまい、本来上書きするべきでないデータを上書きしてしまいます。
ファイル名に、更新日時の情報を入れれば、連番部分がリセットされたとしても上書きされることは回避できるので、そのようにしました。


WMIでファイルの作成・改名イベントを検知する最もシンプルなサンプル


computer = '.'
drive = 'C:'
path = '\\test\\'
wmi = GetObject("winmgmts:\\\\" + computer + "\\root\\CIMV2")
wql = [
"SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE ",
"Targetinstance ISA 'CIM_DataFile' AND ",
"TargetInstance.Drive = '" + drive + "' AND ",
"TargetInstance.Path = '" + path.replace(/\\/g,'\\\\') + "'"
].join('')
eventSrc = wmi.ExecNotificationQuery(wql)
eventObj = eventSrc.NextEvent()
objItem = eventObj.TargetInstance
path = objItem.Name
WScript.Echo(path) // C:\test\new file.txt
view raw 00_simple.js hosted with ❤ by GitHub
上記サンプルでは「C:\test」フォルダ内で以下のイベントが発生した際にメッセージが表示されます。
  • ファイルの追加
  • ファイルの改名

フォルダの追加・改名を検知したい場合は「CIM_DataFile」の部分を「CIM_Directory」にします。

お世話になったサイト
JScript (WSH) でフォルダ監視 - by edvakf in hatena


3回連続して検出するサンプル


computer = '.'
drive = 'C:'
path = '\\test\\'
wmi = GetObject("winmgmts:\\\\" + computer + "\\root\\CIMV2")
wql = [
"SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE ",
"Targetinstance ISA 'CIM_DataFile' AND ",
"TargetInstance.Drive = '" + drive + "' AND ",
"TargetInstance.Path = '" + path.replace(/\\/g,'\\\\') + "'"
].join('')
eventSrc = wmi.ExecNotificationQuery(wql)
arr = []
i = 0
while(i < 3){
eventObj = eventSrc.NextEvent()
objItem = eventObj.TargetInstance
path = objItem.Name
arr[i++] = path
}
WScript.Echo(arr.join('\n'))
view raw 01_loop.js hosted with ❤ by GitHub
eventSrc.NextEvent()をコールすると、対象イベントが発生するまでプログラムは次のステップに進まないこと(同期的)が上記の実行結果から分かりました。



関数化


複数のファイルやフォルダを監視するためには関数にした方が使い勝手が良いです。
これ以降のサンプルは全角文字を含んでいるためSJIS形式で保存しないとwscript.exeでエラーになります。



フォルダを検知したら、その中のファイルを検知する


WQL実行後のWMIはcloseしたり「wmi = null」のような事をしなくても、次のWQLを実行できることが分かりました。



終了トリガー


プログラム使用中に日付が変わったら、出力先のフォルダ名も変わる(例えば「2021.06.28」から「2021.06.29」になる)のですが、一度eventSrc.NextEvent()をコールすると対象フォルダ内でイベントが発生するまでスクリプトは次のステップに進むことができません。
イベント監視を始める前に、任意のタイミングで終了トリガーが出現してイベント検知されるように予約する仕組みを作りました。



更新日時にリネーム


// このスクリプトはUTF-8で保存すると実行時エラーになってしまう。
// SJISで保存すること。
function echo(str){ WScript.echo(str) }
function wmi(path, isDir){
// path : 監視対象のフォルダのpath
// isDir : フォルダの追加・変更を検知したい場合はtrue。ファイルの場合はfalse
var ma = path.match(/^([A-Z]:)(.+)\\?$/)
if(!ma){return echo('error. この関数はローカルドライブ専用です。')}
var computer = '.'
var drive = ma[1]
var path = ma[2] + '\\'
var タイプ = isDir ? 'Directory' : 'DataFile'
var wmi = GetObject("winmgmts:\\\\" + computer + "\\root\\CIMV2")
var wql = [
"SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE ",
"Targetinstance ISA 'CIM_" + タイプ + "' AND ",
"TargetInstance.Drive = '" + drive + "' AND ",
"TargetInstance.Path = '" + path.replace(/\\/g,'\\\\') + "'"
].join('')
var eventSrc = wmi.ExecNotificationQuery(wql)
return eventSrc
}
function イベントを監視(path, isDir){
var eventSrc = wmi(path, isDir)
var eventObj = eventSrc.NextEvent()
var objItem = eventObj.TargetInstance
var path = objItem.Name
return path
}
AXO = function(str){return new ActiveXObject(str)}
終了トリガー = function(){
var path
var 待機秒数
var dat待機開始
var タイプ
var path親 = WScript.ScriptFullName.replace(/\\[^\\]+$/,'\\')
var path遅延操作 = path親 + "遅延操作.js"
var shell = AXO('WScript.Shell')
return {
予約 : function(path対象, 秒数, isDir){
path = path対象 + '\\終了トリガー'
待機秒数 = 秒数
タイプ = isDir
dat待機開始 = new Date()
shell.run('"' + [path遅延操作, path, 秒数, isDir ? 'dir' : 'file', 'create'].join('" "') + '"')
},
キャンセル : function(){
var dat現在 = new Date()
var 経過秒数 = get経過秒数(dat待機開始, dat現在)
if(待機秒数 < 経過秒数){return}
shell.run('"' + [path遅延操作, path, (待機秒数 - 経過秒数 + 1), タイプ ? 'dir' : 'file', 'del'].join('" "') + '"')
},
判定 : function(p){
if(p.toUpperCase() != path.toUpperCase()){return}
ファイルシステム[(タイプ ? 'フォルダ' : 'ファイル')+'削除'](path)
path = null
return true
}
}
}()
ファイルシステム = function(){
var fs = AXO('Scripting.FileSystemObject')
return {
ファイル削除 : function(path){
fs.DeleteFile(path)
},
フォルダ削除 : function(path){
fs.DeleteFolder(path)
},
ファイル名変更 : function(path, ファイル名){
var pathBase = path.replace(/[^\\\/]+$/,'') + ファイル名
var num = 1
var pathAfter = pathBase
while(fs.FileExists(pathAfter)){
pathAfter = pathBase.replace(/(\.[^.]+$)/,'-'+num+'$1')
num++
}
fs.MoveFile(path, pathAfter)
return pathAfter
},
get最終更新日時 : function(path){
return fs.GetFile(path).DateLastModified
}
}
}()
function get経過秒数(datBefore, datAfter){
return Math.floor((datAfter.getTime() - datBefore.getTime()) / 1000)
}
function ファイル名に更新日時を入れる(path){
var ファイル名before = path.match(/[^\\\/]+$/)[0]
if(ファイル名before.match(/^\d{8} \d{6}_/)){ return path } // リネーム済みファイルは無視
var dat更新 = new Date(ファイルシステム.get最終更新日時(path))
// 更新直後だと書き込み中かもしれないので少し待機する
var dat現在 = new Date()
var 経過秒数 = get経過秒数(dat更新, dat現在)
if(経過秒数 < 5){ WScript.Sleep(1000 * (5 - Math.floor(経過秒数))) }
var ファイル名 = get日時(dat更新).replace(/[\/:]/g,'') + '_' + path.match(/[\\\/]([^\\\/]+)$/)[1]
var pathAfter = ファイルシステム.ファイル名変更(path, ファイル名)
return pathAfter
}
function 桁(num, 桁数){
var str0=num+'', str1=Array(桁数+1).join(0)+num
return (桁数 < str0.length) ? str0 : str1.slice(str1.length-桁数,str1.length)
}
function get日時(dat){
dat = dat || new Date()
with(dat){ return [getFullYear(), 桁(getMonth()+1,2), 桁(getDate(),2)].join('/') + ' ' + [桁(getHours(),2), 桁(getMinutes(),2), 桁(getSeconds(),2)].join(':') }
}
function main(path){
終了トリガー.予約(path, 10, true)
pathDir = イベントを監視(path, true)
if(終了トリガー.判定(pathDir)){ return echo(path + '\n\n上記Pathでは10秒間イベントはありませんでした。') }
終了トリガー.キャンセル()
終了トリガー.予約(pathDir, 10)
pathFile = イベントを監視(pathDir)
if(終了トリガー.判定(pathFile)){ return echo(pathDir + '\n\n上記Pathでは10秒間イベントはありませんでした。') }
終了トリガー.キャンセル()
var pathAfter = ファイル名に更新日時を入れる(pathFile)
WScript.Echo([
'dir > ' + pathDir,
'file > ' + pathFile,
'ren > ' + pathAfter
].join('\n'))
}
main('C:\\test')
リネーム処理でもファイル変更イベントが発生するので、リネーム済みかどうかを判定しないと無限ループになってしまう点が要注意です。



日付が変わるまでループ


// このスクリプトはUTF-8で保存すると実行時エラーになってしまう。
// SJISで保存すること。
function echo(str){ WScript.echo(str) }
function wmi(path, isDir){
// path : 監視対象のフォルダのpath
// isDir : フォルダの追加・変更を検知したい場合はtrue。ファイルの場合はfalse
var ma = path.match(/^([A-Z]:)(.+)\\?$/)
if(!ma){return echo('error. この関数はローカルドライブ専用です。')}
var computer = '.'
var drive = ma[1]
var path = ma[2] + '\\'
var タイプ = isDir ? 'Directory' : 'DataFile'
var wmi = GetObject("winmgmts:\\\\" + computer + "\\root\\CIMV2")
var wql = [
"SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE ",
"Targetinstance ISA 'CIM_" + タイプ + "' AND ",
"TargetInstance.Drive = '" + drive + "' AND ",
"TargetInstance.Path = '" + path.replace(/\\/g,'\\\\') + "'"
].join('')
var eventSrc = wmi.ExecNotificationQuery(wql)
return eventSrc
}
function イベントを監視(path, isDir, func){
var eventSrc = wmi(path, isDir)
while(true){
var eventObj = eventSrc.NextEvent()
var objItem = eventObj.TargetInstance
var path = objItem.Name
if(func(path)){return}
}
}
AXO = function(str){return new ActiveXObject(str)}
終了トリガー = function(){
var path
var 待機秒数
var dat待機開始
var タイプ
var path親 = WScript.ScriptFullName.replace(/\\[^\\]+$/,'\\')
var path遅延操作 = path親 + "遅延操作.js"
var shell = AXO('WScript.Shell')
return {
予約 : function(path対象, 秒数, isDir){
path = path対象 + '\\終了トリガー'
待機秒数 = 秒数
タイプ = isDir
dat待機開始 = new Date()
shell.run('"' + [path遅延操作, path, 秒数, isDir ? 'dir' : 'file', 'create'].join('" "') + '"')
},
キャンセル : function(){
var dat現在 = new Date()
var 経過秒数 = get経過秒数(dat待機開始, dat現在)
if(待機秒数 < 経過秒数){return}
shell.run('"' + [path遅延操作, path, (待機秒数 - 経過秒数 + 1), タイプ ? 'dir' : 'file', 'del'].join('" "') + '"')
},
判定 : function(p){
if(p.toUpperCase() != path.toUpperCase()){return}
ファイルシステム[(タイプ ? 'フォルダ' : 'ファイル')+'削除'](path)
path = null
return true
}
}
}()
ファイルシステム = function(){
var fs = AXO('Scripting.FileSystemObject')
return {
ファイル削除 : function(path){
fs.DeleteFile(path)
},
フォルダ削除 : function(path){
fs.DeleteFolder(path)
},
ファイル名変更 : function(path, ファイル名){
var pathBase = path.replace(/[^\\\/]+$/,'') + ファイル名
var num = 1
var pathAfter = pathBase
while(fs.FileExists(pathAfter)){
pathAfter = pathBase.replace(/(\.[^.]+$)/,'-'+num+'$1')
num++
}
fs.MoveFile(path, pathAfter)
return pathAfter
},
get最終更新日時 : function(path){
return fs.GetFile(path).DateLastModified
}
}
}()
function get経過秒数(datBefore, datAfter){
return Math.floor((datAfter.getTime() - datBefore.getTime()) / 1000)
}
function ファイル名に更新日時を入れる(path){
var ファイル名before = path.match(/[^\\\/]+$/)[0]
if(ファイル名before.match(/^\d{8} \d{6}_/)){ return path } // リネーム済みファイルは無視
var dat更新 = new Date(ファイルシステム.get最終更新日時(path))
// 更新直後だと書き込み中かもしれないので少し待機する
var dat現在 = new Date()
var 経過秒数 = get経過秒数(dat更新, dat現在)
if(経過秒数 < 5){ WScript.Sleep(1000 * (5 - Math.floor(経過秒数))) }
var ファイル名 = get日時(dat更新).replace(/[\/:]/g,'') + '_' + path.match(/[\\\/]([^\\\/]+)$/)[1]
var pathAfter = ファイルシステム.ファイル名変更(path, ファイル名)
return pathAfter
}
function 桁(num, 桁数){
var str0=num+'', str1=Array(桁数+1).join(0)+num
return (桁数 < str0.length) ? str0 : str1.slice(str1.length-桁数,str1.length)
}
function get日時(dat){
dat = dat || new Date()
with(dat){ return [getFullYear(), 桁(getMonth()+1,2), 桁(getDate(),2)].join('/') + ' ' + [桁(getHours(),2), 桁(getMinutes(),2), 桁(getSeconds(),2)].join(':') }
}
function loopDir(path){
echo('dir > ' + path)
var 今日の年月日 = get日時().replace(/\//g,'.').replace(/([\d.]+) .+/,'$1')
var フォルダ名 = path.match(/[^\\\/]+$/)[0]
if(フォルダ名.indexOf(今日の年月日)!=0){ return } // loop継続 > 次のdir作成・変更イベントを待つ
終了トリガー.予約(path, get日付が変わるまでの秒数(), false)
イベントを監視(path, false, loopFile)
// loopFileのループが完了したらloopDirのループが再開する
}
function loopFile(path){
echo('file > ' + path)
if(終了トリガー.判定(path)){ return true } // loop中断
ファイル名に更新日時を入れる(path)
}
get日付が変わるまでの秒数 = function(){
with(new Date()){ var 日付が変わるまでの秒数 = ((24 - getHours()) * 60 - getMinutes()) * 60}
return 日付が変わるまでの秒数
}
イベントを監視('C:\\test', true, loopDir)
view raw 06_loop.js hosted with ❤ by GitHub
フォルダとファイルのイベントを監視するループを追加しました。
ファイルのイベントを監視するループが継続している間はフォルダを監視するループは保留状態になります。(ファイル監視ループが終了するとフォルダ監視ループが再開します)



イベント発生前のチェック


今までのサンプルでは、サンプルプログラムが起動した後に発生したイベントのみを扱っていました。
しかし実際にはサンプルプログラムが起動する前に対象フォルダが作成されていたり、そのフォルダ内に対象ファイルが出来上がっている場合もあります。
イベントの監視を始める前に既存のフォルダとファイルをチェックする仕組みを追加しました。



script起動時の引数を使用


// このスクリプトはUTF-8で保存すると実行時エラーになってしまう。
// SJISで保存すること。
function echo(str){ WScript.echo(str) }
function wmi(path, isDir){
// path : 監視対象のフォルダのpath
// isDir : フォルダの追加・変更を検知したい場合はtrue。ファイルの場合はfalse
var ma = path.match(/^([A-Z]:)(.+)\\?$/)
if(!ma){return echo('error. この関数はローカルドライブ専用です。')}
var computer = '.'
var drive = ma[1]
var path = ma[2] + '\\'
var タイプ = isDir ? 'Directory' : 'DataFile'
var wmi = GetObject("winmgmts:\\\\" + computer + "\\root\\CIMV2")
var wql = [
"SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE ",
"Targetinstance ISA 'CIM_" + タイプ + "' AND ",
"TargetInstance.Drive = '" + drive + "' AND ",
"TargetInstance.Path = '" + path.replace(/\\/g,'\\\\') + "'"
].join('')
var eventSrc = wmi.ExecNotificationQuery(wql)
return eventSrc
}
function イベントを監視(path, isDir, func){
var eventSrc = wmi(path, isDir)
while(true){
var eventObj = eventSrc.NextEvent()
var objItem = eventObj.TargetInstance
var path = objItem.Name
if(func(path)){return}
}
}
AXO = function(str){return new ActiveXObject(str)}
終了トリガー = function(){
var path
var 待機秒数
var dat待機開始
var タイプ
var path親 = WScript.ScriptFullName.replace(/\\[^\\]+$/,'\\')
var path遅延操作 = path親 + "遅延操作.js"
return {
予約 : function(path対象, 秒数, isDir){
path = path対象 + '\\終了トリガー'
待機秒数 = 秒数
タイプ = isDir
dat待機開始 = new Date()
shell.run('"' + [path遅延操作, path, 秒数, isDir ? 'dir' : 'file', 'create'].join('" "') + '"')
},
キャンセル : function(){
var dat現在 = new Date()
var 経過秒数 = get経過秒数(dat待機開始, dat現在)
if(待機秒数 < 経過秒数){return}
shell.run('"' + [path遅延操作, path, (待機秒数 - 経過秒数 + 1), タイプ ? 'dir' : 'file', 'del'].join('" "') + '"')
},
判定 : function(p){
if(p.toUpperCase() != (path || '').toUpperCase()){return}
ファイルシステム[(タイプ ? 'フォルダ' : 'ファイル')+'削除'](path)
path = null
return true
}
}
}()
ファイルシステム = function(){
var fs = AXO('Scripting.FileSystemObject')
return {
ファイル削除 : function(path){
fs.DeleteFile(path)
},
フォルダ削除 : function(path){
fs.DeleteFolder(path)
},
ファイル名変更 : function(path, ファイル名){
var pathBase = path.replace(/[^\\\/]+$/,'') + ファイル名
var num = 1
var pathAfter = pathBase
while(fs.FileExists(pathAfter)){
pathAfter = pathBase.replace(/(\.[^.]+$)/,'-'+num+'$1')
num++
}
fs.MoveFile(path, pathAfter)
return pathAfter
},
get最終更新日時 : function(path){
return fs.GetFile(path).DateLastModified
}
}
}()
function get経過秒数(datBefore, datAfter){
return Math.floor((datAfter.getTime() - datBefore.getTime()) / 1000)
}
function ファイル名に更新日時を入れる(path){
var ファイル名before = path.match(/[^\\\/]+$/)[0]
if(ファイル名before.match(/^\d{8} \d{6}_/)){ return path } // リネーム済みファイルは無視
var dat更新 = new Date(ファイルシステム.get最終更新日時(path))
// 更新直後だと書き込み中かもしれないので少し待機する
var dat現在 = new Date()
var 経過秒数 = get経過秒数(dat更新, dat現在)
if(経過秒数 < 5){ WScript.Sleep(1000 * (5 - Math.floor(経過秒数))) }
var ファイル名 = get日時(dat更新).replace(/[\/:]/g,'') + '_' + path.match(/[\\\/]([^\\\/]+)$/)[1]
var pathAfter = ファイルシステム.ファイル名変更(path, ファイル名)
return pathAfter
}
function 桁(num, 桁数){
var str0=num+'', str1=Array(桁数+1).join(0)+num
return (桁数 < str0.length) ? str0 : str1.slice(str1.length-桁数,str1.length)
}
function get日時(dat){
dat = dat || new Date()
with(dat){ return [getFullYear(), 桁(getMonth()+1,2), 桁(getDate(),2)].join('/') + ' ' + [桁(getHours(),2), 桁(getMinutes(),2), 桁(getSeconds(),2)].join(':') }
}
function loopDir(path){
echo('dir > ' + path)
var 今日の年月日 = get日時().replace(/\//g,'.').replace(/([\d.]+) .+/,'$1')
var フォルダ名 = path.match(/[^\\\/]+$/)[0]
if(フォルダ名.indexOf(今日の年月日)!=0){ return } // loop継続 > 次のdir作成・変更イベントを待つ
var arrFile = shell.getDir(path)
for0L(arrFile, function(i, ファイル名){ loopFile(path + '\\' + ファイル名) })
終了トリガー.予約(path, get日付が変わるまでの秒数(), false)
イベントを監視(path, false, loopFile)
// loopFileのループが完了したらloopDirのループが再開する
}
function loopFile(path){
echo('file > ' + path)
if(終了トリガー.判定(path)){ return true } // loop中断
ファイル名に更新日時を入れる(path)
}
get日付が変わるまでの秒数 = function(){
with(new Date()){ var 日付が変わるまでの秒数 = ((24 - getHours()) * 60 - getMinutes()) * 60}
return 日付が変わるまでの秒数
}
shell = function(){
var shell = AXO('WScript.Shell')
return {
getDir:function(path, swDir, swSub){
var arr = shell.exec('cmd /C dir /A'+(swDir?'':'-')+'D /B'+(swSub?' /S':'')+' "'+path.replace(/[/]/g,'\\')+'"').StdOut.ReadAll().split('\r\n').reverse()
!arr[0] && arr.shift()
return arr.reverse()
},
run : function(cmd){ shell.run(cmd) }
}
}()
function for0L(arr, func){
for(var i=0,L=arr.length; i<L ;i++){
var res = func(i, arr[i])
if(res){return res}
}
}
function main(){
var arg = WScript.Arguments
if(arg.length==0){ return echo('対象フォルダをD&Dしてください') }
var path = arg(0)
var arrDir = shell.getDir(path, true)
for0L(arrDir, function(i, フォルダ名){ loopDir(path + '\\' + フォルダ名) })
イベントを監視(path, true, loopDir)
}
main()
監視するフォルダのpathを、script起動時の引数から取得できるように変更して、完成です。



スモールステップは、大事


今回のサンプルの作成を開始する前の段階では、正直なところ1~2時間くらいで完成できると思っていました。
しかし実際には10時間くらいかかってしまいました。
想定よりも時間がかかってしまった要因の比率として大きいのは単体テストを省略したことでした。
単体テスト(ユニットテスト)とは | ソフトウェアの検証の種類 | テクマトリックス株式会社


具体的には以下のような点で躓きました。
  • wmiはcloseしたり「wmi = null」をしなくても次のWQLが実行できる、という事をテストしていませんでした。本当はプログラムにミスがあった(ファイルイベントを監視したいのに引数を間違えてフォルダイベントを監視していた)のに、勘違いして「closeしたりnullの代入が必要なのかも?」と考えて時間を無駄にしました。
  • fs.GetFile(path).DateLastModifiedの返り値は文字列ではないのでreplaceメソッドが無い。しかしDateオブジェクトでもない。という事だけを確認する小さなサンプルを作って試すべきだったが、ファイルイベントの監視プログラムの中で確認したために関係ない部分の動作にかかった時間を無駄にしました。

勘違いした事によって「プロセスを分けて標準出力を使って通信する方法」を模索したりした結果、標準出力の使用時の注意事項を再認識したりもしたので「勘違いが解けるまでの時間は全て無駄だった」とも言い切れないのですが、心理的には減らせるところは減らしたい感じです。

このような、サンプル作成過程も、同じような事で悩む人にとっては有益な情報になり得るかと思い、投稿してみました。



0 件のコメント:

コメントを投稿