2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Finderで気軽にファイルをバージョニング・待避するAppleScript

Posted at

gitとかやるまでもないんだよねー、という場合ありますよね、ないですか。
単に待避フォルダに移動・コピーはAppleScriptでずっとしていたんですが、ちょっとだけ豪華にしました。

適当に待避用フォルダを作成して、ファイルを入れる毎に日時フォルダに分別してコピーまたは移動し、移動の理由も付けておきたいな、という時にもテキストファイルで理由を保存しておけます。
また、複数の待避フォルダがあっても別々に使用したり、一つにマージしたりできます。

これで動作を想像してください。

元フォルダ移動.png

こう聞いてきて
SS_Finder_20250401-140340.png

こんな感じになります。

SS_CleanShot_20250401-140221.png

そのままでもスクリプトエディタやスクリプトメニューから実行できますが、気軽に使うためにはKeyboard Maestroのexecute AppleScriptで実行させましょう。

に待避フォルダ用のiconファイルとscript本体があります。
・icnsファイルは、ホームフォルダのPictures(写真?画像?元からあるものです)に入れてください
・scptは↑のようにスクリプトメニューからでもいいですが、Keyboard Maestroでマクロ作った方が楽しいです

※ほぼChatGPT4oですが、結構な時間かかりました。

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"

property targetBaseFolderName : "■待避:"
property searchDepthLimit : 4
property dateTimeFormat : "%y%m%d-%H%M%S"
property reasonFileName : "★待避理由.txt"
property newFolderTagName : "重要"

tell application "Finder"
	try
		set selectedItems to my getSelectedItems()
		set sourceParentFolder to container of item 1 of selectedItems as alias
		set sourceParentName to name of sourceParentFolder
		
		if my isInsideTargetFolder(selectedItems, targetBaseFolderName) then
			display alert "処理中止" message "選択された項目が「" & targetBaseFolderName & "」フォルダ自体、またはその中にあるようです。" as warning
			return
		end if
		
		set targetBaseFolder to my findTargetBaseFolder(sourceParentFolder)
		if targetBaseFolder is not missing value then
			set targetBaseFolderName to name of targetBaseFolder
		end if
		
		if targetBaseFolder is missing value then
			set userSuffix to my promptForFolderSuffix()
			set targetBaseFolderName to targetBaseFolderName & userSuffix
			set targetBaseFolder to my findTargetBaseFolder(sourceParentFolder)
			if targetBaseFolder is missing value then
				set targetBaseFolder to my createTargetBaseFolder(sourceParentFolder)
			end if
		end if
		
		set {chosenAction, reasonText} to my promptForAction(sourceParentName, targetBaseFolder)
		
		if targetBaseFolder is missing value then
			display alert "エラー" message "ターゲットフォルダが見つからず、処理を続行できません。" as critical
			return
		end if
		my ensureFolderTagOverwrite(targetBaseFolder, newFolderTagName)
		
		set subFolder1 to my getOrCreateFolder(targetBaseFolder, sourceParentName)
		set dateTimeFolderName to do shell script "date +" & quoted form of dateTimeFormat
		set destinationFolder to my createFolder(subFolder1, dateTimeFolderName)
		
		try
			if chosenAction is "移動" then
				move selectedItems to destinationFolder
			else if chosenAction is "コピー" then
				duplicate selectedItems to destinationFolder
			end if
			if reasonText is not "" then my writeReasonFile(destinationFolder, reasonText)
		on error errMsg
			try
				delete destinationFolder
			end try
			display alert "エラー" message chosenAction & "処理中にエラーが発生しました。" & return & errMsg as critical
		end try
	end try
end tell

on getSelectedItems()
	tell application "Finder"
		set selectedList to selection
		if selectedList is {} then
			display alert "エラー" message "項目が選択されていません。" as critical
			error number -128
		end if
		return selectedList
	end tell
end getSelectedItems

on isInsideTargetFolder(targetItems, baseName)
	if targetItems is {} then error number -128
	tell application "Finder"
		set firstSelectedItem to item 1 of targetItems
		if name of firstSelectedItem starts with baseName then return true
		set checkFolderRef to container of firstSelectedItem
		repeat until false
			try
				if name of checkFolderRef starts with baseName then return true
				set checkFolderRef to container of checkFolderRef
			on error
				exit repeat
			end try
		end repeat
	end tell
	return false
end isInsideTargetFolder

on promptForAction(parentFolderName, targetFolder)
	set targetFolderName to name of targetFolder
	set initialAnswer to (linefeed & linefeed & linefeed & linefeed)
	set dialogResult to display dialog "操作を選び、必要なら理由を入力してください:" & return & return & "移動/コピー先:" & return & "「" & targetFolderName & "」>「" & parentFolderName & "」>(日時フォルダ)" default answer initialAnswer buttons {"キャンセル", "コピー", "移動"} default button "移動"
	if button returned of dialogResult is "キャンセル" then error number -128
	set actionResult to button returned of dialogResult
	set reasonResult to text returned of dialogResult
	return {actionResult, reasonResult}
end promptForAction

on promptForFolderSuffix()
	set suffixDialog to display dialog "待避フォルダがありません。作成しますか?" & return & "これから作成する待避フォルダ名に、テキストを追加できます。" default answer "" buttons {"キャンセル", "作成"} default button "作成"
	
	if button returned of suffixDialog is "キャンセル" then
		error number -128
	else
		return text returned of suffixDialog
	end if
end promptForFolderSuffix

