2016年7月28日木曜日

タグでファイルやフォルダを管理するソフト

ブログの記事をラベルで分類してみたら、ちょっと便利な気がしました。
ローカルのファイルやフォルダも同じように分類できたら便利かも?と思いました。
タグで分類するEvernoteは一時期(今も?)流行してたと思います。
そういうことができるソフトがあるのでは、と検索してみましたが、既存のソフトにはそれぞれ一長一短あるようです。

管理対象のファイルそのものには何も手を加えずに、ファイルを移動しても対応できるタグ管理ソフトが欲しいと思い、作りました。


[使い方]
まずはプログラムをどこかのフォルダに保存します。
どこでも良いです。
私は「C:\tags」というフォルダを作って、そこに保存しました。



タグを付けたいアイテム(ファイルでもフォルダでもOK)を用意します。
今回はこのアイテムも「C:\tags」というフォルダに入れますが、同じフォルダでなくてもOKです。



タグを付けたいアイテムをwsfファイルにドラッグ&ドロップします。



タグ設定の画面が開きます。



最初は「既存のタグ」が無いので選択肢は表示されません。
テキストエリアに任意のタグをカンマ区切りで入力します。



上の画面で「決定」ボタンを押すと画面が閉じます。
そしてwsfファイルと同じフォルダにタグ関係のフォルダやファイルが追加されます。



ドラッグ&ドロップではなく「tags.wsf」をダブルクリックして起動すると、アイテム検索画面が開きます。



テキストエリアにタグをカンマ区切りで入力するか、既存のタグを選択して「検索」ボタンを押すと、対応するアイテムが表示されます。
※下の画像では「AND検索」と表示されていますが「OR検索」にすると、選択したタグを含むアイテムの全てが表示されます。

「埋め尽くす.hta」というボタンをクリックするとファイルが開きます。


タグとアイテムの紐づけは「名前」だけで行っています。
そのため、タグ設定したアイテムは名前さえ変えなければ違うフォルダに移動しても問題ありません。

例えば、上記のように「C:\tags\埋め尽くす.hta」に「hta」「削除」というタグを設定したあとで

「items」というフォルダを作り



その中に「埋め尽くす.hta」を移動しても



「埋め尽くす.hta」ボタンを押すと、開けます。(サブフォルダ内も検索するからです)



「tags.wsf」とは違うフォルダ(サブフォルダでもない)にアイテムがある場合、アイテム名のボタンをクリックしてもアイテムを開けず、以下のメッセージが表示されます。



異なるフォルダにあるアイテムを開きたい場合は、そのフォルダのPathを「検索」ボタンの下のインプットエリアに入力すると、



以下のようなフォルダ構成でもアイテムが開けます。



