How to use Applescript to create the same new file in all subfolders?

I am looking to create a .DS_Store file (Mac) in every folder and subfolder where one does not exist, and then to lock every .DS_Store file both newly written and already existing.

I’ve written the following code, which of course does not work. I’d appreciate some help getting this right.

set MyFolder to "Macintosh Backup:Design Photos"
tell application "Finder"
    set SubFolders to every folder of entire contents of MyFolder
    repeat with xFolder in SubFolders
        set myFile to path of xFolder & ":.DS_Store"
        if not (exists myFile) then
            make new file at xFolder with properties {name:".DS_Store"}
        end if
        set locked of myFile to true
    end repeat
end tell

Hi. This should work, but minimal testing was done.

recurse(choose folder)


on recurse(theFolder)
	tell application "Finder"
		repeat with targetFolder in (theFolder's folders as alias list)
			if not (exists item ((targetFolder as text) & ".DS_Store")) then
				make file at targetFolder with properties {name:".DS_Store", locked:true}
			else
				set item ((targetFolder as text) & ".DS_Store")'s locked to true
			end if
			my recurse(targetFolder)
		end repeat
	end tell
end recurse

Works like a charm. Thank you so much!

If I could ask you for one more additional feature, and by way of this explain what I am trying to do.

As you know, for some ridiculous reason Apple’s Finder has always changed its modification date even when only viewing the contents because the invisible .DS_Store file updates itself.

How or why Apple cannot figure out that the modification date should only account for visible files is beyond me, but many have written and complained about this over the years (those of us who want to maintain the dates as they should be) and it’s never going o be fixed.

So, the method that I use as a workaround for this problem is to first lock the .DS_Store file so that it cannot be changed, and then to reset the folder’s modification date to the most recent file it contains. I haven’t found any negative consequence from doing so, but admittedly I am only doing this for archived folders where I am not adding new files, in other words, not active folders. I assume the only effect is that, for example, the view type is not changed from whatever is recorded in the file, but that’s about it.

Can you think of any negative effects of this method? What happens, for example, if I add a new file to a folder with a locked .DS_Store file? Would the Finder never know about it? I’m not totally sure what the .DS_Store file contains and does anyway.

I’m curious to know what your thoughts are about this, and would ask if you could add some code to the routing that would, after locking the .DS_Store file, reset the modification date of every folder to the most recent item (file or folder) it contains, so that I can achieve the entire process in one script.

Thanks again!

One more thing…

Your script processes all folders within the target folder, but I need to also include the target folder as well. Can you please modify the script so that it would work on both the selected folder and all folders within it, no matter how many subfolders within it.

Thanks.

DS Store files track icon and folder positions. Their functional value may be nil, dependent on your view settings. Getting the mod date will require adding in checks for files existing, sorting them, and then using the first item’s mod date; it wouldn’t be particularly efficient, and it may be just as well to delete the invisible DS files. If you just want to add the file to the target’s root level, then you could either select a folder one higher up or change the first line to:

[format]set wasChosen to (choose folder)
#root level business
tell application “Finder” to try
make file at wasChosen with properties {name:“.DS_Store”, locked:1}
end try
#subfolder business
recurse(wasChosen)[/format]

Hi - I think I’ve got this mostly right using your advice. I was also able to get the modification date code to work with just one line and without any sorting. Can you take a look at everything for me to see if it needs improving?

The one issue I’m having is that while it works to get the last mod date of contained files, it seems to be skipping over contained folders, so how can I get this to work where it sets the modification date of a folder to the latest modification date of any contained within it, be it a file or folder?

set theFolder to (choose folder with prompt "Please select the folder to set the modification date by its contents:")
tell application "Finder"
	if not (exists item ((theFolder as text) & ".DS_Store")) then
		make file at theFolder with properties {name:".DS_Store", locked:true}
	else
		set item ((theFolder as text) & ".DS_Store")'s locked to true
	end if
	set modification date of theFolder to modification date of last file of theFolder
end tell

recurse(theFolder)

on recurse(theFolder)
	tell application "Finder"
		repeat with targetFolder in (theFolder's folders as alias list)
			if not (exists item ((targetFolder as text) & ".DS_Store")) then
				make file at targetFolder with properties {name:".DS_Store", locked:true}
			else
				set item ((targetFolder as text) & ".DS_Store")'s locked to true
			end if
			set modification date of targetFolder to modification date of last item of targetFolder
			my recurse(targetFolder)
		end repeat
	end tell
end recurse

Oish! Now that I’m testing this out, the problem is not the code line itself:

“set modification date of targetFolder to modification date of last item of targetFolder”

which works fine.

The problem is that it needs to be doing this recursively from the inside out (i.e. the deepest folder level back up the tree) so that it can take the subfolders’ dates into account when it sets its own date, which it cannot do when it is performing this action from the top down.

I suppose one method would be to perform this process twice, because by that point it should pick up on the subfolder dates, but maybe there’s a better way to do this?

You’ll want to test this with files you can see, as the “last file” is likely alphabetically last. If you want to obtain objects by time, they have to be sorted by date.

set wasChosen to (choose folder)
#root level business
tell application "Finder" to try
	make file at wasChosen with properties {name:".DS_Store", locked:1}
end try
#subfolder business
recurse(wasChosen)

on recurse(theFolder)
	tell application "Finder"
		repeat with targetFolder in (theFolder's folders as alias list)
			if (exists item ((targetFolder as text) & ".DS_Store")) then
				set (item ((targetFolder as text) & ".DS_Store"))'s locked to 1
			else
				make file at targetFolder with properties {name:".DS_Store", locked:1} --current date as mod date
			end if
			try --if contains file(s)
				set targetFolder's modification date to ((sort targetFolder's files by modification date)'s item -1's modification date)
			end try
			my recurse(targetFolder)
		end repeat
	end tell
end recurse

Edited for clarity

Not sure what you’re trying to do here.

To make this more visual…

FOLDER 1

File 1
FOLDER 2

File 2
FOLDER 3

File 3
Folder 4

So, Folder 3’s date needs to be set first by getting the most recent of File 3 or Folder 4. Only then can Folder 2’s date be set by getting the most recent of File 2 or Folder 3. And only then can Folder 1’s date be set by getting the most recent of File 1 or Folder 2.

Also, you raise a good point because I had assumed my “last item” was organically choosing the last item by date but in fact these files are all named by the dates so it is only working by coincidence. So it looks like I am going to need to have some sorting done in order to make sure it is genuinely choosing the most recent by modification date, regardless of the name.

And since it makes no difference what order the .DS_Store files are added to/locked in the folders, if there is a way to “reverse recurse” this from the bottom up, both processes (adding/locking the .DS_Store file and changing the modification dates of the folder) can still work together on each folder as before. We just need to change the order of where it starts and where it finishes.

Hi pavilion.

Would you mind wrapping any AppleScript code in this forum’s [applescript] and [/applescript] tags when posting it here? They make it appear as in Marc’s replies above: in a box with a clickable link which opens it in people’s default script editors. There’s a button for them just above the text field when you post.

Thanks.

Thanks for pointing that out to me. I have gone back and done that.

Now, if you can help me figure out how to perform a reverse recurse… :slight_smile:

This works on my Mojave machine:

on recurse(thisFolder)
	-- Recursively burrow down through this folder's subfolders.
	tell application "Finder" to set folderList to thisFolder's folders as alias list
	repeat with subfolder in folderList
		recurse(subfolder)
	end repeat
	
	-- At the bottom of each recursion branch, or on the way back:
	tell application "Finder"
		-- Sort this folder's contents (if any) by modification date.
		-- The result returned won't include any existing hidden ".DS_Store" file.
		set sortedContents to (sort thisFolder's items by modification date)
		-- If the folder has no visible contents, get its modification date;
		-- otherwise get that of the last item in the returned list.
		if (sortedContents is {}) then
			set lastModDate to thisFolder's modification date
		else
			set lastModDate to modification date of end of sortedContents
		end if
	end tell
	-- Make a ".DS_Store" file in the folder if one doesn't exist already.
	set DSPath to (thisFolder as text) & ".DS_Store"
	try
		close access (open for access file DSPath)
	end try
	tell application "Finder"
		-- Lock the ".DS_Store" file. The Finder can access it through an 'alias' specifier,
		-- but not through one of its own 'file' or 'item' specifiers.
		set locked of alias DSPath to true
		-- (Re)set the folder's modification date to the date obtained above.
		set thisFolder's modification date to lastModDate
	end tell
end recurse

recurse(choose folder)

The deepest subfolder can only contain files, so the diagram’s pattern is inconsistent, and there doesn’t appear to be a reason to consider folder dates or work from the bottom up. I’ve edited my last post (#8).

Nigel- this works perfectly, thanks so much.

The sort routine is taking forever with folders containing thousands of files, though. Is there a quicker way to achieve this, perhaps through a shell script line or something?

Exists a quicker way to achieve this. Using System Events instead of Finder, or AsObjC. Here is using System Events script:


set MyFolder to choose folder
tell application "System Events" to my recurse(get folder (MyFolder as text))

on recurse(the_folder)
	tell application "System Events"
		-- remember last modification date of the_folder
		set lastModDate to modification date of the_folder
		-- Make a ".DS_Store" file in the folder if one doesn't exist already.
		set myFile to file ((path of the_folder) & ":.DS_Store")
		if not (exists myFile) then set myFile to make new file at the_folder with properties {name:".DS_Store"}
		-- Lock the ".DS_Store" file. The Finder can access it through an 'alias' specifier,
		-- but not through one of its own 'file' or 'item' specifiers.
		set {DSPath, folderPath} to {path of myFile, path of the_folder}
		tell application "Finder" to set locked of alias DSPath to true
		-- restore modification date of the_folder
		set modification date of the_folder to lastModDate
		-- process subfolders
		repeat with subFolder in (get folders of the_folder)
			my recurse(subFolder)
		end repeat
	end tell
end recurse

I hadn’t appreciated that folders with thousands of items were involved.

Although simplest codewise, it’s not actually necessary to sort each folder’s contents. It’s much faster to dump the contents’ modification dates to the script and let it find the most recent.

on lockDSStoreFiles(topFolder)
	script o
		property modDates : {}
		
		on recurse(thisFolder)
			-- Recursively burrow down through this folder's subfolders.
			tell application "Finder" to set folderList to thisFolder's folders as alias list
			repeat with subfolder in folderList
				recurse(subfolder)
			end repeat
			
			-- At the bottom of each recursion branch, or on the way back:
			-- Get the modification date of every visible item in this folder.
			-- Surprisingly, this doesn't error (in Mojave) if the folder's empty, but returns an empty list.
			tell application "Finder" to set o's modDates to modification date of thisFolder's items
			-- If the result /is/ an empty list, get the folder's modification date;
			-- otherwise find the most recent date in the list.
			if (o's modDates is {}) then
				tell application "Finder" to set lastModDate to thisFolder's modification date
			else
				set lastModDate to beginning of o's modDates
				repeat with i from 2 to (count o's modDates)
					if (item i of my modDates comes after lastModDate) then set lastModDate to item i of o's modDates
				end repeat
			end if
			-- Make a ".DS_Store" file in the folder if one doesn't exist already.
			set DSPath to (thisFolder as text) & ".DS_Store"
			try
				close access (open for access file DSPath)
			end try
			tell application "Finder"
				-- Lock the ".DS_Store" file. The Finder can access it through an 'alias' specifier,
				-- but not through one of its own 'file' or 'item' specifiers.
				set locked of alias DSPath to true
				-- (Re)set the folder's modification date to the date obtained above.
				set thisFolder's modification date to lastModDate
			end tell
		end recurse
	end script
	
	o's recurse(topFolder)
end lockDSStoreFiles

lockDSStoreFiles(choose folder)

Otherwise, as KniazidisR says, it’s System Events or ASObjC.

Nigel - that did the trick. Processing 12 sample folders with 5,000 each took 3:10 minutes with the former script, and only 18 seconds with the latter!

Having said that, would it be either wise or necessary to add a “with timeout of…” preface for processing hundreds of thousands of files at once, or do I misunderstand how that works, since the Finder is constantly working and thus never idle?

Apropos of that, I have noticed in some of my scripts that copy Gigabytes of files, that even when I add a long "with timeout of…) in the range of 50,000 seconds, it sometimes still times out.

Curious to understand why this would happen and whether it would apply here as well?

In other words, is there some reason why events handled by the Finder (tell application “Finder”) would not comply with the “with timeout of x seconds” condition set in the script?

Hi pavilion.

It’s hard to tell without seeing your script. As you may know, a ‘with timeout …’ statement governs how long a script will wait for replies to commands in the statement that are sent to an application (other than the one running the script) before throwing an error. I think the default’s 30 seconds. If the application’s likely to take longer than this to execute a single command sent to it, it’s a good idea to put such commands in a ‘with timeout …’ statement. If the action’s simply a long process involving many commands that the application can individually complete within 30 seconds, there’s no point.

On the other hand, if the Finder’s sent a single command to copy gigabytes of files in one go, it might just seize up of its own accord, in which case it’ll make no difference how long a script waits for it to finish and report back.

In my case the Finder finishes copying the gigabytes of files each time without problem, so that’s not causing the issue per se. The problem is that the steps after the copying which the script is supposed to take care of get skipped because it times out, presumably waiting for the file copy to finish. What I don’t understand, though, is that at most it takes 30-45 minutes to complete, so you would think setting the timeout for 5,000 seconds would fix this, but unfortunately, sometimes it doesn’t work.

Here is another script that has similar issues. This one takes a folder of epoch-named files and converts them to a standard timestamp before moving them from Downloads into another folder. As you can see, I’ve set a very generous timeout number, but the script still timeouts frequently. Maybe you can identify something else that could be the problem?

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

with timeout of 50000 seconds
	tell application "Finder"
		set theFiles to folder "Hard Drive:Users:Macintosh:Downloads"
		set theLocation to folder "Hard Drive:Users:Macintosh:Documents:Security Videos"
		set theFolder to "Hard Drive:Users:Macintosh:Documents:Security Videos:Security Videos"
		set theFiles to "Hard Drive:Users:Macintosh:Downloads" as alias
		if not (exists folder theFolder) then
			make new folder at theLocation with properties {name:"Security Videos"}
		end if
		set fileNames to name of every file of theFiles
	end tell
	set dateFormatter to current application's class "NSDateFormatter"'s new()
	tell dateFormatter to setDateFormat:("yyyy/MM/dd HH_mm_ss")
	set usedEpochSeconds to {}
	set {TID, text item delimiters} to {text item delimiters, "."}
	repeat with originalName in fileNames
		set epochSeconds to (text 1 thru text item -2 of originalName) div 1000
		set originalDate to (current application's class "NSDate"'s dateWithTimeIntervalSince1970:(epochSeconds))
		set originalDate to (dateFormatter's stringFromDate:(originalDate)) as text
		set text item delimiters to {space, "/", "_"}
		tell (current date) to set {newModDate, day, {year, its month, day, its hours, its minutes, its seconds}} to {it, 1, originalDate's text items}
		set text item delimiters to "."
		repeat while (usedEpochSeconds contains epochSeconds)
			set epochSeconds to epochSeconds + 1
		end repeat
		set end of usedEpochSeconds to epochSeconds
		set nameDate to (current application's class "NSDate"'s dateWithTimeIntervalSince1970:(epochSeconds))
		set newName to ((dateFormatter's stringFromDate:(nameDate)) as text)
		set newName to text 1 thru 11 of newName & "|" & text 11 thru -1 of newName & ("." & text item -1 of originalName)
	end repeat
	tell application "Finder"
		move every file of theFiles to theFolder
	end tell
end timeout