on findTargetBaseFolder(startingFolder)
	tell application "Finder"
		set currentFolderRef to startingFolder
		repeat searchDepthLimit times
			try
				set folderList to folders of currentFolderRef
				set foundFolders to {}
				repeat with subFolderRef in folderList
					if name of subFolderRef starts with targetBaseFolderName then set end of foundFolders to subFolderRef
				end repeat
				if (count of foundFolders) = 1 then
					return item 1 of foundFolders
				else if (count of foundFolders) > 1 then
					set folderNames to {}
					repeat with f in foundFolders
						set end of folderNames to name of f
					end repeat
					
					set chosenName to my promptFolderSelection(folderNames)
					
					if chosenName is false then return missing value
					if class of chosenName is text then
						repeat with f in foundFolders
							if name of f is chosenName then return f
						end repeat
					else if class of chosenName is list then
						set primaryFolderName to item 1 of chosenName
						set primaryFolder to missing value
						repeat with f in foundFolders
							if name of f is primaryFolderName then
								set primaryFolder to f
								exit repeat
							end if
						end repeat
						if primaryFolder is missing value then return missing value
						repeat with f in foundFolders
							set srcPath to POSIX path of (f as alias)
							set dstPath to POSIX path of (primaryFolder as alias)
							if srcPath is not equal to dstPath then
								do shell script "/usr/bin/ditto " & quoted form of srcPath & " " & quoted form of dstPath
								delete f
							end if
						end repeat
						return primaryFolder
					end if
				end if
				if class of container of currentFolderRef is desktop folder then exit repeat
				set currentFolderRef to container of currentFolderRef
			on error errMsg number errNum
				if errNum is -128 then
					error number -128
				else
					exit repeat
				end if
			end try
		end repeat
	end tell
	return missing value
end findTargetBaseFolder

on promptFolderSelection(folderNames)
	set chosenFolder to choose from list folderNames with prompt "同一階層に複数の待避フォルダが見つかりました" & return & "使用する待避フォルダを選んでください:" without multiple selections allowed
	if chosenFolder is false then error number -128
	set selectedName to item 1 of chosenFolder
	set confirmDialog to display dialog "選択した待避フォルダ: " & selectedName & return & return & "このフォルダを使用しますか?それとも全ての待避フォルダをこのフォルダにマージしますか?" & return & "※ 同一の時刻のフォルダがある場合は上書きされます" buttons {"キャンセル", "全てマージ", "このフォルダを使用"} default button "このフォルダを使用"
	set clickedButton to button returned of confirmDialog
	
	if clickedButton is "キャンセル" then error number -128
	if clickedButton is "このフォルダを使用" then return selectedName
	if clickedButton is "全てマージ" then return chosenFolder
	error number -128
end promptFolderSelection

on createTargetBaseFolder(referenceFolder)
	try
		set parentFolderRef to container of referenceFolder
	on error
		set parentFolderRef to referenceFolder
	end try
	set parentFolderAlias to parentFolderRef as alias
	set creationLocationRef to choose folder with prompt "「" & targetBaseFolderName & "」フォルダを作成する場所を選択してください:" default location parentFolderAlias
	if creationLocationRef is false then error number -128
	tell application "Finder"
		set newBaseFolderRef to make new folder at creationLocationRef with properties {name:targetBaseFolderName}
	end tell
	try
		set iconPath to POSIX path of (path to pictures folder) & "hfi.icns"
		set imageData to (current application's NSImage's alloc()'s initWithContentsOfFile:iconPath)
		(current application's NSWorkspace's sharedWorkspace()'s setIcon:imageData forFile:(POSIX path of (newBaseFolderRef as alias)) options:2)
	on error
		-- skip icon error
	end try
	return newBaseFolderRef
end createTargetBaseFolder

on createFolder(parentRef, folderLabel)
	if parentRef is missing value or folderLabel is "" then
		error number -128
	end if
	tell application "Finder"
		return make new folder at parentRef with properties {name:folderLabel}
	end tell
end createFolder

on writeReasonFile(destinationRef, userReasonText)
	if destinationRef is missing value then error number -128
	try
		set trimmedReasonText to do shell script "echo " & quoted form of userReasonText & " | tr -d '[:space:]'"
		if trimmedReasonText is "" then return
		set reasonFileFullPath to POSIX path of (destinationRef as alias) & reasonFileName
		do shell script "echo " & quoted form of userReasonText & " > " & quoted form of reasonFileFullPath
		set urlClass to current application's NSURL
		set tagKey to current application's NSURLTagNamesKey
		set fileURL to urlClass's fileURLWithPath:reasonFileFullPath
		fileURL's setResourceValue:{newFolderTagName} forKey:tagKey |error|:(missing value)
	on error errMsg
		display alert "警告" message "理由ファイルの作成またはタグ付けに失敗しました。" & return & errMsg as warning
	end try
end writeReasonFile

on ensureFolderTagOverwrite(targetFolderRef, tagName)
	if targetFolderRef is missing value or tagName is "" then error number -128
	try
		set folderAlias to targetFolderRef as alias
		set folderPathText to POSIX path of folderAlias
		set folderURL to current application's NSURL's fileURLWithPath:folderPathText
		set tagKey to current application's NSURLTagNamesKey
		folderURL's setResourceValue:{tagName} forKey:tagKey |error|:(missing value)
	on error errMsg
		display alert "タグ設定エラー" message errMsg as warning
	end try
end ensureFolderTagOverwrite

on getOrCreateFolder(parentRef, childName)
	if parentRef is missing value or childName is "" then error number -128
	try
		set parentAlias to parentRef as alias
		tell application "Finder"
			try
				return folder childName of folder parentAlias
			on error
				return make new folder at folder parentAlias with properties {name:childName}
			end try
		end tell
	on error errMsg
		display alert "フォルダ作成エラー" message errMsg as warning
		return missing value
	end try
end getOrCreateFolder

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?