以下、ソース
<job>
<script>
// 引数の有無で動作内容を変える。
// 引数がある場合は引数として渡されたファイル(orフォルダ)にタグ付けする動作。
// 引数がない場合はタグ一覧からファイル(orフォルダ)を検索できる画面を表示する動作。
function onload(){
// 共用関数の読み込みまで完了した後にコールされる関数。
arg=WScript.Arguments
arg.length ? fun引数ありモード() : fun引数なしモード()
// 以下はやらなくても関数終了とともにスクリプト終了するけど分かりやすさのため。
WScript.Quit()
}
fun引数ありモード=function(){
// タグの変更対象となるファイル(orフォルダ)を配列化する。
for(var i=0,L=arg.length,arrファイル名=[]; i<L ;i++){
arrファイル名[i] = fs.GetFileName(arg(i))
}
// 他のファイルなどで使用済みのタグがある場合、選択肢として表示する。そのためのHTMLを作成する。
var arrLI=[], arrタグ=全体.arrタグ, obj={}
if(arg.length==1){
var アイテムCD = 全体.objアイテム[fs.GetFileName(arg(0))]
if(isFinite(アイテムCD)){ obj = arr2obj(RAS(path紐づけ_item2tag+'/'+アイテムCD)) }
}
for(var i=0,L=arrタグ.length; i<L ;i++){
arrLI[i] = '<li><label><input type="checkbox"'+(obj[i]!=undefined?' checked':'')+'><span>'+arrタグ[i]+'</span></label>'
}
// HTAのbodyタグ内に入れる文字列を作成する。
var str=[
'タグをカンマ区切りで記入するか、既存のタグから該当するものを選択してください。テキスト入力と選択は併用可能です。',
'<textarea id=ta style="width:100%;height:100px;"></textarea>',
'<ul>' + arrLI.join('\r\n') + '</ul>',
'<br>',
'<button id=btn>決定</button><br><br>',
'<div id=div>' + arrファイル名.join('<br>\r\n') + '</div>'
].join('\r\n')
// HTAのonloadイベントから呼ばれる関数を作成する。
var fun=function(){
resizeTo(600,500)
btn.onclick=function(){
// 入力、選択されたタグを配列化する
var arr=ta.value?ta.value.split(','):[], arrLI=document.getElementsByTagName('LI'), arrタグ_選択=[]
for(var i=0,L=arr.length;i<L;i++){ arrタグ_選択.push(arr[i]) }
for(var i=0,L=arrLI.length;i<L;i++){
if(!arrLI[i][FI][FI].checked){ continue }
arrタグ_選択.push(arrLI[i][FI].childNodes[1][iT])
}
// 新規要素がある場合、ファイルを更新する。
var arrアイテム_対象=div[iT].split('\r\n')
アイテム追加(arrアイテム_対象)
タグ追加(arrタグ_選択)
// タグとアイテムの紐づけを更新する。
紐づけ更新(arrアイテム_対象, arrタグ_選択)
window.close()
}
}
makeHTA(str, fun, 'タグ設定')
}
fun引数なしモード=function(){
// 他のファイルなどで使用済みのタグがある場合、選択肢として表示する。そのためのHTMLを作成する。
var arrLI=[], arrタグ=全体.arrタグ
for(var i=0,L=arrタグ.length; i<L ;i++){
arrLI[i] = '<li><label><input type="checkbox"><span>'+arrタグ[i]+'</span></label>'
}
// HTAのbodyタグ内に入れる文字列を作成する。
var str=[
'タグをカンマ区切りで記入するか、既存のタグから該当するものを選択してください。テキスト入力と選択は併用可能です。',
'<select id=sel></select>',
'<textarea id=ta style="width:100%;height:100px;"></textarea>',
'<ul>' + arrLI.join('\r\n') + '</ul>',
'<br>',
'<button id=btn>検索</button><br><br>',
'<input id=inp style="width:100%;"><br><br>',
'<div id=div></div>'
].join('\r\n')
// HTAのonloadイベントから呼ばれる関数を作成する。
var fun=function(){
resizeTo(600,500)
var ops=sel.options,i=0
ops[i++] = new Option('AND検索 … 選択したタグを全て含むアイテムだけリストアップします')
ops[i++] = new Option('OR検索 … 選択したタグを含むアイテムを全てリストアップします')
btn.onclick=function(){
// 入力、選択されたタグを配列化する
var arr=ta.value?ta.value.split(','):[], arrLI=document.getElementsByTagName('LI'), arrタグ_選択=[]
for(var i=0,L=arr.length;i<L;i++){ arrタグ_選択.push(arr[i]) }
for(var i=0,L=arrLI.length;i<L;i++){
if(!arrLI[i][FI][FI].checked){ continue }
arrタグ_選択.push(arrLI[i][FI].childNodes[1][iT])
}
var swOR検索=sel.selectedIndex, obj対象アイテムCD={}, タグ名, タグCD, arr紐づけ, アイテムCD, アイテム名, obj={}
for(var i=0,L=arrタグ_選択.length; i<L ;i++){
タグ名 = arrタグ_選択[i]
タグCD = 全体.objタグ[タグ名]
arr紐づけ = RAS(path紐づけ_tag2item+'/'+タグCD)
for(var j=0,jL=arr紐づけ.length; j<jL ;j++){
アイテムCD = arr紐づけ[j]
if(swOR検索 || i==0){
obj対象アイテムCD[アイテムCD] = true
}else{
// AND検索の場合
if(obj対象アイテムCD[アイテムCD]){ obj[アイテムCD]=true }
}
}
if(!swOR検索 && 1<i){ obj対象アイテムCD = obj }
}
var arr=[]
for(アイテムCD in obj対象アイテムCD){
アイテム名 = 全体.arrアイテム[アイテムCD-0]
arr.push('<button onclick="検索(this.innerText)">'+アイテム名+'</button>')
}
div.innerHTML = arr.join('<br>\r\n')
}
検索=function(アイテム名){
var path=fs.GetAbsolutePathName(inp.value).replace(/\\$/,''), swSpace=0<path.indexOf(' ')?'"':'', swSpaceI=0<アイテム名.indexOf(' ')?'"':''
var cd = 'cd '+swSpace+path+swSpace
var di = 'dir /B /S '+swSpaceI+アイテム名+swSpaceI
var pathTMP = gsf2+'/'+fs.GetTempName()
var dr = path.match(/^([A-Z]\:)/) ? (RegExp.$1+' & ') : ''
shell.run('cmd /C '+dr+cd+' & '+di+' > '+pathTMP, 0, true)
var pathI = Read(pathTMP), swSpace2=0<pathI.indexOf(' ')?'"':''
if(!pathI && fs.FolderExists(path+'\\'+アイテム名)){ pathI = path+'\\'+アイテム名 }
if(pathI){
pathI = pathI.split('\r\n')[0]
shell.run((!fs.FileExists(pathI) ? 'explorer ':'')+swSpace2+pathI+swSpace2)
}else{
alert('アイテムが見つかりません')
}
fs.DeleteFile(pathTMP)
}
}
makeHTA(str, fun, 'アイテム検索')
}
function 共用(){
fs = new ActiveXObject('Scripting.FileSystemObject')
shell = new ActiveXObject('WScript.Shell')
Read = function(path){var r=fs.OpenTextFile(path,1,true), v=r.AtEndOfStream?'':r.ReadAll(); r.Close(); return v}
RAS = function(path){var str=Read(path); return str.length ? str.split('\r\n') : []}
Write = function(path,str){var w=fs.CreateTextFile(path); w.Write(str); w.Close()}
gsf2 = fs.GetSpecialFolder(2)
iT='innerText', FI='firstChild'
makeHTA=function(bodyInner, fun, title){
var path=gsf2+'/'+fs.GetTempName()+'.hta'
var str=[
'<html><title>'+title+'</title><body>',bodyInner,'</body><','script>',[
'',
'基準フォルダ = "'+基準フォルダ.replace(/\\/g,'\\\\')+'/a"',
共用,
'fun='+fun,
'onload=function(){共用();fun()}'
].join('\r\n'),'<','/script></html>'
].join('')
Write(path, str)
shell.run(path,1,true)
fs.DeleteFile(path)
}
基準フォルダ = fs.GetParentFolderName(基準フォルダ)
pathタグ = 基準フォルダ+'/タグ.txt'
pathアイテム = 基準フォルダ+'/アイテム.txt'
path紐づけ = 基準フォルダ+'/紐づけ'
path紐づけ_tag2item = path紐づけ+'/tag2item'
path紐づけ_item2tag = path紐づけ+'/item2tag'
if(!fs.FolderExists(path紐づけ )){fs.CreateFolder(path紐づけ )}
if(!fs.FolderExists(path紐づけ_tag2item)){fs.CreateFolder(path紐づけ_tag2item)}
if(!fs.FolderExists(path紐づけ_item2tag)){fs.CreateFolder(path紐づけ_item2tag)}
arr2obj = function(arr){
// 配列になっている値をオブジェクトのプロパティ名にして、そのオブジェクトを返す。
for(var i=0,L=arr.length,obj={}; i<L ;i++){ obj[arr[i]] = i }
return obj
}
アイテム追加=function(arr){
var arr新規=get新規要素の配列(全体.objアイテム, arr)
if(!arr新規.length){return}
全体.arrアイテム = 全体.arrアイテム.concat( arr新規 )
全体.objアイテム = arr2obj(全体.arrアイテム)
Write(pathアイテム, 全体.arrアイテム.join('\r\n'))
}
タグ追加=function(arr){
var arr新規=get新規要素の配列(全体.objタグ, arr)
if(!arr新規.length){return}
全体.arrタグ = 全体.arrタグ.concat( arr新規 )
全体.objタグ = arr2obj(全体.arrタグ)
Write(pathタグ, 全体.arrタグ.join('\r\n'))
}
get新規要素の配列=function(obj元, arr新){
for(var arr=[],i=0,L=arr新.length; i<L ;i++){
if(obj元[arr新[i]]!=undefined){ continue }
arr.push( arr新[i] )
}
return arr
}
紐づけ更新=function(arrアイテム, arrタグ){
// アイテムのタグの増減と、タグのアイテムの増減をファイルに反映する。
var アイテム名, アイテムCD, タグ名, タグCD, 紐づけitem2tag={}, 紐づけtag2item={}, i, L, j, jL, obj更新タグ={}, strタグcode=name2code(arrタグ, 全体.objタグ).join('\r\n')
for(i=0,L=arrタグ.length; i<L ;i++){
タグ名 = arrタグ[i]
タグCD = 全体.objタグ[タグ名]
紐づけtag2item[タグ名] = RAS(path紐づけ_tag2item+'/'+タグCD)
}
for(i=0,L=arrアイテム.length; i<L ;i++){
アイテム名 = arrアイテム[i]
アイテムCD = 全体.objアイテム[アイテム名]
紐づけitem2tag[アイテム名] = RAS(path紐づけ_item2tag+'/'+アイテムCD)
var arrタグ名=code2name(紐づけitem2tag[アイテム名], 全体.arrタグ), arr削除対象=get無い物リスト(arrタグ名, arrタグ), arr
for(j=0,jL=arr削除対象.length; j<jL ;j++){
タグ名 = arr削除対象[j]
arr = 紐づけtag2item[タグ名] || RAS(path紐づけ_tag2item+'/'+全体.objタグ[タグ名])
紐づけtag2item[タグ名] = del要素(arr, アイテムCD)
obj更新タグ[タグ名] = true
}
var arr追加対象=get無い物リスト(arrタグ, arrタグ名), arr
for(j=0,jL=arr追加対象.length; j<jL ;j++){
タグ名 = arr追加対象[j]
arr = 紐づけtag2item[タグ名] || RAS(path紐づけ_tag2item+'/'+全体.objタグ[タグ名])
紐づけtag2item[タグ名] = arr.concat([アイテムCD])
obj更新タグ[タグ名] = true
}
Write(path紐づけ_item2tag+'/'+アイテムCD, strタグcode)
}
for(タグ名 in obj更新タグ){
Write(path紐づけ_tag2item+'/'+全体.objタグ[タグ名], 紐づけtag2item[タグ名].join('\r\n'))
}
}
get無い物リスト=function(arr探し物, arr探す場所){
for(var i=0,L=arr探し物.length,探し物,j,jL=arr探す場所.length,sw有,arr無い物=[]; i<L ;i++){
探し物 = arr探し物[i]
for(j=0,sw有=false; j<jL ;j++){ if(arr探す場所[j]==探し物){ sw有=true;break } }
if(sw有){continue}
arr無い物.push(探し物)
}
return arr無い物
}
code2name = function(arrCD, arrAllName){
for(var arr=[],i=0,L=arrCD.length;i<L;i++){ arr[i]=arrAllName[arrCD[i]] }
return arr
}
name2code = function(arrName, obj){
for(var arr=[],i=0,L=arrName.length;i<L;i++){ arr[i]=obj[arrName[i]] }
return arr
}
del要素=function(arr, val){
for(var i=0,L=arr.length;i<L;i++){ if(arr[i]==val){ return arr.slice(0,i).concat(arr.slice(i+1,L)) } }
}
// 各関数から利用するファイルをあらかじめ読み込んでおく
全体={
arrアイテム:RAS(pathアイテム),
arrタグ :RAS(pathタグ)
}
全体.objアイテム = arr2obj(全体.arrアイテム)
全体.objタグ = arr2obj(全体.arrタグ )
}
基準フォルダ = WScript.ScriptFullName
共用()
onload()
</script>
</job>
view raw tags.wsf hosted with ❤ by GitHub


幾つかのデメリット…というか既知の問題があります。
  • タグを設定したファイルやフォルダは名前さえ変えなければ開けますが、逆に言うと名前を変えたら開けません。基本的にフォルダを移動することはあっても名前を変えることは無いものを管理するのが目的でしたので…。

  • 同じ名前で保存先が異なるファイル(フォルダ)は区別することができません。

  • 登録済みタグとアイテムのリストをプログラム起動時に全て読み込んでいるので、登録件数が数万件以上になると起動が遅くなりそうです。

  • アイテム検索画面で「検索」ボタンの動作は登録件数が多くても、わりと短時間で終了すると思いますが、アイテムを開く処理は「dir」コマンドでサブフォルダも含めて検索して開いていますので、検索するフォルダ内のファイルやフォルダの数が多いと遅くなる恐れがあります。

しばらく使ってみて修正したくなったら修正します。

0 件のコメント:

コメントを投稿